From b93ee90577e315e5c86dc84f4bb1525211270c41 Mon Sep 17 00:00:00 2001
From: Ben B <bongartz@klimlive.de>
Date: Tue, 17 Jan 2023 19:40:04 +0100
Subject: [PATCH] [processor/resourcedetectionprocessor] support openshift
 (#16079)

This pr adds openshift support to the resourcedetectionprocessor.
Closes #15694
---
 ...edetectionprocessor_support_openshift.yaml |  16 +
 .github/CODEOWNERS                            |   1 +
 .github/ISSUE_TEMPLATE/bug_report.yaml        |   1 +
 .github/ISSUE_TEMPLATE/feature_request.yaml   |   1 +
 .github/ISSUE_TEMPLATE/other.yaml             |   1 +
 .../metadataproviders/openshift/metadata.go   | 208 +++++++++++
 .../openshift/metadata_test.go                | 343 ++++++++++++++++++
 .../resourcedetectionprocessor/README.md      |  29 +-
 .../resourcedetectionprocessor/config.go      |   6 +
 .../resourcedetectionprocessor/config_test.go |  15 +
 .../resourcedetectionprocessor/factory.go     |   4 +-
 processor/resourcedetectionprocessor/go.mod   |   1 +
 processor/resourcedetectionprocessor/go.sum   |   1 +
 .../internal/openshift/config.go              |  72 ++++
 .../internal/openshift/openshift.go           | 109 ++++++
 .../internal/openshift/openshift_test.go      | 152 ++++++++
 .../testdata/config.yaml                      |   8 +
 17 files changed, 966 insertions(+), 2 deletions(-)
 create mode 100755 .chloggen/resourcedetectionprocessor_support_openshift.yaml
 create mode 100644 internal/metadataproviders/openshift/metadata.go
 create mode 100644 internal/metadataproviders/openshift/metadata_test.go
 create mode 100644 processor/resourcedetectionprocessor/internal/openshift/config.go
 create mode 100644 processor/resourcedetectionprocessor/internal/openshift/openshift.go
 create mode 100644 processor/resourcedetectionprocessor/internal/openshift/openshift_test.go

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: [ <string> ]
 # determines if existing resource attributes should be overridden or preserved, defaults to true
 override: <bool>
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