diff --git a/.chloggen/resourcedetectionprocessor_support_openshift.yaml b/.chloggen/resourcedetectionprocessor_support_openshift.yaml new file mode 100755 index 000000000000..1bd88d444e8b --- /dev/null +++ b/.chloggen/resourcedetectionprocessor_support_openshift.yaml @@ -0,0 +1,16 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: resourcedetectionprocessor + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: add openshift support + +# One or more tracking issues related to the change +issues: [15694] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d9d81fac9ae6..cb12d56efc9b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -141,6 +141,7 @@ processor/resourcedetectionprocessor/ @open-telemetry/collect processor/resourceprocessor/ @open-telemetry/collector-contrib-approvers @dmitryax processor/resourcedetectionprocessor/internal/azure/ @open-telemetry/collector-contrib-approvers @mx-psi processor/resourcedetectionprocessor/internal/heroku/ @open-telemetry/collector-contrib-approvers @atoulme +processor/resourcedetectionprocessor/internal/openshift/ @open-telemetry/collector-contrib-approvers @frzifus processor/routingprocessor/ @open-telemetry/collector-contrib-approvers @jpkrohling @kovrus processor/schemaprocessor/ @open-telemetry/collector-contrib-approvers @MovieStoreGuy processor/servicegraphprocessor/ @open-telemetry/collector-contrib-approvers @jpkrohling @mapno diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index baec9031bce4..8276272b6797 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -131,6 +131,7 @@ body: - processor/resourcedetection - processor/resourcedetection/internal/azure - processor/resourcedetection/internal/heroku + - processor/resourcedetection/internal/openshift - processor/routing - processor/schema - processor/servicegraph diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 90b0d0b50e71..513201bc3bd9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -125,6 +125,7 @@ body: - processor/resourcedetection - processor/resourcedetection/internal/azure - processor/resourcedetection/internal/heroku + - processor/resourcedetection/internal/openshift - processor/routing - processor/schema - processor/servicegraph diff --git a/.github/ISSUE_TEMPLATE/other.yaml b/.github/ISSUE_TEMPLATE/other.yaml index 8350177c42b4..94d444835adf 100644 --- a/.github/ISSUE_TEMPLATE/other.yaml +++ b/.github/ISSUE_TEMPLATE/other.yaml @@ -125,6 +125,7 @@ body: - processor/resourcedetection - processor/resourcedetection/internal/azure - processor/resourcedetection/internal/heroku + - processor/resourcedetection/internal/openshift - processor/routing - processor/schema - processor/servicegraph diff --git a/internal/metadataproviders/openshift/metadata.go b/internal/metadataproviders/openshift/metadata.go new file mode 100644 index 000000000000..199e0646c7c6 --- /dev/null +++ b/internal/metadataproviders/openshift/metadata.go @@ -0,0 +1,208 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openshift // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/metadataproviders/openshift" + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" +) + +// Provider gets cluster metadata from Openshift. +type Provider interface { + K8SClusterVersion(context.Context) (string, error) + OpenShiftClusterVersion(context.Context) (string, error) + Infrastructure(context.Context) (*InfrastructureAPIResponse, error) +} + +// NewProvider creates a new metadata provider. +func NewProvider(address, token string) Provider { + return &openshiftProvider{ + address: address, + token: token, + client: &http.Client{}, + } +} + +type openshiftProvider struct { + client *http.Client + address string + token string +} + +func (o *openshiftProvider) makeOCPRequest(ctx context.Context, endpoint, target string) (*http.Request, error) { + addr := fmt.Sprintf("%s/apis/config.openshift.io/v1/%s/%s/status", o.address, endpoint, target) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr, nil) + if err != nil { + return nil, err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", o.token)) + return req, nil +} + +// OpenShiftClusterVersion requests the ClusterVersion from the openshift api. +func (o *openshiftProvider) OpenShiftClusterVersion(ctx context.Context) (string, error) { + req, err := o.makeOCPRequest(ctx, "clusterversions", "version") + if err != nil { + return "", err + } + resp, err := o.client.Do(req) + if err != nil { + return "", err + } + res := ocpClusterVersionAPIResponse{} + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + return "", err + } + return res.Status.Desired.Version, nil +} + +// ClusterVersion requests Infrastructure details from the openshift api. +func (o *openshiftProvider) Infrastructure(ctx context.Context) (*InfrastructureAPIResponse, error) { + req, err := o.makeOCPRequest(ctx, "infrastructures", "cluster") + if err != nil { + return nil, err + } + resp, err := o.client.Do(req) + if err != nil { + return nil, err + } + res := &InfrastructureAPIResponse{} + if err := json.NewDecoder(resp.Body).Decode(res); err != nil { + return nil, err + } + + return res, nil +} + +// K8SClusterVersion requests the ClusterVersion from the kubernetes api. +func (o *openshiftProvider) K8SClusterVersion(ctx context.Context) (string, error) { + addr := fmt.Sprintf("%s/version", o.address) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr, nil) + if err != nil { + return "", err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", o.token)) + + resp, err := o.client.Do(req) + if err != nil { + return "", err + } + res := k8sClusterVersionAPIResponse{} + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + return "", err + } + version := res.GitVersion + if strings.Contains(version, "+") { + version = strings.Split(version, "+")[0] + } + return version, nil +} + +type ocpClusterVersionAPIResponse struct { + Status struct { + Desired struct { + Version string `json:"version"` + } `json:"desired"` + } `json:"status"` +} + +// InfrastructureAPIResponse from OpenShift API. +type InfrastructureAPIResponse struct { + Status InfrastructureStatus `json:"status"` +} + +// InfrastructureStatus holds cluster-wide information about Infrastructure. +// https://docs.openshift.com/container-platform/4.11/rest_api/config_apis/infrastructure-config-openshift-io-v1.html#apisconfig-openshift-iov1infrastructuresnamestatus +type InfrastructureStatus struct { + // ControlPlaneTopology expresses the expectations for operands that normally + // run on control nodes. The default is 'HighlyAvailable', which represents + // the behavior operators have in a "normal" cluster. The 'SingleReplica' mode + // will be used in single-node deployments and the operators should not + // configure the operand for highly-available operation The 'External' mode + // indicates that the control plane is hosted externally to the cluster and + // that its components are not visible within the cluster. + ControlPlaneTopology string `json:"controlPlaneTopology"` + // InfrastructureName uniquely identifies a cluster with a human friendly + // name. Once set it should not be changed. Must be of max length 27 and must + // have only alphanumeric or hyphen characters. + InfrastructureName string `json:"infrastructureName"` + // InfrastructureTopology expresses the expectations for infrastructure + // services that do not run on control plane nodes, usually indicated by a + // node selector for a role value other than master. The default is + // 'HighlyAvailable', which represents the behavior operators have in a + // "normal" cluster. The 'SingleReplica' mode will be used in single-node + // deployments and the operators should not configure the operand for + // highly-available operation. + InfrastructureTopology string `json:"infrastructureTopology"` + // PlatformStatus holds status information specific to the underlying + // infrastructure provider. + PlatformStatus InfrastructurePlatformStatus `json:"platformStatus"` +} + +// InfrastructurePlatformStatus reported by the OpenShift API. +type InfrastructurePlatformStatus struct { + Aws InfrastructureStatusAWS `json:"aws"` + Azure InfrastructureStatusAzure `json:"azure"` + Baremetal struct{} `json:"baremetal"` + GCP InfrastructureStatusGCP `json:"gcp"` + IBMCloud InfrastructureStatusIBMCloud `json:"ibmcloud"` + OpenStack InfrastructureStatusOpenStack `json:"openstack"` + OVirt struct{} `json:"ovirt"` + VSphere struct{} `json:"vsphere"` + Type string `json:"type"` +} + +// InfrastructureStatusAWS reported by the OpenShift API. +type InfrastructureStatusAWS struct { + // Region holds the default AWS region for new AWS resources created by the + // cluster. + Region string `json:"region"` +} + +// InfrastructureStatusAzure reported by the OpenShift API. +type InfrastructureStatusAzure struct { + // CloudName is the name of the Azure cloud environment which can be used to + // configure the Azure SDK with the appropriate Azure API endpoints. If empty, + // the value is equal to AzurePublicCloud. + CloudName string `json:"cloudName"` +} + +// InfrastructureStatusGCP reported by the OpenShift API. +type InfrastructureStatusGCP struct { + // Region holds the region for new GCP resources created for the cluster. + Region string `json:"region"` +} + +// InfrastructureStatusIBMCloud reported by the OpenShift API. +type InfrastructureStatusIBMCloud struct { + // Location is where the cluster has been deployed. + Location string `json:"location"` +} + +// InfrastructureStatusOpenStack reported by the OpenShift API. +type InfrastructureStatusOpenStack struct { + // CloudName is the name of the desired OpenStack cloud in the client + // configuration file (clouds.yaml). + CloudName string `json:"cloudName"` +} + +// k8sClusterVersionAPIResponse of OpenShift. +// https://docs.openshift.com/container-platform/4.11/rest_api/config_apis/clusterversion-config-openshift-io-v1.html#apisconfig-openshift-iov1clusterversionsnamestatus +type k8sClusterVersionAPIResponse struct { + GitVersion string `json:"gitVersion"` +} diff --git a/internal/metadataproviders/openshift/metadata_test.go b/internal/metadataproviders/openshift/metadata_test.go new file mode 100644 index 000000000000..467af24e05af --- /dev/null +++ b/internal/metadataproviders/openshift/metadata_test.go @@ -0,0 +1,343 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openshift + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewProvider(t *testing.T) { + provider1 := NewProvider("127.0.0.1:4444", "abc") + assert.NotNil(t, provider1) + provider2 := NewProvider("", "") + assert.NotNil(t, provider2) +} + +func TestQueryEndpointFailed(t *testing.T) { + ts := httptest.NewServer(http.NotFoundHandler()) + defer ts.Close() + + provider := &openshiftProvider{ + address: ts.URL, + token: "test", + client: &http.Client{}, + } + + _, err := provider.OpenShiftClusterVersion(context.Background()) + assert.Error(t, err) + + _, err = provider.K8SClusterVersion(context.Background()) + assert.Error(t, err) + + _, err = provider.Infrastructure(context.Background()) + assert.Error(t, err) +} + +func TestQueryEndpointMalformed(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintln(w, "{") + assert.NoError(t, err) + })) + defer ts.Close() + + provider := &openshiftProvider{ + address: ts.URL, + token: "", + client: &http.Client{}, + } + + _, err := provider.Infrastructure(context.Background()) + assert.Error(t, err) +} + +func TestQueryEndpointCorrectK8SClusterVersion(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintf(w, `{ + "major": "1", + "minor": "21", + "gitVersion": "v1.21.11+5cc9227", + "gitCommit": "047f86f8e2212f25394de1c8bad35d9426ae0f4c", + "gitTreeState": "clean", + "buildDate": "2022-09-20T16:39:45Z", + "goVersion": "go1.16.12", + "compiler": "gc", + "platform": "linux/amd64" +}`) + assert.NoError(t, err) + })) + defer ts.Close() + + provider := &openshiftProvider{ + address: ts.URL, + token: "test", + client: &http.Client{}, + } + + got, err := provider.K8SClusterVersion(context.Background()) + require.NoError(t, err) + expect := "v1.21.11" + assert.Equal(t, expect, got) +} + +func TestQueryEndpointCorrectOpenShiftClusterVersion(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintf(w, `{ +"apiVersion": "config.openshift.io/v1", +"kind": "ClusterVersion", +"status": { + "desired": {"version": "4.8.51"} + } +}`) + assert.NoError(t, err) + })) + defer ts.Close() + + provider := &openshiftProvider{ + address: ts.URL, + token: "test", + client: &http.Client{}, + } + + got, err := provider.OpenShiftClusterVersion(context.Background()) + require.NoError(t, err) + expect := "4.8.51" + assert.Equal(t, expect, got) +} + +func TestQueryEndpointCorrectInfrastructureAWS(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintf(w, `{ +"apiVersion": "config.openshift.io/v1", +"kind": "Infrastructure", +"status": { + "apiServerInternalURI": "https://api.myopenshift.com:4443", + "apiServerURL": "https://api.myopenshift.com:4443", + "controlPlaneTopology": "HighlyAvailable", + "etcdDiscoveryDomain": "", + "infrastructureName": "test-d-bm4rt", + "infrastructureTopology": "HighlyAvailable", + "platform": "AWS", + "platformStatus": { + "type": "AWS", + "aws": {"region": "us-east-1"} +}}}`) + assert.NoError(t, err) + })) + defer ts.Close() + + provider := &openshiftProvider{ + address: ts.URL, + token: "test", + client: &http.Client{}, + } + + got, err := provider.Infrastructure(context.Background()) + require.NoError(t, err) + expect := InfrastructureAPIResponse{ + Status: InfrastructureStatus{ + InfrastructureName: "test-d-bm4rt", + ControlPlaneTopology: "HighlyAvailable", + InfrastructureTopology: "HighlyAvailable", + PlatformStatus: InfrastructurePlatformStatus{ + Type: "AWS", + Aws: InfrastructureStatusAWS{ + Region: "us-east-1", + }, + }, + }, + } + assert.Equal(t, expect, *got) +} + +func TestQueryEndpointCorrectInfrastructureAzure(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintf(w, `{ +"apiVersion": "config.openshift.io/v1", +"kind": "Infrastructure", +"status": { + "apiServerInternalURI": "https://api.myopenshift.com:4443", + "apiServerURL": "https://api.myopenshift.com:4443", + "controlPlaneTopology": "HighlyAvailable", + "etcdDiscoveryDomain": "", + "infrastructureName": "test-d-bm4rt", + "infrastructureTopology": "HighlyAvailable", + "platform": "AZURE", + "platformStatus": { + "type": "AZURE", + "azure": {"cloudName": "us-east-1"} +}}}`) + assert.NoError(t, err) + })) + defer ts.Close() + + provider := &openshiftProvider{ + address: ts.URL, + token: "test", + client: &http.Client{}, + } + + got, err := provider.Infrastructure(context.Background()) + require.NoError(t, err) + expect := InfrastructureAPIResponse{ + Status: InfrastructureStatus{ + InfrastructureName: "test-d-bm4rt", + ControlPlaneTopology: "HighlyAvailable", + InfrastructureTopology: "HighlyAvailable", + PlatformStatus: InfrastructurePlatformStatus{ + Type: "AZURE", + Azure: InfrastructureStatusAzure{ + CloudName: "us-east-1", + }, + }, + }, + } + assert.Equal(t, expect, *got) +} + +func TestQueryEndpointCorrectInfrastructureGCP(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintf(w, `{ +"apiVersion": "config.openshift.io/v1", +"kind": "Infrastructure", +"status": { + "controlPlaneTopology": "HighlyAvailable", + "etcdDiscoveryDomain": "", + "infrastructureName": "test-d-bm4rt", + "infrastructureTopology": "HighlyAvailable", + "platform": "GCP", + "platformStatus": { + "type": "GCP", + "gcp": {"region": "us-east-1"} +}}}`) + assert.NoError(t, err) + })) + defer ts.Close() + + provider := &openshiftProvider{ + address: ts.URL, + token: "test", + client: &http.Client{}, + } + + got, err := provider.Infrastructure(context.Background()) + require.NoError(t, err) + expect := InfrastructureAPIResponse{ + Status: InfrastructureStatus{ + InfrastructureName: "test-d-bm4rt", + ControlPlaneTopology: "HighlyAvailable", + InfrastructureTopology: "HighlyAvailable", + PlatformStatus: InfrastructurePlatformStatus{ + Type: "GCP", + GCP: InfrastructureStatusGCP{ + Region: "us-east-1", + }, + }, + }, + } + assert.Equal(t, expect, *got) +} + +func TestQueryEndpointCorrectInfrastructureIBMCloud(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintf(w, `{ +"apiVersion": "config.openshift.io/v1", +"kind": "Infrastructure", +"status": { + "controlPlaneTopology": "HighlyAvailable", + "etcdDiscoveryDomain": "", + "infrastructureName": "test-d-bm4rt", + "infrastructureTopology": "HighlyAvailable", + "platform": "IBMCloud", + "platformStatus": { + "type": "ibmcloud", + "ibmcloud": {"location": "us-east-1"} +}}}`) + assert.NoError(t, err) + })) + defer ts.Close() + + provider := &openshiftProvider{ + address: ts.URL, + token: "test", + client: &http.Client{}, + } + + got, err := provider.Infrastructure(context.Background()) + require.NoError(t, err) + expect := InfrastructureAPIResponse{ + Status: InfrastructureStatus{ + InfrastructureName: "test-d-bm4rt", + ControlPlaneTopology: "HighlyAvailable", + InfrastructureTopology: "HighlyAvailable", + PlatformStatus: InfrastructurePlatformStatus{ + Type: "ibmcloud", + IBMCloud: InfrastructureStatusIBMCloud{ + Location: "us-east-1", + }, + }, + }, + } + assert.Equal(t, expect, *got) +} + +func TestQueryEndpointCorrectInfrastructureOpenstack(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := fmt.Fprintf(w, `{ +"apiVersion": "config.openshift.io/v1", +"kind": "Infrastructure", +"status": { + "controlPlaneTopology": "HighlyAvailable", + "etcdDiscoveryDomain": "", + "infrastructureName": "test-d-bm4rt", + "infrastructureTopology": "HighlyAvailable", + "platform": "openstack", + "platformStatus": { + "type": "openstack", + "openstack": {"cloudName": "us-east-1"} +}}}`) + assert.NoError(t, err) + })) + defer ts.Close() + + provider := &openshiftProvider{ + address: ts.URL, + token: "test", + client: &http.Client{}, + } + + got, err := provider.Infrastructure(context.Background()) + require.NoError(t, err) + expect := InfrastructureAPIResponse{ + Status: InfrastructureStatus{ + InfrastructureName: "test-d-bm4rt", + ControlPlaneTopology: "HighlyAvailable", + InfrastructureTopology: "HighlyAvailable", + PlatformStatus: InfrastructurePlatformStatus{ + Type: "openstack", + OpenStack: InfrastructureStatusOpenStack{ + CloudName: "us-east-1", + }, + }, + }, + } + assert.Equal(t, expect, *got) +} diff --git a/processor/resourcedetectionprocessor/README.md b/processor/resourcedetectionprocessor/README.md index d7d264a135cc..d70525cff05e 100644 --- a/processor/resourcedetectionprocessor/README.md +++ b/processor/resourcedetectionprocessor/README.md @@ -314,12 +314,39 @@ processors: detectors: [env, heroku] timeout: 2s override: false + +### Openshift + +Queries the OpenShift and Kubernetes API to retrieve the following resource attributes: + + * cloud.provider + * cloud.platform + * cloud.region + * k8s.cluster.name + * k8s.cluster.version + * openshift.cluster.version + +Your service account needs `read` access to the API endpoints `/apis/config.openshift.io/v1/clusterversions/version/status`, `/apis/config.openshift.io/v1/infrastructures/cluster/status` and `/version`. +By default, the API address is determined from the environment variables `KUBERNETES_SERVICE_HOST`, `KUBERNETES_SERVICE_PORT` and the service token is read from `/var/run/secrets/kubernetes.io/serviceaccount/token`. +The determination of the API address and the service token is skipped if they are set in the configuration. + +Example: + +```yaml +processors: + resourcedetection/openshift: + detectors: [openshift] + timeout: 2s + override: false + openshift: # optional + address: "https://api.example.com" + token: "token" ``` ## Configuration ```yaml -# a list of resource detectors to run, valid options are: "env", "system", "gce", "gke", "ec2", "ecs", "elastic_beanstalk", "eks", "azure", "heroku" +# a list of resource detectors to run, valid options are: "env", "system", "gce", "gke", "ec2", "ecs", "elastic_beanstalk", "eks", "azure", "heroku", "openshift" detectors: [ ] # determines if existing resource attributes should be overridden or preserved, defaults to true override: diff --git a/processor/resourcedetectionprocessor/config.go b/processor/resourcedetectionprocessor/config.go index 21935ad7901d..a28586c89c41 100644 --- a/processor/resourcedetectionprocessor/config.go +++ b/processor/resourcedetectionprocessor/config.go @@ -20,6 +20,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/aws/ec2" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/consul" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/openshift" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/system" ) @@ -52,6 +53,9 @@ type DetectorConfig struct { // SystemConfig contains user-specified configurations for the System detector SystemConfig system.Config `mapstructure:"system"` + + // OpenShift contains user-specified configurations for the Openshift detector + OpenShiftConfig openshift.Config `mapstructure:"openshift"` } func (d *DetectorConfig) GetConfigFromType(detectorType internal.DetectorType) internal.DetectorConfig { @@ -62,6 +66,8 @@ func (d *DetectorConfig) GetConfigFromType(detectorType internal.DetectorType) i return d.ConsulConfig case system.TypeStr: return d.SystemConfig + case openshift.TypeStr: + return d.OpenShiftConfig default: return nil } diff --git a/processor/resourcedetectionprocessor/config_test.go b/processor/resourcedetectionprocessor/config_test.go index 5e31e924ff19..64ca4dbd20d8 100644 --- a/processor/resourcedetectionprocessor/config_test.go +++ b/processor/resourcedetectionprocessor/config_test.go @@ -28,6 +28,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/aws/ec2" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/heroku" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/openshift" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/system" ) @@ -42,6 +43,20 @@ func TestLoadConfig(t *testing.T) { expected component.Config errorMessage string }{ + { + id: component.NewIDWithName(typeStr, "openshift"), + expected: &Config{ + Detectors: []string{"openshift"}, + DetectorConfig: DetectorConfig{ + OpenShiftConfig: openshift.Config{ + Address: "127.0.0.1:4444", + Token: "some_token", + }, + }, + HTTPClientSettings: cfg, + Override: false, + }, + }, { id: component.NewIDWithName(typeStr, "gcp"), expected: &Config{ diff --git a/processor/resourcedetectionprocessor/factory.go b/processor/resourcedetectionprocessor/factory.go index bab7d668faaf..18f47308f97d 100644 --- a/processor/resourcedetectionprocessor/factory.go +++ b/processor/resourcedetectionprocessor/factory.go @@ -38,6 +38,7 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/env" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/gcp" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/heroku" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/openshift" "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/system" ) @@ -74,6 +75,7 @@ func NewFactory() processor.Factory { gcp.TypeStr: gcp.NewDetector, heroku.TypeStr: heroku.NewDetector, system.TypeStr: system.NewDetector, + openshift.TypeStr: openshift.NewDetector, }) f := &factory{ @@ -101,7 +103,7 @@ func createDefaultConfig() component.Config { Override: true, Attributes: nil, // TODO: Once issue(https://github.com/open-telemetry/opentelemetry-collector/issues/4001) gets resolved, - // Set the default value of 'hostname_source' here instead of 'system' detector + // Set the default value of 'hostname_source' here instead of 'system' detector } } diff --git a/processor/resourcedetectionprocessor/go.mod b/processor/resourcedetectionprocessor/go.mod index 55229e7a56f3..6fb04c78c52b 100644 --- a/processor/resourcedetectionprocessor/go.mod +++ b/processor/resourcedetectionprocessor/go.mod @@ -28,6 +28,7 @@ require ( github.com/Microsoft/go-winio v0.5.2 // indirect github.com/Showmax/go-fqdn v1.0.0 // indirect github.com/armon/go-metrics v0.3.10 // indirect + github.com/benbjohnson/clock v1.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.8.1+incompatible // indirect github.com/docker/docker v20.10.22+incompatible // indirect diff --git a/processor/resourcedetectionprocessor/go.sum b/processor/resourcedetectionprocessor/go.sum index dcb73b071c9d..20093bc52e4b 100644 --- a/processor/resourcedetectionprocessor/go.sum +++ b/processor/resourcedetectionprocessor/go.sum @@ -39,6 +39,7 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+ github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= diff --git a/processor/resourcedetectionprocessor/internal/openshift/config.go b/processor/resourcedetectionprocessor/internal/openshift/config.go new file mode 100644 index 000000000000..63bbc5a5b3bf --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/openshift/config.go @@ -0,0 +1,72 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openshift // import "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/openshift" + +import ( + "fmt" + "os" +) + +const defaultServiceTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token" //#nosec + +func readK8STokenFromFile() (string, error) { + token, err := os.ReadFile(defaultServiceTokenPath) + if err != nil { + return "", err + } + return string(token), nil +} + +func readSVCAddressFromENV() (string, error) { + host := os.Getenv("KUBERNETES_SERVICE_HOST") + if host == "" { + return "", fmt.Errorf("could not extract openshift api host") + } + port := os.Getenv("KUBERNETES_SERVICE_PORT") + if port == "" { + return "", fmt.Errorf("could not extract openshift api port") + } + return fmt.Sprintf("https://%s:%s", host, port), nil +} + +// Config can contain user-specified inputs to overwrite default values. +// See `openshift.go#NewDetector` for more information. +type Config struct { + // Address is the address of the openshift api server + Address string `mapstructure:"address"` + + // Token is used to identify against the openshift api server + Token string `mapstructure:"token"` +} + +// MergeWithDefaults fills unset fields with default values. +func (c *Config) MergeWithDefaults() error { + if c.Token == "" { + token, err := readK8STokenFromFile() + if err != nil { + return err + } + c.Token = token + } + + if c.Address == "" { + addr, err := readSVCAddressFromENV() + if err != nil { + return err + } + c.Address = addr + } + return nil +} diff --git a/processor/resourcedetectionprocessor/internal/openshift/openshift.go b/processor/resourcedetectionprocessor/internal/openshift/openshift.go new file mode 100644 index 000000000000..cf82e7330f38 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/openshift/openshift.go @@ -0,0 +1,109 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openshift // import "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/openshift" + +import ( + "context" + "strings" + + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/processor" + conventions "go.opentelemetry.io/collector/semconv/v1.16.0" + "go.uber.org/zap" + + ocp "github.com/open-telemetry/opentelemetry-collector-contrib/internal/metadataproviders/openshift" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal" +) + +const ( + // TypeStr is type of detector. + TypeStr = "openshift" +) + +// NewDetector returns a detector which can detect resource attributes on OpenShift 4. +func NewDetector(set processor.CreateSettings, dcfg internal.DetectorConfig) (internal.Detector, error) { + userCfg := dcfg.(Config) + + if err := userCfg.MergeWithDefaults(); err != nil { + return nil, err + } + + return &detector{ + logger: set.Logger, + provider: ocp.NewProvider(userCfg.Address, userCfg.Token), + }, nil +} + +type detector struct { + logger *zap.Logger + provider ocp.Provider +} + +func (d *detector) Detect(ctx context.Context) (resource pcommon.Resource, schemaURL string, err error) { + res := pcommon.NewResource() + attrs := res.Attributes() + + infra, err := d.provider.Infrastructure(ctx) + if err != nil { + d.logger.Debug("OpenShift detector metadata retrieval failed", zap.Error(err)) + // return an empty Resource and no error + return res, "", nil + } + + var ( + region string + platform string + provider string + ) + + switch strings.ToLower(infra.Status.PlatformStatus.Type) { + case "aws": + provider = conventions.AttributeCloudProviderAWS + platform = conventions.AttributeCloudPlatformAWSOpenshift + region = strings.ToLower(infra.Status.PlatformStatus.Aws.Region) + case "azure": + provider = conventions.AttributeCloudProviderAzure + platform = conventions.AttributeCloudPlatformAzureOpenshift + region = strings.ToLower(infra.Status.PlatformStatus.Azure.CloudName) + case "gcp": + provider = conventions.AttributeCloudProviderGCP + platform = conventions.AttributeCloudPlatformGoogleCloudOpenshift + region = strings.ToLower(infra.Status.PlatformStatus.GCP.Region) + case "ibmcloud": + provider = conventions.AttributeCloudProviderIbmCloud + platform = conventions.AttributeCloudPlatformIbmCloudOpenshift + region = strings.ToLower(infra.Status.PlatformStatus.IBMCloud.Location) + case "openstack": + region = strings.ToLower(infra.Status.PlatformStatus.OpenStack.CloudName) + } + + if infra.Status.InfrastructureName != "" { + attrs.PutStr(conventions.AttributeK8SClusterName, infra.Status.InfrastructureName) + } + if provider != "" { + attrs.PutStr(conventions.AttributeCloudProvider, provider) + } + if platform != "" { + attrs.PutStr(conventions.AttributeCloudPlatform, platform) + } + if region != "" { + attrs.PutStr(conventions.AttributeCloudRegion, region) + } + + // TODO(frzifus): support conventions openshift and kubernetes cluster version. + // SEE: https://github.com/open-telemetry/opentelemetry-specification/issues/2913 + + return res, conventions.SchemaURL, nil +} diff --git a/processor/resourcedetectionprocessor/internal/openshift/openshift_test.go b/processor/resourcedetectionprocessor/internal/openshift/openshift_test.go new file mode 100644 index 000000000000..67ce9c4c7520 --- /dev/null +++ b/processor/resourcedetectionprocessor/internal/openshift/openshift_test.go @@ -0,0 +1,152 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openshift // import "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal/openshift" + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/pcommon" + conventions "go.opentelemetry.io/collector/semconv/v1.16.0" + "go.uber.org/zap/zaptest" + + ocp "github.com/open-telemetry/opentelemetry-collector-contrib/internal/metadataproviders/openshift" + "github.com/open-telemetry/opentelemetry-collector-contrib/processor/resourcedetectionprocessor/internal" +) + +type providerResponse struct { + ocp.InfrastructureAPIResponse + + OpenShiftClusterVersion string + K8SClusterVersion string +} + +type mockProvider struct { + res *providerResponse + ocpCVErr error + k8sCVErr error + infraErr error +} + +func (m *mockProvider) OpenShiftClusterVersion(context.Context) (string, error) { + if m.ocpCVErr != nil { + return "", m.ocpCVErr + } + return m.res.OpenShiftClusterVersion, nil +} + +func (m *mockProvider) K8SClusterVersion(context.Context) (string, error) { + if m.k8sCVErr != nil { + return "", m.k8sCVErr + } + return m.res.K8SClusterVersion, nil +} + +func (m *mockProvider) Infrastructure(context.Context) (*ocp.InfrastructureAPIResponse, error) { + if m.infraErr != nil { + return nil, m.infraErr + } + return &m.res.InfrastructureAPIResponse, nil +} + +func newTestDetector(t *testing.T, res *providerResponse, ocpCVErr, k8sCVErr, infraErr error) internal.Detector { + return &detector{ + logger: zaptest.NewLogger(t), + provider: &mockProvider{ + res: res, + ocpCVErr: ocpCVErr, + k8sCVErr: k8sCVErr, + infraErr: infraErr, + }, + } +} + +func TestDetect(t *testing.T) { + someErr := errors.New("test") + tt := []struct { + name string + detector internal.Detector + expectedResource pcommon.Resource + expectedSchemaURL string + expectedErr error + }{ + { + name: "error getting openshift cluster version", + detector: newTestDetector(t, &providerResponse{}, someErr, nil, nil), + expectedErr: someErr, + expectedResource: pcommon.NewResource(), + expectedSchemaURL: conventions.SchemaURL, + }, + { + name: "error getting k8s cluster version", + detector: newTestDetector(t, &providerResponse{}, nil, someErr, nil), + expectedErr: someErr, + expectedResource: pcommon.NewResource(), + expectedSchemaURL: conventions.SchemaURL, + }, + { + name: "error getting infrastructure details", + detector: newTestDetector(t, &providerResponse{}, nil, nil, someErr), + expectedErr: someErr, + expectedResource: pcommon.NewResource(), + }, + { + name: "detect all details", + detector: newTestDetector(t, &providerResponse{ + InfrastructureAPIResponse: ocp.InfrastructureAPIResponse{ + Status: ocp.InfrastructureStatus{ + InfrastructureName: "test-d-bm4rt", + ControlPlaneTopology: "HighlyAvailable", + InfrastructureTopology: "HighlyAvailable", + PlatformStatus: ocp.InfrastructurePlatformStatus{ + Type: "AWS", + Aws: ocp.InfrastructureStatusAWS{ + Region: "us-east-1", + }, + }, + }, + }, + OpenShiftClusterVersion: "4.1.2", + K8SClusterVersion: "1.23.4", + }, nil, nil, nil), + expectedErr: nil, + expectedResource: func() pcommon.Resource { + res := pcommon.NewResource() + attrs := res.Attributes() + attrs.PutStr(conventions.AttributeK8SClusterName, "test-d-bm4rt") + attrs.PutStr(conventions.AttributeCloudProvider, "aws") + attrs.PutStr(conventions.AttributeCloudPlatform, "aws_openshift") + attrs.PutStr(conventions.AttributeCloudRegion, "us-east-1") + return res + }(), + expectedSchemaURL: conventions.SchemaURL, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + resource, schemaURL, err := tc.detector.Detect(context.Background()) + if err != nil && errors.Is(err, tc.expectedErr) { + return + } else if err != nil && !errors.Is(err, tc.expectedErr) { + t.Fatal(err) + } + + assert.Equal(t, tc.expectedResource, resource) + assert.Equal(t, tc.expectedSchemaURL, schemaURL) + }) + } +} diff --git a/processor/resourcedetectionprocessor/testdata/config.yaml b/processor/resourcedetectionprocessor/testdata/config.yaml index 1940ed900281..aa4d5d0085b2 100644 --- a/processor/resourcedetectionprocessor/testdata/config.yaml +++ b/processor/resourcedetectionprocessor/testdata/config.yaml @@ -1,4 +1,12 @@ resourcedetection: +resourcedetection/openshift: + detectors: [openshift] + timeout: 2s + override: false + openshift: + address: "127.0.0.1:4444" + token: "some_token" + resourcedetection/gcp: detectors: [env, gcp] timeout: 2s