diff --git a/.chloggen/TA-update-configs-to-enable-mtls.yaml b/.chloggen/TA-update-configs-to-enable-mtls.yaml new file mode 100755 index 0000000000..64f318ad01 --- /dev/null +++ b/.chloggen/TA-update-configs-to-enable-mtls.yaml @@ -0,0 +1,18 @@ +# 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. collector, target allocator, auto-instrumentation, opamp, github action) +component: target allocator, collector + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "Enable mTLS between the TA and collector for passing secrets in the scrape_config securely" + +# One or more tracking issues related to the change +issues: [1669] + +# (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: | + This change enables mTLS between the collector and the target allocator (requires cert-manager). + This is necessary for passing secrets securely from the TA to the collector for scraping endpoints that have authentication. diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 5bc7aaeeec..922467d9b0 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -34,6 +34,7 @@ jobs: - e2e-upgrade - e2e-multi-instrumentation - e2e-metadata-filters + - e2e-ta-collector-mtls include: - group: e2e-instrumentation setup: "add-instrumentation-params prepare-e2e" @@ -41,6 +42,8 @@ jobs: setup: "add-instrumentation-params prepare-e2e" - group: e2e-metadata-filters setup: "add-operator-arg OPERATOR_ARG='--annotations-filter=.*filter.out --annotations-filter=config.*.gke.io.* --labels-filter=.*filter.out' prepare-e2e" + - group: e2e-ta-collector-mtls + setup: "add-operator-arg OPERATOR_ARG='--feature-gates=operator.targetallocator.mtls' add-certmanager-permissions prepare-e2e" - group: e2e-automatic-rbac setup: "add-rbac-permissions-to-operator prepare-e2e" steps: diff --git a/.gitignore b/.gitignore index 1438657894..52b40a6635 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ - # Binaries for programs and plugins *.exe *.exe~ @@ -39,8 +38,9 @@ config/manager/kustomization.yaml kubeconfig tests/_build/ config/rbac/extra-permissions-operator/ +config/rbac/certmanager-permissions/ # autoinstrumentation artifacts build node_modules -package-lock.json \ No newline at end of file +package-lock.json diff --git a/Makefile b/Makefile index 939af881d5..2e73d990d9 100644 --- a/Makefile +++ b/Makefile @@ -312,6 +312,18 @@ e2e-prometheuscr: chainsaw e2e-targetallocator: chainsaw $(CHAINSAW) test --test-dir ./tests/e2e-targetallocator +.PHONY: add-certmanager-permissions +add-certmanager-permissions: + # Kustomize only allows patches in the folder where the kustomization is located + # This folder is ignored by .gitignore + cp -r tests/e2e-ta-collector-mtls/certmanager-permissions config/rbac/certmanager-permissions + cd config/rbac && $(KUSTOMIZE) edit add patch --kind ClusterRole --name manager-role --path certmanager-permissions/certmanager.yaml + +# Target allocator collector mTLS end-to-tests +.PHONY: e2e-ta-collector-mtls +e2e-ta-collector-mtls: chainsaw + $(CHAINSAW) test --test-dir ./tests/e2e-ta-collector-mtls + # end-to-end-test for Annotations/Labels Filters .PHONY: e2e-metadata-filters e2e-metadata-filters: chainsaw diff --git a/cmd/otel-allocator/README.md b/cmd/otel-allocator/README.md index 0b10a85614..7b4741d42b 100644 --- a/cmd/otel-allocator/README.md +++ b/cmd/otel-allocator/README.md @@ -211,9 +211,42 @@ rules: ### Service / Pod monitor endpoint credentials -If your service or pod monitor endpoints require credentials or other supported form of authentication (bearer token, basic auth, OAuth2 etc.), you need to ensure that the collector has access to this information. Due to some limitations in how the endpoints configuration is handled, target allocator currently does **not** support credentials provided via secrets. It is only possible to provide credentials in a file (for more details see issue https://github.com/open-telemetry/opentelemetry-operator/issues/1669). +If your service or pod monitor endpoints require authentication (such as bearer tokens, basic auth, OAuth2, etc.), you must ensure that the collector has access to these credentials. + +To secure the connection between the target allocator and the collector so that the secrets can be retrieved, mTLS is used. This involves the use of cert-manager to manage the CA, server, and client certificates. + +Prerequisites: +- Ensure cert-manager is installed in your Kubernetes cluster. +- Grant RBAC Permissions: + + - The target allocator needs the appropriate RBAC permissions to get the secrets referenced in the Service / Pod monitor. + + - The operator needs the appropriate RBAC permissions to manage cert-manager resources. The following clusterRole can be used to grant the necessary permissions: + + ```yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: opentelemetry-operator-controller-manager-cert-manager-role + rules: + - apiGroups: + - cert-manager.io + resources: + - issuers + - certificaterequests + - certificates + verbs: + - create + - get + - list + - watch + - update + - patch + - delete + ``` + +- Enable the `operator.targetallocator.mtls` feature gate in the operator's deployment. -In order to ensure your endpoints can be scraped, your collector instance needs to have the particular secret mounted as a file at the correct path. # Design diff --git a/cmd/otel-allocator/config/config.go b/cmd/otel-allocator/config/config.go index 3e3fd389c7..8a8b7c188a 100644 --- a/cmd/otel-allocator/config/config.go +++ b/cmd/otel-allocator/config/config.go @@ -115,29 +115,34 @@ func LoadFromCLI(target *Config, flagSet *pflag.FlagSet) error { target.PrometheusCR.Enabled = prometheusCREnabled } - target.HTTPS.Enabled, err = getHttpsEnabled(flagSet) - if err != nil { + if httpsEnabled, changed, err := getHttpsEnabled(flagSet); err != nil { return err + } else if changed { + target.HTTPS.Enabled = httpsEnabled } - target.HTTPS.ListenAddr, err = getHttpsListenAddr(flagSet) - if err != nil { + if listenAddrHttps, changed, err := getHttpsListenAddr(flagSet); err != nil { return err + } else if changed { + target.HTTPS.ListenAddr = listenAddrHttps } - target.HTTPS.CAFilePath, err = getHttpsCAFilePath(flagSet) - if err != nil { + if caFilePath, changed, err := getHttpsCAFilePath(flagSet); err != nil { return err + } else if changed { + target.HTTPS.CAFilePath = caFilePath } - target.HTTPS.TLSCertFilePath, err = getHttpsTLSCertFilePath(flagSet) - if err != nil { + if tlsCertFilePath, changed, err := getHttpsTLSCertFilePath(flagSet); err != nil { return err + } else if changed { + target.HTTPS.TLSCertFilePath = tlsCertFilePath } - target.HTTPS.TLSKeyFilePath, err = getHttpsTLSKeyFilePath(flagSet) - if err != nil { + if tlsKeyFilePath, changed, err := getHttpsTLSKeyFilePath(flagSet); err != nil { return err + } else if changed { + target.HTTPS.TLSKeyFilePath = tlsKeyFilePath } return nil diff --git a/cmd/otel-allocator/config/config_test.go b/cmd/otel-allocator/config/config_test.go index 53ddc52a49..c1b721b773 100644 --- a/cmd/otel-allocator/config/config_test.go +++ b/cmd/otel-allocator/config/config_test.go @@ -64,6 +64,7 @@ func TestLoad(t *testing.T) { }, HTTPS: HTTPSServerConfig{ Enabled: true, + ListenAddr: ":8443", CAFilePath: "/path/to/ca.pem", TLSCertFilePath: "/path/to/cert.pem", TLSKeyFilePath: "/path/to/key.pem", diff --git a/cmd/otel-allocator/config/flags.go b/cmd/otel-allocator/config/flags.go index 5b3a3705db..0a47c27636 100644 --- a/cmd/otel-allocator/config/flags.go +++ b/cmd/otel-allocator/config/flags.go @@ -78,22 +78,47 @@ func getPrometheusCREnabled(flagSet *pflag.FlagSet) (value bool, changed bool, e return } -func getHttpsListenAddr(flagSet *pflag.FlagSet) (string, error) { - return flagSet.GetString(listenAddrHttpsFlagName) +func getHttpsListenAddr(flagSet *pflag.FlagSet) (value string, changed bool, err error) { + if changed = flagSet.Changed(listenAddrHttpsFlagName); !changed { + value, err = ":8443", nil + return + } + value, err = flagSet.GetString(listenAddrHttpsFlagName) + return } -func getHttpsEnabled(flagSet *pflag.FlagSet) (bool, error) { - return flagSet.GetBool(httpsEnabledFlagName) +func getHttpsEnabled(flagSet *pflag.FlagSet) (value bool, changed bool, err error) { + if changed = flagSet.Changed(httpsEnabledFlagName); !changed { + value, err = false, nil + return + } + value, err = flagSet.GetBool(httpsEnabledFlagName) + return } -func getHttpsCAFilePath(flagSet *pflag.FlagSet) (string, error) { - return flagSet.GetString(httpsCAFilePathFlagName) +func getHttpsCAFilePath(flagSet *pflag.FlagSet) (value string, changed bool, err error) { + if changed = flagSet.Changed(httpsCAFilePathFlagName); !changed { + value, err = "", nil + return + } + value, err = flagSet.GetString(httpsCAFilePathFlagName) + return } -func getHttpsTLSCertFilePath(flagSet *pflag.FlagSet) (string, error) { - return flagSet.GetString(httpsTLSCertFilePathFlagName) +func getHttpsTLSCertFilePath(flagSet *pflag.FlagSet) (value string, changed bool, err error) { + if changed = flagSet.Changed(httpsTLSCertFilePathFlagName); !changed { + value, err = "", nil + return + } + value, err = flagSet.GetString(httpsTLSCertFilePathFlagName) + return } -func getHttpsTLSKeyFilePath(flagSet *pflag.FlagSet) (string, error) { - return flagSet.GetString(httpsTLSKeyFilePathFlagName) +func getHttpsTLSKeyFilePath(flagSet *pflag.FlagSet) (value string, changed bool, err error) { + if changed = flagSet.Changed(httpsTLSKeyFilePathFlagName); !changed { + value, err = "", nil + return + } + value, err = flagSet.GetString(httpsTLSKeyFilePathFlagName) + return } diff --git a/cmd/otel-allocator/config/flags_test.go b/cmd/otel-allocator/config/flags_test.go index 2c33d65017..b2725c170e 100644 --- a/cmd/otel-allocator/config/flags_test.go +++ b/cmd/otel-allocator/config/flags_test.go @@ -77,13 +77,19 @@ func TestFlagGetters(t *testing.T) { name: "HttpsServer", flagArgs: []string{"--" + httpsEnabledFlagName, "true"}, expectedValue: true, - getterFunc: func(fs *pflag.FlagSet) (interface{}, error) { return getHttpsEnabled(fs) }, + getterFunc: func(fs *pflag.FlagSet) (interface{}, error) { + value, _, err := getHttpsEnabled(fs) + return value, err + }, }, { name: "HttpsServerKey", flagArgs: []string{"--" + httpsTLSKeyFilePathFlagName, "/path/to/tls.key"}, expectedValue: "/path/to/tls.key", - getterFunc: func(fs *pflag.FlagSet) (interface{}, error) { return getHttpsTLSKeyFilePath(fs) }, + getterFunc: func(fs *pflag.FlagSet) (interface{}, error) { + value, _, err := getHttpsTLSKeyFilePath(fs) + return value, err + }, }, } diff --git a/cmd/otel-allocator/config/testdata/config_test.yaml b/cmd/otel-allocator/config/testdata/config_test.yaml index bcb220adf8..47a3226517 100644 --- a/cmd/otel-allocator/config/testdata/config_test.yaml +++ b/cmd/otel-allocator/config/testdata/config_test.yaml @@ -7,6 +7,7 @@ prometheus_cr: scrape_interval: 60s https: enabled: true + listen_addr: :8443 ca_file_path: /path/to/ca.pem tls_cert_file_path: /path/to/cert.pem tls_key_file_path: /path/to/key.pem diff --git a/controllers/builder_test.go b/controllers/builder_test.go index e3b495e00a..eb4566cfb0 100644 --- a/controllers/builder_test.go +++ b/controllers/builder_test.go @@ -18,6 +18,8 @@ import ( "strings" "testing" + cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + cmmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" "github.com/go-logr/logr" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" "github.com/stretchr/testify/require" @@ -35,6 +37,7 @@ import ( "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" "github.com/open-telemetry/opentelemetry-operator/internal/manifests" "github.com/open-telemetry/opentelemetry-operator/internal/manifests/collector" @@ -1243,6 +1246,7 @@ service: want []client.Object featuregates []string wantErr bool + opts []config.Option }{ { name: "base case", @@ -2186,12 +2190,654 @@ prometheus_cr: wantErr: false, featuregates: []string{}, }, + { + name: "target allocator mtls enabled", + args: args{ + instance: v1beta1.OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: v1beta1.OpenTelemetryCollectorSpec{ + OpenTelemetryCommonFields: v1beta1.OpenTelemetryCommonFields{ + Image: "test", + Replicas: &one, + }, + Mode: "statefulset", + Config: goodConfig, + TargetAllocator: v1beta1.TargetAllocatorEmbedded{ + Enabled: true, + FilterStrategy: "relabel-config", + AllocationStrategy: v1beta1.TargetAllocatorAllocationStrategyConsistentHashing, + PrometheusCR: v1beta1.TargetAllocatorPrometheusCR{ + Enabled: true, + }, + }, + }, + }, + }, + want: []client.Object{ + &appsv1.StatefulSet{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-collector", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-collector", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-collector", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + Annotations: map[string]string{}, + }, + Spec: appsv1.StatefulSetSpec{ + ServiceName: "test-collector", + Replicas: &one, + Selector: &metav1.LabelSelector{ + MatchLabels: selectorLabels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-collector", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-collector", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + Annotations: map[string]string{ + "opentelemetry-operator-config/sha256": "42773025f65feaf30df59a306a9e38f1aaabe94c8310983beaddb7f648d699b0", + "prometheus.io/path": "/metrics", + "prometheus.io/port": "8888", + "prometheus.io/scrape": "true", + }, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "otc-internal", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-collector-" + goodConfigHash, + }, + Items: []corev1.KeyToPath{ + { + Key: "collector.yaml", + Path: "collector.yaml", + }, + }, + }, + }, + }, + { + Name: "test-ta-client-cert", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "test-ta-client-cert", + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "otc-container", + Image: "test", + Args: []string{ + "--config=/conf/collector.yaml", + }, + Env: []corev1.EnvVar{ + { + Name: "POD_NAME", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.name", + }, + }, + }, + { + Name: "SHARD", + Value: "0", + }, + }, + Ports: []corev1.ContainerPort{ + { + Name: "metrics", + HostPort: 0, + ContainerPort: 8888, + Protocol: "TCP", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "otc-internal", + MountPath: "/conf", + }, + { + Name: "test-ta-client-cert", + MountPath: "/tls", + }, + }, + }, + }, + ShareProcessNamespace: ptr.To(false), + DNSPolicy: "ClusterFirst", + DNSConfig: &corev1.PodDNSConfig{}, + ServiceAccountName: "test-collector", + }, + }, + PodManagementPolicy: "Parallel", + }, + }, + &policyV1.PodDisruptionBudget{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-collector", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-collector", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-collector", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + Annotations: map[string]string{}, + }, + Spec: policyV1.PodDisruptionBudgetSpec{ + Selector: &v1.LabelSelector{ + MatchLabels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-collector", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-collector", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + }, + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-collector-" + goodConfigHash, + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-collector", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-collector", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + Annotations: map[string]string{}, + }, + Data: map[string]string{ + "collector.yaml": "exporters:\n debug: null\nreceivers:\n prometheus:\n config: {}\n target_allocator:\n collector_id: ${POD_NAME}\n endpoint: https://test-targetallocator:443\n interval: 30s\n tls:\n ca_file: /tls/ca.crt\n cert_file: /tls/tls.crt\n key_file: /tls/tls.key\nservice:\n pipelines:\n metrics:\n exporters:\n - debug\n receivers:\n - prometheus\n", + }, + }, + &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-collector", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-collector", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-collector", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + Annotations: map[string]string{}, + }, + }, + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-collector-monitoring", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-collector", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-collector-monitoring", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + "operator.opentelemetry.io/collector-service-type": "monitoring", + "operator.opentelemetry.io/collector-monitoring-service": "Exists", + }, + Annotations: map[string]string{}, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "monitoring", + Port: 8888, + }, + }, + Selector: selectorLabels, + }, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-targetallocator", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-targetallocator", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + Annotations: nil, + }, + Data: map[string]string{ + "targetallocator.yaml": `allocation_strategy: consistent-hashing +collector_selector: + matchlabels: + app.kubernetes.io/component: opentelemetry-collector + app.kubernetes.io/instance: test.test + app.kubernetes.io/managed-by: opentelemetry-operator + app.kubernetes.io/part-of: opentelemetry + matchexpressions: [] +config: + scrape_configs: + - job_name: example + metric_relabel_configs: + - replacement: $1_$2 + source_labels: + - job + target_label: job + relabel_configs: + - replacement: my_service_$1 + source_labels: + - __meta_service_id + target_label: job + - replacement: $1 + source_labels: + - __meta_service_name + target_label: instance +filter_strategy: relabel-config +https: + ca_file_path: /tls/ca.crt + enabled: true + listen_addr: :8443 + tls_cert_file_path: /tls/tls.crt + tls_key_file_path: /tls/tls.key +prometheus_cr: + enabled: true + pod_monitor_selector: null + service_monitor_selector: null +`, + }, + }, + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-targetallocator", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-targetallocator", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + Annotations: nil, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: taSelectorLabels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-targetallocator", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + Annotations: map[string]string{ + "opentelemetry-targetallocator-config/hash": "f1ce0fdbf69924576576d1d6eb2a3cc91a3f72675b3facbb36702d57027bc6ae", + }, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: "ta-internal", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "test-targetallocator", + }, + Items: []corev1.KeyToPath{ + { + Key: "targetallocator.yaml", + Path: "targetallocator.yaml", + }, + }, + }, + }, + }, + { + Name: "test-ta-server-cert", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "test-ta-server-cert", + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "ta-container", + Image: "default-ta-allocator", + Env: []corev1.EnvVar{ + { + Name: "OTELCOL_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }, + }, + Ports: []corev1.ContainerPort{ + { + Name: "http", + HostPort: 0, + ContainerPort: 8080, + Protocol: "TCP", + }, + { + Name: "https", + HostPort: 0, + ContainerPort: 8443, + Protocol: "TCP", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "ta-internal", + MountPath: "/conf", + }, + { + Name: "test-ta-server-cert", + MountPath: "/tls", + }, + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/livez", + Port: intstr.FromInt(8080), + }, + }, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/readyz", + Port: intstr.FromInt(8080), + }, + }, + }, + }, + }, + DNSPolicy: "ClusterFirst", + DNSConfig: &corev1.PodDNSConfig{}, + ShareProcessNamespace: ptr.To(false), + ServiceAccountName: "test-targetallocator", + }, + }, + }, + }, + &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-targetallocator", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-targetallocator", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + }, + }, + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-targetallocator", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-targetallocator", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + Annotations: nil, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "targetallocation", + Port: 80, + TargetPort: intstr.IntOrString{ + Type: 1, + StrVal: "http", + }, + }, + { + Name: "targetallocation-https", + Port: 443, + TargetPort: intstr.IntOrString{ + Type: 1, + StrVal: "https", + }, + }, + }, + Selector: taSelectorLabels, + }, + }, + &policyV1.PodDisruptionBudget{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-targetallocator", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-targetallocator", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + Annotations: map[string]string{ + "opentelemetry-targetallocator-config/hash": "f1ce0fdbf69924576576d1d6eb2a3cc91a3f72675b3facbb36702d57027bc6ae", + }, + }, + Spec: policyV1.PodDisruptionBudgetSpec{ + Selector: &v1.LabelSelector{ + MatchLabels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-targetallocator", + "app.kubernetes.io/part-of": "opentelemetry", + }, + }, + MaxUnavailable: &intstr.IntOrString{ + Type: intstr.Int, + IntVal: 1, + }, + }, + }, + &cmv1.Issuer{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-self-signed-issuer", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-self-signed-issuer", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + }, + Spec: cmv1.IssuerSpec{ + IssuerConfig: cmv1.IssuerConfig{ + SelfSigned: &cmv1.SelfSignedIssuer{ + CRLDistributionPoints: nil, + }, + }, + }, + }, + &cmv1.Certificate{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ca-cert", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-ca-cert", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + }, + Spec: cmv1.CertificateSpec{ + Subject: &cmv1.X509Subject{ + OrganizationalUnits: []string{"opentelemetry-operator"}, + }, + CommonName: "test-ca-cert", + IsCA: true, + SecretName: "test-ca-cert", + IssuerRef: cmmetav1.ObjectReference{ + Name: "test-self-signed-issuer", + Kind: "Issuer", + }, + }, + }, + &cmv1.Issuer{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ca-issuer", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-ca-issuer", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + }, + Spec: cmv1.IssuerSpec{ + IssuerConfig: cmv1.IssuerConfig{ + CA: &cmv1.CAIssuer{ + SecretName: "test-ca-cert", + }, + }, + }, + }, + &cmv1.Certificate{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ta-server-cert", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-ta-server-cert", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + }, + Spec: cmv1.CertificateSpec{ + Subject: &cmv1.X509Subject{ + OrganizationalUnits: []string{"opentelemetry-operator"}, + }, + DNSNames: []string{ + "test-targetallocator", + "test-targetallocator.test.svc", + "test-targetallocator.test.svc.cluster.local", + }, + SecretName: "test-ta-server-cert", + IssuerRef: cmmetav1.ObjectReference{ + Name: "test-ca-issuer", + Kind: "Issuer", + }, + Usages: []cmv1.KeyUsage{ + "client auth", + "server auth", + }, + }, + }, + &cmv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ta-client-cert", + Namespace: "test", + Labels: map[string]string{ + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/instance": "test.test", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/name": "test-ta-client-cert", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/version": "latest", + }, + }, + Spec: cmv1.CertificateSpec{ + Subject: &cmv1.X509Subject{ + OrganizationalUnits: []string{"opentelemetry-operator"}, + }, + DNSNames: []string{ + "test-targetallocator", + "test-targetallocator.test.svc", + "test-targetallocator.test.svc.cluster.local", + }, + SecretName: "test-ta-client-cert", + IssuerRef: cmmetav1.ObjectReference{ + Name: "test-ca-issuer", + Kind: "Issuer", + }, + Usages: []cmv1.KeyUsage{ + "client auth", + "server auth", + }, + }, + }, + }, + wantErr: false, + opts: []config.Option{ + config.WithCertManagerAvailability(certmanager.Available), + }, + featuregates: []string{"operator.targetallocator.mtls"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cfg := config.New( + opts := []config.Option{ config.WithCollectorImage("default-collector"), config.WithTargetAllocatorImage("default-ta-allocator"), + } + opts = append(opts, tt.opts...) + cfg := config.New( + opts..., ) params := manifests.Params{ Log: logr.Discard(), diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 4e56fb16de..8cb8a24420 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -55,6 +55,7 @@ import ( "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus" autoRBAC "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/rbac" @@ -100,6 +101,7 @@ type mockAutoDetect struct { OpenShiftRoutesAvailabilityFunc func() (openshift.RoutesAvailability, error) PrometheusCRsAvailabilityFunc func() (prometheus.Availability, error) RBACPermissionsFunc func(ctx context.Context) (autoRBAC.Availability, error) + CertManagerAvailabilityFunc func(ctx context.Context) (certmanager.Availability, error) } func (m *mockAutoDetect) FIPSEnabled(ctx context.Context) bool { @@ -127,6 +129,13 @@ func (m *mockAutoDetect) RBACPermissions(ctx context.Context) (autoRBAC.Availabi return autoRBAC.NotAvailable, nil } +func (m *mockAutoDetect) CertManagerAvailability(ctx context.Context) (certmanager.Availability, error) { + if m.CertManagerAvailabilityFunc != nil { + return m.CertManagerAvailabilityFunc(ctx) + } + return certmanager.NotAvailable, nil +} + func TestMain(m *testing.M) { ctx, cancel = context.WithCancel(context.TODO()) defer cancel() diff --git a/go.mod b/go.mod index 3947fed58d..c29aafe2a5 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( k8s.io/kubectl v0.31.1 k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 sigs.k8s.io/controller-runtime v0.19.0 + sigs.k8s.io/gateway-api v1.0.0 // indirect sigs.k8s.io/yaml v1.4.0 ) @@ -74,6 +75,7 @@ require ( github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cert-manager/cert-manager v1.14.5 github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b // indirect @@ -149,7 +151,7 @@ require ( github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ionos-cloud/sdk-go/v6 v6.1.11 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect diff --git a/go.sum b/go.sum index f1069f8cf6..66953b8fe6 100644 --- a/go.sum +++ b/go.sum @@ -103,6 +103,8 @@ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cert-manager/cert-manager v1.14.5 h1:uuM1O2g2S80nxiH3eW2cZYMGiL2zmDFVdAzg8sibWuc= +github.com/cert-manager/cert-manager v1.14.5/go.mod h1:fmr/cU5jiLxWj69CroDggSOa49RljUK+dU583TaQUXM= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -394,8 +396,9 @@ github.com/ionos-cloud/sdk-go/v6 v6.1.11 h1:J/uRN4UWO3wCyGOeDdMKv8LWRzKu6UIkLEae github.com/ionos-cloud/sdk-go/v6 v6.1.11/go.mod h1:EzEgRIDxBELvfoa/uBN0kOQaqovLjUWEB7iW4/Q+t4k= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -1077,6 +1080,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q= sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= +sigs.k8s.io/gateway-api v1.0.0 h1:iPTStSv41+d9p0xFydll6d7f7MOBGuqXM6p2/zVYMAs= +sigs.k8s.io/gateway-api v1.0.0/go.mod h1:4cUgr0Lnp5FZ0Cdq8FdRwCvpiWws7LVhLHGIudLlf4c= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= diff --git a/internal/autodetect/autodetectutils/utils.go b/internal/autodetect/autodetectutils/utils.go new file mode 100644 index 0000000000..9bbf64357e --- /dev/null +++ b/internal/autodetect/autodetectutils/utils.go @@ -0,0 +1,47 @@ +// 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 autodetectutils + +import ( + "fmt" + "os" +) + +const ( + SA_ENV_VAR = "SERVICE_ACCOUNT_NAME" + NAMESPACE_ENV_VAR = "NAMESPACE" + NAMESPACE_FILE_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" +) + +func GetOperatorNamespace() (string, error) { + namespace := os.Getenv(NAMESPACE_ENV_VAR) + if namespace != "" { + return namespace, nil + } + + nsBytes, err := os.ReadFile(NAMESPACE_FILE_PATH) + if err != nil { + return "", err + } + return string(nsBytes), nil +} + +func GetOperatorServiceAccount() (string, error) { + sa := os.Getenv(SA_ENV_VAR) + if sa == "" { + return sa, fmt.Errorf("%s env variable not found", SA_ENV_VAR) + } + return sa, nil +} diff --git a/internal/autodetect/certmanager/check.go b/internal/autodetect/certmanager/check.go new file mode 100644 index 0000000000..f4f58da623 --- /dev/null +++ b/internal/autodetect/certmanager/check.go @@ -0,0 +1,55 @@ +// 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 certmanager + +import ( + "context" + "fmt" + + rbacv1 "k8s.io/api/rbac/v1" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/autodetectutils" + rbac "github.com/open-telemetry/opentelemetry-operator/internal/rbac" +) + +// CheckCertManagerPermissions checks if the operator has the needed permissions to manage cert-manager certificates automatically. +// If the RBAC is there, no errors nor warnings are returned. +func CheckCertManagerPermissions(ctx context.Context, reviewer *rbac.Reviewer) (admission.Warnings, error) { + namespace, err := autodetectutils.GetOperatorNamespace() + if err != nil { + return nil, fmt.Errorf("%s: %w", "not possible to check RBAC rules", err) + } + + serviceAccount, err := autodetectutils.GetOperatorServiceAccount() + if err != nil { + return nil, fmt.Errorf("%s: %w", "not possible to check RBAC rules", err) + } + + rules := []*rbacv1.PolicyRule{ + { + APIGroups: []string{"cert-manager.io"}, + Resources: []string{"issuers", "certificaterequests", "certificates"}, + Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"}, + }, + } + + if subjectAccessReviews, err := reviewer.CheckPolicyRules(ctx, serviceAccount, namespace, rules...); err != nil { + return nil, fmt.Errorf("%s: %w", "unable to check rbac rules", err) + } else if allowed, deniedReviews := rbac.AllSubjectAccessReviewsAllowed(subjectAccessReviews); !allowed { + return rbac.WarningsGroupedByResource(deniedReviews), nil + } + return nil, nil +} diff --git a/internal/autodetect/certmanager/operator.go b/internal/autodetect/certmanager/operator.go new file mode 100644 index 0000000000..19ec9baf18 --- /dev/null +++ b/internal/autodetect/certmanager/operator.go @@ -0,0 +1,30 @@ +// 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 certmanager + +// Availability represents that the Cert Manager CRDs are installed and the operator's service account has permissions to manage cert-manager resources. +type Availability int + +const ( + // NotAvailable Cert Manager CRDs or RBAC permissions to manage cert-manager certificates are not available. + NotAvailable Availability = iota + + // Available Cert Manager CRDs and RBAC permissions to manage cert-manager certificates are available. + Available +) + +func (p Availability) String() string { + return [...]string{"NotAvailable", "Available"}[p] +} diff --git a/internal/autodetect/main.go b/internal/autodetect/main.go index 27c368f3f5..850a907957 100644 --- a/internal/autodetect/main.go +++ b/internal/autodetect/main.go @@ -22,6 +22,7 @@ import ( "k8s.io/client-go/discovery" "k8s.io/client-go/rest" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/fips" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus" @@ -36,6 +37,7 @@ type AutoDetect interface { OpenShiftRoutesAvailability() (openshift.RoutesAvailability, error) PrometheusCRsAvailability() (prometheus.Availability, error) RBACPermissions(ctx context.Context) (autoRBAC.Availability, error) + CertManagerAvailability(ctx context.Context) (certmanager.Availability, error) FIPSEnabled(ctx context.Context) bool } @@ -125,6 +127,36 @@ func (a *autoDetect) RBACPermissions(ctx context.Context) (autoRBAC.Availability return autoRBAC.Available, nil } +func (a *autoDetect) CertManagerAvailability(ctx context.Context) (certmanager.Availability, error) { + apiList, err := a.dcl.ServerGroups() + if err != nil { + return certmanager.NotAvailable, err + } + + apiGroups := apiList.Groups + certManagerFound := false + for i := 0; i < len(apiGroups); i++ { + if apiGroups[i].Name == "cert-manager.io" { + certManagerFound = true + break + } + } + + if !certManagerFound { + return certmanager.NotAvailable, nil + } + + w, err := certmanager.CheckCertManagerPermissions(ctx, a.reviewer) + if err != nil { + return certmanager.NotAvailable, err + } + if w != nil { + return certmanager.NotAvailable, fmt.Errorf("missing permissions: %s", w) + } + + return certmanager.Available, nil +} + func (a *autoDetect) FIPSEnabled(_ context.Context) bool { return fips.IsFipsEnabled() } diff --git a/internal/autodetect/main_test.go b/internal/autodetect/main_test.go index cae05f1563..82e7a2a093 100644 --- a/internal/autodetect/main_test.go +++ b/internal/autodetect/main_test.go @@ -33,6 +33,8 @@ import ( kubeTesting "k8s.io/client-go/testing" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/autodetectutils" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus" autoRBAC "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/rbac" @@ -243,8 +245,8 @@ func TestDetectRBACPermissionsBasedOnAvailableClusterRoles(t *testing.T) { } { t.Run(tt.description, func(t *testing.T) { // These settings can be get from env vars - t.Setenv(autoRBAC.NAMESPACE_ENV_VAR, tt.namespace) - t.Setenv(autoRBAC.SA_ENV_VAR, tt.serviceAccount) + t.Setenv(autodetectutils.NAMESPACE_ENV_VAR, tt.namespace) + t.Setenv(autodetectutils.SA_ENV_VAR, tt.serviceAccount) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {})) defer server.Close() @@ -267,3 +269,94 @@ func TestDetectRBACPermissionsBasedOnAvailableClusterRoles(t *testing.T) { }) } } + +func TestCertManagerAvailability(t *testing.T) { + // test data + for _, tt := range []struct { + description string + apiGroupList *metav1.APIGroupList + expectedAvailability certmanager.Availability + namespace string + serviceAccount string + clientGenerator fakeClientGenerator + shouldError bool + }{ + { + description: "CertManager is not installed", + namespace: "default", + serviceAccount: "defaultSA", + apiGroupList: &metav1.APIGroupList{}, + expectedAvailability: certmanager.NotAvailable, + clientGenerator: reactorFactory(v1.SubjectAccessReviewStatus{ + Allowed: true, + }), + shouldError: false, + }, + { + description: "CertManager is installed but RBAC permissions are not granted", + namespace: "default", + serviceAccount: "defaultSA", + apiGroupList: &metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "cert-manager.io", + }, + }, + }, + expectedAvailability: certmanager.NotAvailable, + clientGenerator: reactorFactory(v1.SubjectAccessReviewStatus{ + Allowed: false, + }), + shouldError: true, + }, + { + description: "CertManager is installed and RBAC permissions are granted", + namespace: "default", + serviceAccount: "defaultSA", + apiGroupList: &metav1.APIGroupList{ + Groups: []metav1.APIGroup{ + { + Name: "cert-manager.io", + }, + }, + }, + expectedAvailability: certmanager.Available, + clientGenerator: reactorFactory(v1.SubjectAccessReviewStatus{ + Allowed: true, + }), + shouldError: false, + }, + } { + t.Run(tt.description, func(t *testing.T) { + t.Setenv(autodetectutils.NAMESPACE_ENV_VAR, tt.namespace) + t.Setenv(autodetectutils.SA_ENV_VAR, tt.serviceAccount) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + output, err := json.Marshal(tt.apiGroupList) + require.NoError(t, err) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, err = w.Write(output) + require.NoError(t, err) + })) + defer server.Close() + + r := rbac.NewReviewer(tt.clientGenerator()) + + aD, err := autodetect.New(&rest.Config{Host: server.URL}, r) + require.NoError(t, err) + + // test + cma, err := aD.CertManagerAvailability(context.Background()) + + // verify + assert.Equal(t, tt.expectedAvailability, cma) + if tt.shouldError { + require.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/autodetect/rbac/check.go b/internal/autodetect/rbac/check.go index 1e133ebf49..9c67d79cc3 100644 --- a/internal/autodetect/rbac/check.go +++ b/internal/autodetect/rbac/check.go @@ -17,50 +17,23 @@ package rbac import ( "context" "fmt" - "os" rbacv1 "k8s.io/api/rbac/v1" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/autodetectutils" "github.com/open-telemetry/opentelemetry-operator/internal/rbac" ) -const ( - SA_ENV_VAR = "SERVICE_ACCOUNT_NAME" - NAMESPACE_ENV_VAR = "NAMESPACE" - NAMESPACE_FILE_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" -) - -func getOperatorNamespace() (string, error) { - namespace := os.Getenv(NAMESPACE_ENV_VAR) - if namespace != "" { - return namespace, nil - } - - nsBytes, err := os.ReadFile(NAMESPACE_FILE_PATH) - if err != nil { - return "", err - } - return string(nsBytes), nil -} - -func getOperatorServiceAccount() (string, error) { - sa := os.Getenv(SA_ENV_VAR) - if sa == "" { - return sa, fmt.Errorf("%s env variable not found", SA_ENV_VAR) - } - return sa, nil -} - // CheckRBACPermissions checks if the operator has the needed permissions to create RBAC resources automatically. // If the RBAC is there, no errors nor warnings are returned. func CheckRBACPermissions(ctx context.Context, reviewer *rbac.Reviewer) (admission.Warnings, error) { - namespace, err := getOperatorNamespace() + namespace, err := autodetectutils.GetOperatorNamespace() if err != nil { return nil, fmt.Errorf("%s: %w", "not possible to check RBAC rules", err) } - serviceAccount, err := getOperatorServiceAccount() + serviceAccount, err := autodetectutils.GetOperatorServiceAccount() if err != nil { return nil, fmt.Errorf("%s: %w", "not possible to check RBAC rules", err) } diff --git a/internal/config/main.go b/internal/config/main.go index 48a09faa67..434ae5493f 100644 --- a/internal/config/main.go +++ b/internal/config/main.go @@ -23,6 +23,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus" autoRBAC "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/rbac" @@ -65,6 +66,7 @@ type Config struct { openshiftRoutesAvailability openshift.RoutesAvailability prometheusCRAvailability prometheus.Availability + certManagerAvailability certmanager.Availability labelsFilter []string annotationsFilter []string } @@ -76,6 +78,7 @@ func New(opts ...Option) Config { prometheusCRAvailability: prometheus.NotAvailable, openshiftRoutesAvailability: openshift.RoutesNotAvailable, createRBACPermissions: autoRBAC.NotAvailable, + certManagerAvailability: certmanager.NotAvailable, collectorConfigMapEntry: defaultCollectorConfigMapEntry, targetAllocatorConfigMapEntry: defaultTargetAllocatorConfigMapEntry, operatorOpAMPBridgeConfigMapEntry: defaultOperatorOpAMPBridgeConfigMapEntry, @@ -108,6 +111,7 @@ func New(opts ...Option) Config { logger: o.logger, openshiftRoutesAvailability: o.openshiftRoutesAvailability, prometheusCRAvailability: o.prometheusCRAvailability, + certManagerAvailability: o.certManagerAvailability, autoInstrumentationJavaImage: o.autoInstrumentationJavaImage, autoInstrumentationNodeJSImage: o.autoInstrumentationNodeJSImage, autoInstrumentationPythonImage: o.autoInstrumentationPythonImage, @@ -146,6 +150,13 @@ func (c *Config) AutoDetect() error { c.createRBACPermissions = rAuto c.logger.V(2).Info("create rbac permissions detected", "availability", rAuto) + cmAvl, err := c.autoDetect.CertManagerAvailability(context.Background()) + if err != nil { + c.logger.V(2).Info("the cert manager crd and permissions are not set for the operator", "reason", err) + } + c.certManagerAvailability = cmAvl + c.logger.V(2).Info("the cert manager crd and permissions are set for the operator", "availability", cmAvl) + return nil } @@ -234,6 +245,11 @@ func (c *Config) PrometheusCRAvailability() prometheus.Availability { return c.prometheusCRAvailability } +// CertManagerAvailability represents the availability of the Cert-Manager. +func (c *Config) CertManagerAvailability() certmanager.Availability { + return c.certManagerAvailability +} + // AutoInstrumentationJavaImage returns OpenTelemetry Java auto-instrumentation container image. func (c *Config) AutoInstrumentationJavaImage() string { return c.autoInstrumentationJavaImage diff --git a/internal/config/main_test.go b/internal/config/main_test.go index 08882a0392..4d075e62bb 100644 --- a/internal/config/main_test.go +++ b/internal/config/main_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/require" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/rbac" @@ -56,6 +57,9 @@ func TestConfigChangesOnAutoDetect(t *testing.T) { RBACPermissionsFunc: func(ctx context.Context) (rbac.Availability, error) { return rbac.Available, nil }, + CertManagerAvailabilityFunc: func(ctx context.Context) (certmanager.Availability, error) { + return certmanager.Available, nil + }, } cfg := config.New( config.WithAutoDetect(mock), @@ -80,6 +84,7 @@ type mockAutoDetect struct { OpenShiftRoutesAvailabilityFunc func() (openshift.RoutesAvailability, error) PrometheusCRsAvailabilityFunc func() (prometheus.Availability, error) RBACPermissionsFunc func(ctx context.Context) (rbac.Availability, error) + CertManagerAvailabilityFunc func(ctx context.Context) (certmanager.Availability, error) } func (m *mockAutoDetect) FIPSEnabled(_ context.Context) bool { @@ -106,3 +111,10 @@ func (m *mockAutoDetect) RBACPermissions(ctx context.Context) (rbac.Availability } return rbac.NotAvailable, nil } + +func (m *mockAutoDetect) CertManagerAvailability(ctx context.Context) (certmanager.Availability, error) { + if m.CertManagerAvailabilityFunc != nil { + return m.CertManagerAvailabilityFunc(ctx) + } + return certmanager.NotAvailable, nil +} diff --git a/internal/config/options.go b/internal/config/options.go index 5cb687337e..6046dcc356 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -19,6 +19,7 @@ import ( "go.uber.org/zap/zapcore" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus" autoRBAC "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/rbac" @@ -56,6 +57,7 @@ type options struct { operatorOpAMPBridgeImage string openshiftRoutesAvailability openshift.RoutesAvailability prometheusCRAvailability prometheus.Availability + certManagerAvailability certmanager.Availability labelsFilter []string annotationsFilter []string } @@ -206,6 +208,12 @@ func WithRBACPermissions(rAuto autoRBAC.Availability) Option { } } +func WithCertManagerAvailability(cmAvl certmanager.Availability) Option { + return func(o *options) { + o.certManagerAvailability = cmAvl + } +} + func WithLabelFilters(labelFilters []string) Option { return func(o *options) { o.labelsFilter = append(o.labelsFilter, labelFilters...) diff --git a/internal/manifests/collector/config_replace.go b/internal/manifests/collector/config_replace.go index 6ea55dc44d..6ba35ed435 100644 --- a/internal/manifests/collector/config_replace.go +++ b/internal/manifests/collector/config_replace.go @@ -42,7 +42,7 @@ type Config struct { TargetAllocConfig *targetAllocator `yaml:"target_allocator,omitempty"` } -func ReplaceConfig(otelcol v1beta1.OpenTelemetryCollector, targetAllocator *v1alpha1.TargetAllocator) (string, error) { +func ReplaceConfig(otelcol v1beta1.OpenTelemetryCollector, targetAllocator *v1alpha1.TargetAllocator, options ...ta.TAOption) (string, error) { collectorSpec := otelcol.Spec taEnabled := targetAllocator != nil cfgStr, err := collectorSpec.Config.Yaml() @@ -71,7 +71,7 @@ func ReplaceConfig(otelcol v1beta1.OpenTelemetryCollector, targetAllocator *v1al // To avoid issues caused by Prometheus validation logic, which fails regex validation when it encounters // $$ in the prom config, we update the YAML file directly without marshaling and unmarshalling. - updPromCfgMap, getCfgPromErr := ta.AddTAConfigToPromConfig(promCfgMap, naming.TAService(targetAllocator.Name)) + updPromCfgMap, getCfgPromErr := ta.AddTAConfigToPromConfig(promCfgMap, naming.TAService(targetAllocator.Name), options...) if getCfgPromErr != nil { return "", getCfgPromErr } diff --git a/internal/manifests/collector/configmap.go b/internal/manifests/collector/configmap.go index 54362549ad..b611dea178 100644 --- a/internal/manifests/collector/configmap.go +++ b/internal/manifests/collector/configmap.go @@ -15,12 +15,18 @@ package collector import ( + "path/filepath" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/manifests" "github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils" + ta "github.com/open-telemetry/opentelemetry-operator/internal/manifests/targetallocator/adapters" "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/constants" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) func ConfigMap(params manifests.Params) (*corev1.ConfigMap, error) { @@ -37,7 +43,19 @@ func ConfigMap(params manifests.Params) (*corev1.ConfigMap, error) { return nil, err } - replacedConf, err := ReplaceConfig(params.OtelCol, params.TargetAllocator) + replaceCfgOpts := []ta.TAOption{} + + if params.Config.CertManagerAvailability() == certmanager.Available && featuregate.EnableTargetAllocatorMTLS.IsEnabled() { + replaceCfgOpts = append(replaceCfgOpts, ta.WithTLSConfig( + filepath.Join(constants.TACollectorTLSDirPath, constants.TACollectorCAFileName), + filepath.Join(constants.TACollectorTLSDirPath, constants.TACollectorTLSCertFileName), + filepath.Join(constants.TACollectorTLSDirPath, constants.TACollectorTLSKeyFileName), + naming.TAService(params.OtelCol.Name)), + ) + } + + replacedConf, err := ReplaceConfig(params.OtelCol, params.TargetAllocator, replaceCfgOpts...) + if err != nil { params.Log.V(2).Info("failed to update prometheus config to use sharded targets: ", "err", err) return nil, err diff --git a/internal/manifests/collector/configmap_test.go b/internal/manifests/collector/configmap_test.go index fc66cf3794..a6469704ea 100644 --- a/internal/manifests/collector/configmap_test.go +++ b/internal/manifests/collector/configmap_test.go @@ -18,9 +18,14 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + colfg "go.opentelemetry.io/collector/featuregate" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" + "github.com/open-telemetry/opentelemetry-operator/internal/config" "github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils" "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) func TestDesiredConfigMap(t *testing.T) { @@ -123,4 +128,58 @@ service: }) + t.Run("should return expected escaped collector config map with target_allocator and https config block", func(t *testing.T) { + expectedData := map[string]string{ + "collector.yaml": `exporters: + debug: +receivers: + prometheus: + config: {} + target_allocator: + collector_id: ${POD_NAME} + endpoint: https://test-targetallocator:443 + interval: 30s + tls: + ca_file: /tls/ca.crt + cert_file: /tls/tls.crt + key_file: /tls/tls.key +service: + pipelines: + metrics: + exporters: + - debug + receivers: + - prometheus +`, + } + + param, err := newParams("test/test-img", "testdata/http_sd_config_servicemonitor_test.yaml", config.WithCertManagerAvailability(certmanager.Available)) + require.NoError(t, err) + flgs := featuregate.Flags(colfg.GlobalRegistry()) + err = flgs.Parse([]string{"--feature-gates=operator.targetallocator.mtls"}) + require.NoError(t, err) + + hash, _ := manifestutils.GetConfigMapSHA(param.OtelCol.Spec.Config) + expectedName := naming.ConfigMap("test", hash) + + expectedLables["app.kubernetes.io/component"] = "opentelemetry-collector" + expectedLables["app.kubernetes.io/name"] = "test-collector" + expectedLables["app.kubernetes.io/version"] = "latest" + + param.OtelCol.Spec.TargetAllocator.Enabled = true + actual, err := ConfigMap(param) + + assert.NoError(t, err) + assert.Equal(t, expectedName, actual.Name) + assert.Equal(t, expectedLables, actual.Labels) + assert.Equal(t, len(expectedData), len(actual.Data)) + for k, expected := range expectedData { + assert.YAMLEq(t, expected, actual.Data[k]) + } + + // Reset the value + expectedLables["app.kubernetes.io/version"] = "0.47.0" + assert.NoError(t, err) + + }) } diff --git a/internal/manifests/collector/container.go b/internal/manifests/collector/container.go index 3cf0b1a2e4..9be69c2234 100644 --- a/internal/manifests/collector/container.go +++ b/internal/manifests/collector/container.go @@ -25,8 +25,10 @@ import ( "k8s.io/apimachinery/pkg/util/validation" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/constants" "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) @@ -83,6 +85,14 @@ func Container(cfg config.Config, logger logr.Logger, otelcol v1beta1.OpenTeleme }) } + if cfg.CertManagerAvailability() == certmanager.Available && featuregate.EnableTargetAllocatorMTLS.IsEnabled() { + volumeMounts = append(volumeMounts, + corev1.VolumeMount{ + Name: naming.TAClientCertificate(otelcol.Name), + MountPath: constants.TACollectorTLSDirPath, + }) + } + // ensure that the v1alpha1.OpenTelemetryCollectorSpec.Args are ordered when moved to container.Args, // where iterating over a map does not guarantee, so that reconcile will not be fooled by different // ordering in args. diff --git a/internal/manifests/collector/container_test.go b/internal/manifests/collector/container_test.go index 597e98c1e7..3f48fc26da 100644 --- a/internal/manifests/collector/container_test.go +++ b/internal/manifests/collector/container_test.go @@ -20,14 +20,19 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + colfg "go.opentelemetry.io/collector/featuregate" "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" logf "sigs.k8s.io/controller-runtime/pkg/log" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" . "github.com/open-telemetry/opentelemetry-operator/internal/manifests/collector" + "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/constants" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) var logger = logf.Log.WithName("unit-tests") @@ -860,3 +865,22 @@ func mustUnmarshalToConfig(t *testing.T, config string) v1beta1.Config { } return cfg } + +func TestContainerWithCertManagerAvailable(t *testing.T) { + otelcol := v1beta1.OpenTelemetryCollector{} + + cfg := config.New(config.WithCertManagerAvailability(certmanager.Available)) + + flgs := featuregate.Flags(colfg.GlobalRegistry()) + err := flgs.Parse([]string{"--feature-gates=operator.targetallocator.mtls"}) + require.NoError(t, err) + + // test + c := Container(cfg, logger, otelcol, true) + + // verify + assert.Contains(t, c.VolumeMounts, corev1.VolumeMount{ + Name: naming.TAClientCertificate(""), + MountPath: constants.TACollectorTLSDirPath, + }) +} diff --git a/internal/manifests/collector/volume.go b/internal/manifests/collector/volume.go index ea033b3a4a..f1bd201056 100644 --- a/internal/manifests/collector/volume.go +++ b/internal/manifests/collector/volume.go @@ -19,9 +19,11 @@ import ( corev1 "k8s.io/api/core/v1" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" "github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils" "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) // Volumes builds the volumes for the given instance, including the config map volume. @@ -41,6 +43,17 @@ func Volumes(cfg config.Config, otelcol v1beta1.OpenTelemetryCollector) []corev1 }, }} + if cfg.CertManagerAvailability() == certmanager.Available && featuregate.EnableTargetAllocatorMTLS.IsEnabled() { + volumes = append(volumes, corev1.Volume{ + Name: naming.TAClientCertificate(otelcol.Name), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: naming.TAClientCertificateSecretName(otelcol.Name), + }, + }, + }) + } + if len(otelcol.Spec.Volumes) > 0 { volumes = append(volumes, otelcol.Spec.Volumes...) } diff --git a/internal/manifests/collector/volume_test.go b/internal/manifests/collector/volume_test.go index 06832e6314..03747d519e 100644 --- a/internal/manifests/collector/volume_test.go +++ b/internal/manifests/collector/volume_test.go @@ -18,12 +18,17 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + colfg "go.opentelemetry.io/collector/featuregate" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" . "github.com/open-telemetry/opentelemetry-operator/internal/manifests/collector" "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) func TestVolumeNewDefault(t *testing.T) { @@ -89,3 +94,50 @@ func TestVolumeWithMoreConfigMaps(t *testing.T) { assert.Equal(t, "configmap-configmap-test", volumes[1].Name) assert.Equal(t, "configmap-configmap-test2", volumes[2].Name) } + +func TestVolumeWithTargetAllocatorMTLS(t *testing.T) { + t.Run("CertManager available and EnableTargetAllocatorMTLS enabled", func(t *testing.T) { + otelcol := v1beta1.OpenTelemetryCollector{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-collector", + }, + } + cfg := config.New(config.WithCertManagerAvailability(certmanager.Available)) + + flgs := featuregate.Flags(colfg.GlobalRegistry()) + err := flgs.Parse([]string{"--feature-gates=operator.targetallocator.mtls"}) + require.NoError(t, err) + + volumes := Volumes(cfg, otelcol) + + expectedVolume := corev1.Volume{ + Name: naming.TAClientCertificate(otelcol.Name), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: naming.TAClientCertificateSecretName(otelcol.Name), + }, + }, + } + assert.Contains(t, volumes, expectedVolume) + }) + + t.Run("CertManager not available", func(t *testing.T) { + otelcol := v1beta1.OpenTelemetryCollector{} + cfg := config.New(config.WithCertManagerAvailability(certmanager.NotAvailable)) + + flgs := featuregate.Flags(colfg.GlobalRegistry()) + err := flgs.Parse([]string{"--feature-gates=operator.targetallocator.mtls"}) + require.NoError(t, err) + + volumes := Volumes(cfg, otelcol) + assert.NotContains(t, volumes, corev1.Volume{Name: naming.TAClientCertificate(otelcol.Name)}) + }) + + t.Run("EnableTargetAllocatorMTLS disabled", func(t *testing.T) { + otelcol := v1beta1.OpenTelemetryCollector{} + cfg := config.New(config.WithCertManagerAvailability(certmanager.Available)) + + volumes := Volumes(cfg, otelcol) + assert.NotContains(t, volumes, corev1.Volume{Name: naming.TAClientCertificate(otelcol.Name)}) + }) +} diff --git a/internal/manifests/mutate.go b/internal/manifests/mutate.go index 75c1a07804..27b4e31266 100644 --- a/internal/manifests/mutate.go +++ b/internal/manifests/mutate.go @@ -20,6 +20,7 @@ import ( "reflect" "dario.cat/mergo" + cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" routev1 "github.com/openshift/api/route/v1" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" appsv1 "k8s.io/api/apps/v1" @@ -166,6 +167,16 @@ func MutateFuncFor(existing, desired client.Object) controllerutil.MutateFn { wantPr := desired.(*corev1.Secret) mutateSecret(pr, wantPr) + case *cmv1.Certificate: + cert := existing.(*cmv1.Certificate) + wantCert := desired.(*cmv1.Certificate) + mutateCertificate(cert, wantCert) + + case *cmv1.Issuer: + issuer := existing.(*cmv1.Issuer) + wantIssuer := desired.(*cmv1.Issuer) + mutateIssuer(issuer, wantIssuer) + default: t := reflect.TypeOf(existing).String() return fmt.Errorf("missing mutate implementation for resource type: %s", t) @@ -330,6 +341,18 @@ func mutateStatefulSet(existing, desired *appsv1.StatefulSet) error { return nil } +func mutateCertificate(existing, desired *cmv1.Certificate) { + existing.Annotations = desired.Annotations + existing.Labels = desired.Labels + existing.Spec = desired.Spec +} + +func mutateIssuer(existing, desired *cmv1.Issuer) { + existing.Annotations = desired.Annotations + existing.Labels = desired.Labels + existing.Spec = desired.Spec +} + func hasImmutableFieldChange(existing, desired *appsv1.StatefulSet) (bool, string) { if existing.CreationTimestamp.IsZero() { return false, "" diff --git a/internal/manifests/targetallocator/adapters/config_to_prom_config.go b/internal/manifests/targetallocator/adapters/config_to_prom_config.go index e0d7cd38e2..6395c6f2a0 100644 --- a/internal/manifests/targetallocator/adapters/config_to_prom_config.go +++ b/internal/manifests/targetallocator/adapters/config_to_prom_config.go @@ -23,6 +23,8 @@ import ( "github.com/open-telemetry/opentelemetry-operator/internal/manifests/collector/adapters" ) +type TAOption func(targetAllocatorCfg map[interface{}]interface{}) error + func errorNoComponent(component string) error { return fmt.Errorf("no %s available as part of the configuration", component) } @@ -257,10 +259,31 @@ func AddHTTPSDConfigToPromConfig(prometheus map[interface{}]interface{}, taServi return prometheus, nil } +func WithTLSConfig(caFile, certFile, keyFile, taServiceName string) TAOption { + return func(targetAllocatorCfg map[interface{}]interface{}) error { + if _, exists := targetAllocatorCfg["tls"]; !exists { + targetAllocatorCfg["tls"] = make(map[interface{}]interface{}) + } + + tlsCfg, ok := targetAllocatorCfg["tls"].(map[interface{}]interface{}) + if !ok { + return errorNotAMap("tls") + } + + tlsCfg["ca_file"] = caFile + tlsCfg["cert_file"] = certFile + tlsCfg["key_file"] = keyFile + + targetAllocatorCfg["endpoint"] = fmt.Sprintf("https://%s:443", taServiceName) + + return nil + } +} + // AddTAConfigToPromConfig adds or updates the target_allocator configuration in the Prometheus configuration. // If the `EnableTargetAllocatorRewrite` feature flag for the target allocator is enabled, this function // removes the existing scrape_configs from the collector's Prometheus configuration as it's not required. -func AddTAConfigToPromConfig(prometheus map[interface{}]interface{}, taServiceName string) (map[interface{}]interface{}, error) { +func AddTAConfigToPromConfig(prometheus map[interface{}]interface{}, taServiceName string, taOpts ...TAOption) (map[interface{}]interface{}, error) { prometheusConfigProperty, ok := prometheus["config"] if !ok { return nil, errorNoComponent("prometheusConfig") @@ -285,6 +308,13 @@ func AddTAConfigToPromConfig(prometheus map[interface{}]interface{}, taServiceNa targetAllocatorCfg["interval"] = "30s" targetAllocatorCfg["collector_id"] = "${POD_NAME}" + for _, opt := range taOpts { + err := opt(targetAllocatorCfg) + if err != nil { + return nil, err + } + } + // Remove the scrape_configs key from the map delete(prometheusCfg, "scrape_configs") diff --git a/internal/manifests/targetallocator/adapters/config_to_prom_config_test.go b/internal/manifests/targetallocator/adapters/config_to_prom_config_test.go index 2ad7b741c6..b06d1ed67a 100644 --- a/internal/manifests/targetallocator/adapters/config_to_prom_config_test.go +++ b/internal/manifests/targetallocator/adapters/config_to_prom_config_test.go @@ -518,3 +518,45 @@ func TestValidateTargetAllocatorConfig(t *testing.T) { }) } } + +func TestAddTAConfigToPromConfigWithTLSConfig(t *testing.T) { + t.Run("should return expected prom config map with TA config and TLS config", func(t *testing.T) { + cfg := map[interface{}]interface{}{ + "config": map[interface{}]interface{}{ + "scrape_configs": []interface{}{ + map[interface{}]interface{}{ + "job_name": "test_job", + "static_configs": []interface{}{ + map[interface{}]interface{}{ + "targets": []interface{}{ + "localhost:9090", + }, + }, + }, + }, + }, + }, + } + + taServiceName := "test-targetallocator" + + expectedResult := map[interface{}]interface{}{ + "config": map[interface{}]interface{}{}, + "target_allocator": map[interface{}]interface{}{ + "endpoint": "https://test-targetallocator:443", + "interval": "30s", + "collector_id": "${POD_NAME}", + "tls": map[interface{}]interface{}{ + "ca_file": "ca.crt", + "cert_file": "tls.crt", + "key_file": "tls.key", + }, + }, + } + + result, err := ta.AddTAConfigToPromConfig(cfg, taServiceName, ta.WithTLSConfig("ca.crt", "tls.crt", "tls.key", taServiceName)) + + assert.NoError(t, err) + assert.Equal(t, expectedResult, result) + }) +} diff --git a/internal/manifests/targetallocator/certificate.go b/internal/manifests/targetallocator/certificate.go new file mode 100644 index 0000000000..46357eca23 --- /dev/null +++ b/internal/manifests/targetallocator/certificate.go @@ -0,0 +1,118 @@ +// 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 targetallocator + +import ( + "fmt" + + cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils" + "github.com/open-telemetry/opentelemetry-operator/internal/naming" +) + +// / CACertificate returns a CA Certificate for the given instance. +func CACertificate(params Params) *cmv1.Certificate { + name := naming.CACertificate(params.TargetAllocator.Name) + labels := manifestutils.Labels(params.TargetAllocator.ObjectMeta, name, params.TargetAllocator.Spec.Image, ComponentOpenTelemetryTargetAllocator, nil) + + return &cmv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: params.TargetAllocator.Namespace, + Name: name, + Labels: labels, + }, + Spec: cmv1.CertificateSpec{ + IsCA: true, + CommonName: naming.CACertificate(params.TargetAllocator.Name), + Subject: &cmv1.X509Subject{ + OrganizationalUnits: []string{"opentelemetry-operator"}, + }, + SecretName: naming.CACertificate(params.TargetAllocator.Name), + IssuerRef: cmmeta.ObjectReference{ + Name: naming.SelfSignedIssuer(params.TargetAllocator.Name), + Kind: "Issuer", + }, + }, + } +} + +// ServingCertificate returns a serving Certificate for the given instance. +func ServingCertificate(params Params) *cmv1.Certificate { + name := naming.TAServerCertificate(params.TargetAllocator.Name) + labels := manifestutils.Labels(params.TargetAllocator.ObjectMeta, name, params.TargetAllocator.Spec.Image, ComponentOpenTelemetryTargetAllocator, nil) + + return &cmv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: params.TargetAllocator.Namespace, + Name: name, + Labels: labels, + }, + Spec: cmv1.CertificateSpec{ + DNSNames: []string{ + naming.TAService(params.TargetAllocator.Name), + fmt.Sprintf("%s.%s.svc", naming.TAService(params.TargetAllocator.Name), params.TargetAllocator.Namespace), + fmt.Sprintf("%s.%s.svc.cluster.local", naming.TAService(params.TargetAllocator.Name), params.TargetAllocator.Namespace), + }, + IssuerRef: cmmeta.ObjectReference{ + Kind: "Issuer", + Name: naming.CAIssuer(params.TargetAllocator.Name), + }, + Usages: []cmv1.KeyUsage{ + cmv1.UsageClientAuth, + cmv1.UsageServerAuth, + }, + SecretName: naming.TAServerCertificate(params.TargetAllocator.Name), + Subject: &cmv1.X509Subject{ + OrganizationalUnits: []string{"opentelemetry-operator"}, + }, + }, + } +} + +// ClientCertificate returns a client Certificate for the given instance. +func ClientCertificate(params Params) *cmv1.Certificate { + name := naming.TAClientCertificate(params.TargetAllocator.Name) + labels := manifestutils.Labels(params.TargetAllocator.ObjectMeta, name, params.TargetAllocator.Spec.Image, ComponentOpenTelemetryTargetAllocator, nil) + + return &cmv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: params.TargetAllocator.Namespace, + Name: name, + Labels: labels, + }, + Spec: cmv1.CertificateSpec{ + DNSNames: []string{ + naming.TAService(params.TargetAllocator.Name), + fmt.Sprintf("%s.%s.svc", naming.TAService(params.TargetAllocator.Name), params.TargetAllocator.Namespace), + fmt.Sprintf("%s.%s.svc.cluster.local", naming.TAService(params.TargetAllocator.Name), params.TargetAllocator.Namespace), + }, + IssuerRef: cmmeta.ObjectReference{ + Kind: "Issuer", + Name: naming.CAIssuer(params.TargetAllocator.Name), + }, + Usages: []cmv1.KeyUsage{ + cmv1.UsageClientAuth, + cmv1.UsageServerAuth, + }, + SecretName: naming.TAClientCertificate(params.TargetAllocator.Name), + Subject: &cmv1.X509Subject{ + OrganizationalUnits: []string{"opentelemetry-operator"}, + }, + }, + } +} diff --git a/internal/manifests/targetallocator/certificate_test.go b/internal/manifests/targetallocator/certificate_test.go new file mode 100644 index 0000000000..ae9dceb6a7 --- /dev/null +++ b/internal/manifests/targetallocator/certificate_test.go @@ -0,0 +1,221 @@ +// 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 targetallocator + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" + "github.com/open-telemetry/opentelemetry-operator/internal/config" +) + +type CACertificateConfig struct { + Name string + Namespace string + SecretName string + IssuerName string +} + +type ServingCertificateConfig struct { + Name string + Namespace string + SecretName string + IssuerName string +} + +type ClientCertificateConfig struct { + Name string + Namespace string + SecretName string + IssuerName string +} + +func TestCACertificate(t *testing.T) { + tests := []struct { + name string + targetAllocator v1alpha1.TargetAllocator + expectedCAConfig CACertificateConfig + expectedLabels map[string]string + }{ + { + name: "Default CA Certificate", + targetAllocator: v1alpha1.TargetAllocator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + Namespace: "my-namespace", + }, + }, + expectedCAConfig: CACertificateConfig{ + Name: "my-instance-ca-cert", + Namespace: "my-namespace", + SecretName: "my-instance-ca-cert", + IssuerName: "my-instance-self-signed-issuer", + }, + expectedLabels: map[string]string{ + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/instance": "my-namespace.my-instance", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/name": "my-instance-ca-cert", + "app.kubernetes.io/version": "latest", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := Params{ + TargetAllocator: tt.targetAllocator, + Config: config.New(), + } + + caCert := CACertificate(params) + + assert.Equal(t, tt.expectedCAConfig.Name, caCert.Name) + assert.Equal(t, tt.expectedCAConfig.Namespace, caCert.Namespace) + assert.Equal(t, tt.expectedCAConfig.SecretName, caCert.Spec.SecretName) + assert.Equal(t, tt.expectedCAConfig.IssuerName, caCert.Spec.IssuerRef.Name) + assert.True(t, caCert.Spec.IsCA) + assert.Equal(t, "Issuer", caCert.Spec.IssuerRef.Kind) + assert.Equal(t, []string{"opentelemetry-operator"}, caCert.Spec.Subject.OrganizationalUnits) + assert.Equal(t, tt.expectedLabels, caCert.Labels) + }) + } +} + +func TestServingCertificate(t *testing.T) { + tests := []struct { + name string + targetAllocator v1alpha1.TargetAllocator + expectedServingConfig ServingCertificateConfig + expectedDNSNames []string + expectedOrganizationUnit []string + expectedLabels map[string]string + }{ + { + name: "Default Serving Certificate", + targetAllocator: v1alpha1.TargetAllocator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + Namespace: "my-namespace", + }, + }, + expectedServingConfig: ServingCertificateConfig{ + Name: "my-instance-ta-server-cert", + Namespace: "my-namespace", + SecretName: "my-instance-ta-server-cert", + IssuerName: "my-instance-ca-issuer", + }, + expectedDNSNames: []string{ + "my-instance-targetallocator", + "my-instance-targetallocator.my-namespace.svc", + "my-instance-targetallocator.my-namespace.svc.cluster.local", + }, + expectedOrganizationUnit: []string{"opentelemetry-operator"}, + expectedLabels: map[string]string{ + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/instance": "my-namespace.my-instance", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/name": "my-instance-ta-server-cert", + "app.kubernetes.io/version": "latest", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := Params{ + TargetAllocator: tt.targetAllocator, + Config: config.New(), + } + + servingCert := ServingCertificate(params) + + assert.Equal(t, tt.expectedServingConfig.Name, servingCert.Name) + assert.Equal(t, tt.expectedServingConfig.Namespace, servingCert.Namespace) + assert.Equal(t, tt.expectedServingConfig.SecretName, servingCert.Spec.SecretName) + assert.Equal(t, tt.expectedServingConfig.IssuerName, servingCert.Spec.IssuerRef.Name) + assert.Equal(t, "Issuer", servingCert.Spec.IssuerRef.Kind) + assert.ElementsMatch(t, tt.expectedDNSNames, servingCert.Spec.DNSNames) + assert.ElementsMatch(t, tt.expectedOrganizationUnit, servingCert.Spec.Subject.OrganizationalUnits) + assert.Equal(t, tt.expectedLabels, servingCert.Labels) + }) + } +} + +func TestClientCertificate(t *testing.T) { + tests := []struct { + name string + targetAllocator v1alpha1.TargetAllocator + expectedClientConfig ClientCertificateConfig + expectedDNSNames []string + expectedOrganizationUnit []string + expectedLabels map[string]string + }{ + { + name: "Default Client Certificate", + targetAllocator: v1alpha1.TargetAllocator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + Namespace: "my-namespace", + }, + }, + expectedClientConfig: ClientCertificateConfig{ + Name: "my-instance-ta-client-cert", + Namespace: "my-namespace", + SecretName: "my-instance-ta-client-cert", + IssuerName: "my-instance-ca-issuer", + }, + expectedDNSNames: []string{ + "my-instance-targetallocator", + "my-instance-targetallocator.my-namespace.svc", + "my-instance-targetallocator.my-namespace.svc.cluster.local", + }, + expectedOrganizationUnit: []string{"opentelemetry-operator"}, + expectedLabels: map[string]string{ + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/instance": "my-namespace.my-instance", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/name": "my-instance-ta-client-cert", + "app.kubernetes.io/version": "latest", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := Params{ + TargetAllocator: tt.targetAllocator, + Config: config.New(), + } + + clientCert := ClientCertificate(params) + + assert.Equal(t, tt.expectedClientConfig.Name, clientCert.Name) + assert.Equal(t, tt.expectedClientConfig.Namespace, clientCert.Namespace) + assert.Equal(t, tt.expectedClientConfig.SecretName, clientCert.Spec.SecretName) + assert.Equal(t, tt.expectedClientConfig.IssuerName, clientCert.Spec.IssuerRef.Name) + assert.Equal(t, "Issuer", clientCert.Spec.IssuerRef.Kind) + assert.ElementsMatch(t, tt.expectedDNSNames, clientCert.Spec.DNSNames) + assert.ElementsMatch(t, tt.expectedOrganizationUnit, clientCert.Spec.Subject.OrganizationalUnits) + assert.Equal(t, tt.expectedLabels, clientCert.Labels) + }) + } +} diff --git a/internal/manifests/targetallocator/configmap.go b/internal/manifests/targetallocator/configmap.go index 496c625ae3..b17df29151 100644 --- a/internal/manifests/targetallocator/configmap.go +++ b/internal/manifests/targetallocator/configmap.go @@ -15,16 +15,21 @@ package targetallocator import ( + "path/filepath" + "github.com/mitchellh/mapstructure" "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/manifests/collector" "github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils" "github.com/open-telemetry/opentelemetry-operator/internal/manifests/targetallocator/adapters" "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/constants" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) const ( @@ -102,6 +107,16 @@ func ConfigMap(params Params) (*corev1.ConfigMap, error) { taConfig["prometheus_cr"] = prometheusCRConfig } + if params.Config.CertManagerAvailability() == certmanager.Available && featuregate.EnableTargetAllocatorMTLS.IsEnabled() { + taConfig["https"] = map[string]interface{}{ + "enabled": true, + "listen_addr": ":8443", + "ca_file_path": filepath.Join(constants.TACollectorTLSDirPath, constants.TACollectorCAFileName), + "tls_cert_file_path": filepath.Join(constants.TACollectorTLSDirPath, constants.TACollectorTLSCertFileName), + "tls_key_file_path": filepath.Join(constants.TACollectorTLSDirPath, constants.TACollectorTLSKeyFileName), + } + } + taConfigYAML, err := yaml.Marshal(taConfig) if err != nil { return &corev1.ConfigMap{}, err diff --git a/internal/manifests/targetallocator/configmap_test.go b/internal/manifests/targetallocator/configmap_test.go index 66553bf783..de863874db 100644 --- a/internal/manifests/targetallocator/configmap_test.go +++ b/internal/manifests/targetallocator/configmap_test.go @@ -23,10 +23,13 @@ import ( "github.com/mitchellh/mapstructure" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + colfg "go.opentelemetry.io/collector/featuregate" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) func TestDesiredConfigMap(t *testing.T) { @@ -239,6 +242,61 @@ prometheus_cr: }) + t.Run("should return expected target allocator config map with HTTPS configuration", func(t *testing.T) { + expectedLabels["app.kubernetes.io/component"] = "opentelemetry-targetallocator" + expectedLabels["app.kubernetes.io/name"] = "my-instance-targetallocator" + + cfg := config.New(config.WithCertManagerAvailability(certmanager.Available)) + + flgs := featuregate.Flags(colfg.GlobalRegistry()) + err := flgs.Parse([]string{"--feature-gates=operator.targetallocator.mtls"}) + require.NoError(t, err) + + testParams := Params{ + Collector: collector, + TargetAllocator: targetAllocator, + Config: cfg, + } + + expectedData := map[string]string{ + targetAllocatorFilename: `allocation_strategy: consistent-hashing +collector_selector: + matchlabels: + app.kubernetes.io/component: opentelemetry-collector + app.kubernetes.io/instance: default.my-instance + app.kubernetes.io/managed-by: opentelemetry-operator + app.kubernetes.io/part-of: opentelemetry + matchexpressions: [] +config: + scrape_configs: + - job_name: otel-collector + scrape_interval: 10s + static_configs: + - targets: + - 0.0.0.0:8888 + - 0.0.0.0:9999 +filter_strategy: relabel-config +https: + ca_file_path: /tls/ca.crt + enabled: true + listen_addr: :8443 + tls_cert_file_path: /tls/tls.crt + tls_key_file_path: /tls/tls.key +prometheus_cr: + enabled: true + pod_monitor_selector: null + scrape_interval: 30s + service_monitor_selector: null +`, + } + + actual, err := ConfigMap(testParams) + assert.NoError(t, err) + + assert.Equal(t, "my-instance-targetallocator", actual.Name) + assert.Equal(t, expectedLabels, actual.Labels) + assert.Equal(t, expectedData, actual.Data) + }) } func TestGetScrapeConfigsFromOtelConfig(t *testing.T) { diff --git a/internal/manifests/targetallocator/container.go b/internal/manifests/targetallocator/container.go index 4409912a76..f1e5e78bbc 100644 --- a/internal/manifests/targetallocator/container.go +++ b/internal/manifests/targetallocator/container.go @@ -24,8 +24,10 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/constants" "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) @@ -128,6 +130,18 @@ func Container(cfg config.Config, logger logr.Logger, instance v1alpha1.TargetAl }, } + if cfg.CertManagerAvailability() == certmanager.Available && featuregate.EnableTargetAllocatorMTLS.IsEnabled() { + ports = append(ports, corev1.ContainerPort{ + Name: "https", + ContainerPort: 8443, + Protocol: corev1.ProtocolTCP, + }) + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: naming.TAServerCertificate(instance.Name), + MountPath: constants.TACollectorTLSDirPath, + }) + } + envVars = append(envVars, proxy.ReadProxyVarsFromEnv()...) return corev1.Container{ Name: naming.TAContainer(), diff --git a/internal/manifests/targetallocator/container_test.go b/internal/manifests/targetallocator/container_test.go index 6bfcce4eb9..7ce57d4257 100644 --- a/internal/manifests/targetallocator/container_test.go +++ b/internal/manifests/targetallocator/container_test.go @@ -20,6 +20,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + colfg "go.opentelemetry.io/collector/featuregate" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/intstr" @@ -27,8 +28,11 @@ import ( "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/constants" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) var logger = logf.Log.WithName("unit-tests") @@ -384,6 +388,31 @@ func TestArgs(t *testing.T) { assert.Equal(t, expected, c.Args) } +func TestContainerWithCertManagerAvailable(t *testing.T) { + // prepare + targetAllocator := v1alpha1.TargetAllocator{} + + flgs := featuregate.Flags(colfg.GlobalRegistry()) + err := flgs.Parse([]string{"--feature-gates=operator.targetallocator.mtls"}) + require.NoError(t, err) + + cfg := config.New(config.WithCertManagerAvailability(certmanager.Available)) + + // test + c := Container(cfg, logger, targetAllocator) + + // verify + assert.Equal(t, "http", c.Ports[0].Name) + assert.Equal(t, int32(8080), c.Ports[0].ContainerPort) + assert.Equal(t, "https", c.Ports[1].Name) + assert.Equal(t, int32(8443), c.Ports[1].ContainerPort) + + assert.Contains(t, c.VolumeMounts, corev1.VolumeMount{ + Name: naming.TAServerCertificate(""), + MountPath: constants.TACollectorTLSDirPath, + }) +} + func TestContainerCustomVolumes(t *testing.T) { // prepare targetAllocator := v1alpha1.TargetAllocator{ diff --git a/internal/manifests/targetallocator/issuer.go b/internal/manifests/targetallocator/issuer.go new file mode 100644 index 0000000000..8732fd1376 --- /dev/null +++ b/internal/manifests/targetallocator/issuer.go @@ -0,0 +1,63 @@ +// 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 targetallocator + +import ( + cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils" + "github.com/open-telemetry/opentelemetry-operator/internal/naming" +) + +// SelfSignedIssuer returns a self-signed issuer for the given instance. +func SelfSignedIssuer(params Params) *cmv1.Issuer { + name := naming.SelfSignedIssuer(params.TargetAllocator.Name) + labels := manifestutils.Labels(params.TargetAllocator.ObjectMeta, name, params.TargetAllocator.Spec.Image, ComponentOpenTelemetryTargetAllocator, nil) + + return &cmv1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: params.TargetAllocator.Namespace, + Labels: labels, + }, + Spec: cmv1.IssuerSpec{ + IssuerConfig: cmv1.IssuerConfig{ + SelfSigned: &cmv1.SelfSignedIssuer{}, + }, + }, + } +} + +// CAIssuer returns a CA issuer for the given instance. +func CAIssuer(params Params) *cmv1.Issuer { + name := naming.CAIssuer(params.TargetAllocator.Name) + labels := manifestutils.Labels(params.TargetAllocator.ObjectMeta, name, params.TargetAllocator.Spec.Image, ComponentOpenTelemetryTargetAllocator, nil) + + return &cmv1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: params.TargetAllocator.Namespace, + Labels: labels, + }, + Spec: cmv1.IssuerSpec{ + IssuerConfig: cmv1.IssuerConfig{ + CA: &cmv1.CAIssuer{ + SecretName: naming.CACertificate(params.TargetAllocator.Name), + }, + }, + }, + } +} diff --git a/internal/manifests/targetallocator/issuer_test.go b/internal/manifests/targetallocator/issuer_test.go new file mode 100644 index 0000000000..d5d0c1d021 --- /dev/null +++ b/internal/manifests/targetallocator/issuer_test.go @@ -0,0 +1,113 @@ +// 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 targetallocator + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" + "github.com/open-telemetry/opentelemetry-operator/internal/config" +) + +type SelfSignedIssuerConfig struct { + Name string + Namespace string + Labels map[string]string +} + +type CAIssuerConfig struct { + Name string + Namespace string + Labels map[string]string + SecretName string +} + +func TestSelfSignedIssuer(t *testing.T) { + taSpec := v1alpha1.TargetAllocatorSpec{} + ta := v1alpha1.TargetAllocator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + Namespace: "my-namespace", + }, + Spec: taSpec, + } + + cfg := config.New() + + expected := SelfSignedIssuerConfig{ + Name: "my-instance-self-signed-issuer", + Namespace: "my-namespace", + Labels: map[string]string{ + "app.kubernetes.io/name": "my-instance-self-signed-issuer", + "app.kubernetes.io/instance": "my-namespace.my-instance", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/version": "latest", + }, + } + + params := Params{ + Config: cfg, + TargetAllocator: ta, + } + + issuer := SelfSignedIssuer(params) + + assert.Equal(t, expected.Name, issuer.Name) + assert.Equal(t, expected.Namespace, issuer.Namespace) + assert.Equal(t, expected.Labels, issuer.Labels) + assert.NotNil(t, issuer.Spec.SelfSigned) +} + +func TestCAIssuer(t *testing.T) { + ta := v1alpha1.TargetAllocator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + Namespace: "my-namespace", + }, + } + + cfg := config.New() + + expected := CAIssuerConfig{ + Name: "my-instance-ca-issuer", + Namespace: "my-namespace", + Labels: map[string]string{ + "app.kubernetes.io/name": "my-instance-ca-issuer", + "app.kubernetes.io/instance": "my-namespace.my-instance", + "app.kubernetes.io/managed-by": "opentelemetry-operator", + "app.kubernetes.io/part-of": "opentelemetry", + "app.kubernetes.io/component": "opentelemetry-targetallocator", + "app.kubernetes.io/version": "latest", + }, + SecretName: "my-instance-ca-cert", + } + + params := Params{ + Config: cfg, + TargetAllocator: ta, + } + + issuer := CAIssuer(params) + + assert.Equal(t, expected.Name, issuer.Name) + assert.Equal(t, expected.Namespace, issuer.Namespace) + assert.Equal(t, expected.Labels, issuer.Labels) + assert.Equal(t, expected.SecretName, issuer.Spec.CA.SecretName) +} diff --git a/internal/manifests/targetallocator/service.go b/internal/manifests/targetallocator/service.go index 9577a43290..b372cd97a2 100644 --- a/internal/manifests/targetallocator/service.go +++ b/internal/manifests/targetallocator/service.go @@ -19,8 +19,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/manifests/manifestutils" "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) func Service(params Params) *corev1.Service { @@ -28,6 +30,19 @@ func Service(params Params) *corev1.Service { labels := manifestutils.Labels(params.TargetAllocator.ObjectMeta, name, params.TargetAllocator.Spec.Image, ComponentOpenTelemetryTargetAllocator, nil) selector := manifestutils.TASelectorLabels(params.TargetAllocator, ComponentOpenTelemetryTargetAllocator) + ports := make([]corev1.ServicePort, 0) + ports = append(ports, corev1.ServicePort{ + Name: "targetallocation", + Port: 80, + TargetPort: intstr.FromString("http")}) + + if params.Config.CertManagerAvailability() == certmanager.Available && featuregate.EnableTargetAllocatorMTLS.IsEnabled() { + ports = append(ports, corev1.ServicePort{ + Name: "targetallocation-https", + Port: 443, + TargetPort: intstr.FromString("https")}) + } + return &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: naming.TAService(params.TargetAllocator.Name), @@ -35,12 +50,8 @@ func Service(params Params) *corev1.Service { Labels: labels, }, Spec: corev1.ServiceSpec{ - Selector: selector, - Ports: []corev1.ServicePort{{ - Name: "targetallocation", - Port: 80, - TargetPort: intstr.FromString("http"), - }}, + Selector: selector, + Ports: ports, IPFamilies: params.TargetAllocator.Spec.IpFamilies, IPFamilyPolicy: params.TargetAllocator.Spec.IpFamilyPolicy, }, diff --git a/internal/manifests/targetallocator/service_test.go b/internal/manifests/targetallocator/service_test.go index f21e0fe5d6..2c0aead766 100644 --- a/internal/manifests/targetallocator/service_test.go +++ b/internal/manifests/targetallocator/service_test.go @@ -18,10 +18,14 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + colfg "go.opentelemetry.io/collector/featuregate" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) func TestServicePorts(t *testing.T) { @@ -42,3 +46,32 @@ func TestServicePorts(t *testing.T) { assert.Equal(t, ports[0].Port, s.Spec.Ports[0].Port) assert.Equal(t, ports[0].TargetPort, s.Spec.Ports[0].TargetPort) } + +func TestServicePortsWithTargetAllocatorMTLS(t *testing.T) { + targetAllocator := targetAllocatorInstance() + cfg := config.New(config.WithCertManagerAvailability(certmanager.Available)) + + flgs := featuregate.Flags(colfg.GlobalRegistry()) + err := flgs.Parse([]string{"--feature-gates=operator.targetallocator.mtls"}) + require.NoError(t, err) + + params := Params{ + TargetAllocator: targetAllocator, + Config: cfg, + Log: logger, + } + + ports := []v1.ServicePort{ + {Name: "targetallocation", Port: 80, TargetPort: intstr.FromString("http")}, + {Name: "targetallocation-https", Port: 443, TargetPort: intstr.FromString("https")}, + } + + s := Service(params) + + assert.Equal(t, ports[0].Name, s.Spec.Ports[0].Name) + assert.Equal(t, ports[0].Port, s.Spec.Ports[0].Port) + assert.Equal(t, ports[0].TargetPort, s.Spec.Ports[0].TargetPort) + assert.Equal(t, ports[1].Name, s.Spec.Ports[1].Name) + assert.Equal(t, ports[1].Port, s.Spec.Ports[1].Port) + assert.Equal(t, ports[1].TargetPort, s.Spec.Ports[1].TargetPort) +} diff --git a/internal/manifests/targetallocator/targetallocator.go b/internal/manifests/targetallocator/targetallocator.go index e1da206f4f..21b00eebc8 100644 --- a/internal/manifests/targetallocator/targetallocator.go +++ b/internal/manifests/targetallocator/targetallocator.go @@ -22,6 +22,7 @@ import ( "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" "github.com/open-telemetry/opentelemetry-operator/internal/manifests" "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" @@ -46,6 +47,16 @@ func Build(params Params) ([]client.Object, error) { resourceFactories = append(resourceFactories, manifests.FactoryWithoutError(ServiceMonitor)) } + if params.Config.CertManagerAvailability() == certmanager.Available && featuregate.EnableTargetAllocatorMTLS.IsEnabled() { + resourceFactories = append(resourceFactories, + manifests.FactoryWithoutError(SelfSignedIssuer), + manifests.FactoryWithoutError(CACertificate), + manifests.FactoryWithoutError(CAIssuer), + manifests.FactoryWithoutError(ServingCertificate), + manifests.FactoryWithoutError(ClientCertificate), + ) + } + for _, factory := range resourceFactories { res, err := factory(params) if err != nil { diff --git a/internal/manifests/targetallocator/volume.go b/internal/manifests/targetallocator/volume.go index 2da3a961b2..c78b736254 100644 --- a/internal/manifests/targetallocator/volume.go +++ b/internal/manifests/targetallocator/volume.go @@ -18,8 +18,10 @@ import ( corev1 "k8s.io/api/core/v1" "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) // Volumes builds the volumes for the given instance, including the config map volume. @@ -38,5 +40,16 @@ func Volumes(cfg config.Config, instance v1alpha1.TargetAllocator) []corev1.Volu }, }} + if cfg.CertManagerAvailability() == certmanager.Available && featuregate.EnableTargetAllocatorMTLS.IsEnabled() { + volumes = append(volumes, corev1.Volume{ + Name: naming.TAServerCertificate(instance.Name), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: naming.TAServerCertificateSecretName(instance.Name), + }, + }, + }) + } + return volumes } diff --git a/internal/manifests/targetallocator/volume_test.go b/internal/manifests/targetallocator/volume_test.go index 6d255e849c..898f900924 100644 --- a/internal/manifests/targetallocator/volume_test.go +++ b/internal/manifests/targetallocator/volume_test.go @@ -18,10 +18,16 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + colfg "go.opentelemetry.io/collector/featuregate" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/open-telemetry/opentelemetry-operator/apis/v1alpha1" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/config" "github.com/open-telemetry/opentelemetry-operator/internal/naming" + "github.com/open-telemetry/opentelemetry-operator/pkg/featuregate" ) func TestVolumeNewDefault(t *testing.T) { @@ -41,3 +47,58 @@ func TestVolumeNewDefault(t *testing.T) { // check that it's the ta-internal volume, with the config map assert.Equal(t, naming.TAConfigMapVolume(), volumes[0].Name) } + +func TestVolumeWithTargetAllocatorMTLS(t *testing.T) { + t.Run("CertManager available and EnableTargetAllocatorMTLS enabled", func(t *testing.T) { + ta := v1alpha1.TargetAllocator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-targetallocator", + }, + } + cfg := config.New(config.WithCertManagerAvailability(certmanager.Available)) + + flgs := featuregate.Flags(colfg.GlobalRegistry()) + err := flgs.Parse([]string{"--feature-gates=operator.targetallocator.mtls"}) + require.NoError(t, err) + + volumes := Volumes(cfg, ta) + + expectedVolume := corev1.Volume{ + Name: naming.TAServerCertificate(ta.Name), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: naming.TAServerCertificateSecretName(ta.Name), + }, + }, + } + assert.Contains(t, volumes, expectedVolume) + }) + + t.Run("CertManager not available", func(t *testing.T) { + ta := v1alpha1.TargetAllocator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-targetallocator", + }, + } + cfg := config.New(config.WithCertManagerAvailability(certmanager.NotAvailable)) + + flgs := featuregate.Flags(colfg.GlobalRegistry()) + err := flgs.Parse([]string{"--feature-gates=operator.targetallocator.mtls"}) + require.NoError(t, err) + + volumes := Volumes(cfg, ta) + assert.NotContains(t, volumes, corev1.Volume{Name: naming.TAServerCertificate(ta.Name)}) + }) + + t.Run("EnableTargetAllocatorMTLS disabled", func(t *testing.T) { + ta := v1alpha1.TargetAllocator{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-targetallocator", + }, + } + cfg := config.New(config.WithCertManagerAvailability(certmanager.Available)) + + volumes := Volumes(cfg, ta) + assert.NotContains(t, volumes, corev1.Volume{Name: naming.TAServerCertificate(ta.Name)}) + }) +} diff --git a/internal/naming/main.go b/internal/naming/main.go index f4c6dc3389..8642f618c3 100644 --- a/internal/naming/main.go +++ b/internal/naming/main.go @@ -180,3 +180,38 @@ func TargetAllocatorServiceMonitor(otelcol string) string { func OpAMPBridgeServiceAccount(opampBridge string) string { return DNSName(Truncate("%s-opamp-bridge", 63, opampBridge)) } + +// SelfSignedIssuer returns the SelfSigned Issuer name based on the instance. +func SelfSignedIssuer(otelcol string) string { + return DNSName(Truncate("%s-self-signed-issuer", 63, otelcol)) +} + +// CAIssuer returns the CA Issuer name based on the instance. +func CAIssuer(otelcol string) string { + return DNSName(Truncate("%s-ca-issuer", 63, otelcol)) +} + +// CACertificateSecret returns the Secret name based on the instance. +func CACertificate(otelcol string) string { + return DNSName(Truncate("%s-ca-cert", 63, otelcol)) +} + +// TAServerCertificate returns the Certificate name based on the instance. +func TAServerCertificate(otelcol string) string { + return DNSName(Truncate("%s-ta-server-cert", 63, otelcol)) +} + +// TAServerCertificateSecretName returns the Secret name based on the instance. +func TAServerCertificateSecretName(otelcol string) string { + return DNSName(Truncate("%s-ta-server-cert", 63, otelcol)) +} + +// TAClientCertificate returns the Certificate name based on the instance. +func TAClientCertificate(otelcol string) string { + return DNSName(Truncate("%s-ta-client-cert", 63, otelcol)) +} + +// TAClientCertificateSecretName returns the Secret name based on the instance. +func TAClientCertificateSecretName(otelcol string) string { + return DNSName(Truncate("%s-ta-client-cert", 63, otelcol)) +} diff --git a/main.go b/main.go index 1d3471898f..8d37edce7d 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ import ( "strings" "time" + cmv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" routev1 "github.com/openshift/api/route/v1" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" "github.com/spf13/pflag" @@ -50,6 +51,7 @@ import ( otelv1beta1 "github.com/open-telemetry/opentelemetry-operator/apis/v1beta1" "github.com/open-telemetry/opentelemetry-operator/controllers" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect" + "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/certmanager" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/openshift" "github.com/open-telemetry/opentelemetry-operator/internal/autodetect/prometheus" "github.com/open-telemetry/opentelemetry-operator/internal/config" @@ -349,7 +351,16 @@ func main() { } else { setupLog.Info("Openshift CRDs are not installed, skipping adding to scheme.") } + if cfg.CertManagerAvailability() == certmanager.Available { + setupLog.Info("Cert-Manager is available to the operator, adding to scheme.") + utilruntime.Must(cmv1.AddToScheme(scheme)) + if featuregate.EnableTargetAllocatorMTLS.IsEnabled() { + setupLog.Info("Securing the connection between the target allocator and the collector") + } + } else { + setupLog.Info("Cert-Manager is not available to the operator, skipping adding to scheme.") + } if cfg.AnnotationsFilter() != nil { for _, basePattern := range cfg.AnnotationsFilter() { _, compileErr := regexp.Compile(basePattern) diff --git a/pkg/constants/env.go b/pkg/constants/env.go index ac89f13e6d..8dd4f65200 100644 --- a/pkg/constants/env.go +++ b/pkg/constants/env.go @@ -52,4 +52,9 @@ const ( FlagNginx = "enable-nginx-instrumentation" FlagNodeJS = "enable-nodejs-instrumentation" FlagJava = "enable-java-instrumentation" + + TACollectorTLSDirPath = "/tls" + TACollectorCAFileName = "ca.crt" + TACollectorTLSKeyFileName = "tls.key" + TACollectorTLSCertFileName = "tls.crt" ) diff --git a/pkg/featuregate/featuregate.go b/pkg/featuregate/featuregate.go index bf83d666ce..5b452da235 100644 --- a/pkg/featuregate/featuregate.go +++ b/pkg/featuregate/featuregate.go @@ -40,6 +40,13 @@ var ( featuregate.WithRegisterDescription("enables feature to set GOMEMLIMIT and GOMAXPROCS automatically"), featuregate.WithRegisterFromVersion("v0.100.0"), ) + // EnableTargetAllocatorMTLS is the feature gate that enables mTLS between the target allocator and the collector. + EnableTargetAllocatorMTLS = featuregate.GlobalRegistry().MustRegister( + "operator.targetallocator.mtls", + featuregate.StageAlpha, + featuregate.WithRegisterDescription("enables mTLS between the target allocator and the collector"), + featuregate.WithRegisterFromVersion("v0.111.0"), + ) // EnableConfigDefaulting is the feature gate that enables the operator to default the endpoint for known components. EnableConfigDefaulting = featuregate.GlobalRegistry().MustRegister( "operator.collector.default.config", diff --git a/tests/e2e-ta-collector-mtls/certmanager-permissions/certmanager.yaml b/tests/e2e-ta-collector-mtls/certmanager-permissions/certmanager.yaml new file mode 100644 index 0000000000..1ef192378a --- /dev/null +++ b/tests/e2e-ta-collector-mtls/certmanager-permissions/certmanager.yaml @@ -0,0 +1,17 @@ +- op: add + path: /rules/- + value: + apiGroups: + - cert-manager.io + resources: + - issuers + - certificaterequests + - certificates + verbs: + - create + - get + - list + - watch + - update + - patch + - delete \ No newline at end of file diff --git a/tests/e2e-ta-collector-mtls/ta-collector-mtls/00-assert.yaml b/tests/e2e-ta-collector-mtls/ta-collector-mtls/00-assert.yaml new file mode 100644 index 0000000000..08aacf33b6 --- /dev/null +++ b/tests/e2e-ta-collector-mtls/ta-collector-mtls/00-assert.yaml @@ -0,0 +1,86 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: prometheus-cr-collector +status: + readyReplicas: 1 + replicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prometheus-cr-targetallocator +status: + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: prometheus-cr-targetallocator +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: prometheus-cr-ca-cert +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: prometheus-cr-ta-server-cert +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: prometheus-cr-ta-client-cert +--- +apiVersion: v1 +data: + collector.yaml: | + exporters: + prometheus: + endpoint: 0.0.0.0:9090 + receivers: + prometheus: + config: {} + target_allocator: + collector_id: ${POD_NAME} + endpoint: https://prometheus-cr-targetallocator:443 + interval: 30s + tls: + ca_file: /tls/ca.crt + cert_file: /tls/tls.crt + key_file: /tls/tls.key + service: + pipelines: + metrics: + exporters: + - prometheus + receivers: + - prometheus +kind: ConfigMap +metadata: + name: prometheus-cr-collector-52e1d2ae +--- +apiVersion: v1 +kind: Pod +metadata: + labels: + app.kubernetes.io/component: opentelemetry-targetallocator + app.kubernetes.io/managed-by: opentelemetry-operator +spec: + containers: + - name: ta-container + ports: + - containerPort: 8080 + name: http + - containerPort: 8443 + name: https +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: check-ta-serving-over-https +status: + succeeded: 1 \ No newline at end of file diff --git a/tests/e2e-ta-collector-mtls/ta-collector-mtls/00-install.yaml b/tests/e2e-ta-collector-mtls/ta-collector-mtls/00-install.yaml new file mode 100644 index 0000000000..5d0359b079 --- /dev/null +++ b/tests/e2e-ta-collector-mtls/ta-collector-mtls/00-install.yaml @@ -0,0 +1,187 @@ +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + name: ta +--- +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + name: collector +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: (join('-', ['ta', $namespace])) +rules: +- apiGroups: + - "" + resources: + - pods + - nodes + - services + - endpoints + - configmaps + - secrets + - namespaces + verbs: + - get + - watch + - list +- apiGroups: + - apps + resources: + - statefulsets + - services + - endpoints + verbs: + - get + - watch + - list +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - get + - watch + - list +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - watch + - list +- apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + - podmonitors + verbs: + - get + - watch + - list +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: (join('-', ['collector', $namespace])) +rules: +- apiGroups: + - "" + resources: + - pods + - nodes + - nodes/metrics + - services + - endpoints + - namespaces + verbs: + - get + - watch + - list +- apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - watch + - list +- nonResourceURLs: + - /metrics + - /metrics/cadvisor + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: (join('-', ['ta', $namespace])) +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: (join('-', ['ta', $namespace])) +subjects: +- kind: ServiceAccount + name: ta + namespace: ($namespace) +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: (join('-', ['collector', $namespace])) +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: (join('-', ['collector', $namespace])) +subjects: +- kind: ServiceAccount + name: collector + namespace: ($namespace) +--- +apiVersion: opentelemetry.io/v1alpha1 +kind: OpenTelemetryCollector +metadata: + name: prometheus-cr +spec: + config: | + receivers: + prometheus: + config: + scrape_configs: [] + + processors: + + exporters: + prometheus: + endpoint: 0.0.0.0:9090 + service: + pipelines: + metrics: + receivers: [prometheus] + exporters: [prometheus] + mode: statefulset + serviceAccount: collector + targetAllocator: + enabled: true + prometheusCR: + enabled: true + scrapeInterval: 1s + serviceAccount: ta +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: check-ta-serving-over-https +spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: check-ta + image: curlimages/curl + volumeMounts: + - name: tls-secret + mountPath: /etc/tls + readOnly: true + args: + - /bin/sh + - -c + - | + curl -s \ + --cert /etc/tls/tls.crt \ + --key /etc/tls/tls.key \ + --cacert /etc/tls/ca.crt \ + https://prometheus-cr-targetallocator:443 + volumes: + - name: tls-secret + secret: + secretName: prometheus-cr-ta-client-cert diff --git a/tests/e2e-ta-collector-mtls/ta-collector-mtls/01-assert.yaml b/tests/e2e-ta-collector-mtls/ta-collector-mtls/01-assert.yaml new file mode 100644 index 0000000000..e4f67bf8d4 --- /dev/null +++ b/tests/e2e-ta-collector-mtls/ta-collector-mtls/01-assert.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Secret +metadata: + name: metrics-app-secret +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: metrics-app + labels: + app: metrics-app +status: + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 +--- +apiVersion: v1 +kind: Service +metadata: + name: metrics-service + labels: + app: metrics-app +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: metrics-servicemonitor + labels: + app: metrics-app \ No newline at end of file diff --git a/tests/e2e-ta-collector-mtls/ta-collector-mtls/01-install.yaml b/tests/e2e-ta-collector-mtls/ta-collector-mtls/01-install.yaml new file mode 100644 index 0000000000..30bc058eae --- /dev/null +++ b/tests/e2e-ta-collector-mtls/ta-collector-mtls/01-install.yaml @@ -0,0 +1,78 @@ +apiVersion: v1 +kind: Secret +metadata: + name: metrics-app-secret +type: Opaque +stringData: + BASIC_AUTH_USERNAME: user + BASIC_AUTH_PASSWORD: t0p$ecreT +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: metrics-app + labels: + app: metrics-app +spec: + replicas: 1 + selector: + matchLabels: + app: metrics-app + template: + metadata: + labels: + app: metrics-app + spec: + containers: + - name: metrics-app + image: ghcr.io/open-telemetry/opentelemetry-operator/e2e-test-app-metrics-basic-auth:main + ports: + - containerPort: 9123 + env: + - name: BASIC_AUTH_USERNAME + valueFrom: + secretKeyRef: + name: metrics-app-secret + key: BASIC_AUTH_USERNAME + - name: BASIC_AUTH_PASSWORD + valueFrom: + secretKeyRef: + name: metrics-app-secret + key: BASIC_AUTH_PASSWORD +--- +apiVersion: v1 +kind: Service +metadata: + name: metrics-service + labels: + app: metrics-app +spec: + ports: + - name: metrics + port: 9123 + targetPort: 9123 + protocol: TCP + selector: + app: metrics-app + type: ClusterIP +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: metrics-servicemonitor + labels: + app: metrics-app +spec: + selector: + matchLabels: + app: metrics-app + endpoints: + - port: metrics + interval: 30s + basicAuth: + username: + name: metrics-app-secret + key: BASIC_AUTH_USERNAME + password: + name: metrics-app-secret + key: BASIC_AUTH_PASSWORD diff --git a/tests/e2e-ta-collector-mtls/ta-collector-mtls/02-assert.yaml b/tests/e2e-ta-collector-mtls/ta-collector-mtls/02-assert.yaml new file mode 100644 index 0000000000..b3b95bf022 --- /dev/null +++ b/tests/e2e-ta-collector-mtls/ta-collector-mtls/02-assert.yaml @@ -0,0 +1,20 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: check-metrics +status: + succeeded: 1 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: check-ta-jobs +status: + succeeded: 1 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: check-ta-scrape-configs +status: + succeeded: 1 \ No newline at end of file diff --git a/tests/e2e-ta-collector-mtls/ta-collector-mtls/02-install.yaml b/tests/e2e-ta-collector-mtls/ta-collector-mtls/02-install.yaml new file mode 100644 index 0000000000..5e45f4e150 --- /dev/null +++ b/tests/e2e-ta-collector-mtls/ta-collector-mtls/02-install.yaml @@ -0,0 +1,63 @@ +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: prometheus-cr +spec: + endpoints: + - port: monitoring + selector: + matchLabels: + app.kubernetes.io/managed-by: opentelemetry-operator +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: check-metrics +spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: check-metrics + image: curlimages/curl + args: + - /bin/sh + - -c + - | + for i in $(seq 30); do + if curl -m 1 -s http://prometheus-cr-collector:9090/metrics | grep "Client was authenticated"; then exit 0; fi + sleep 5 + done + exit 1 +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: check-ta-jobs +spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: check-metrics + image: curlimages/curl + args: + - /bin/sh + - -c + - curl -s http://prometheus-cr-targetallocator/scrape_configs | grep "prometheus-cr" +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: check-ta-scrape-configs +spec: + template: + spec: + restartPolicy: OnFailure + containers: + - name: check-metrics + image: curlimages/curl + args: + - /bin/sh + - -c + - curl -s http://prometheus-cr-targetallocator/jobs | grep "prometheus-cr" diff --git a/tests/e2e-ta-collector-mtls/ta-collector-mtls/chainsaw-test.yaml b/tests/e2e-ta-collector-mtls/ta-collector-mtls/chainsaw-test.yaml new file mode 100755 index 0000000000..6db3baf206 --- /dev/null +++ b/tests/e2e-ta-collector-mtls/ta-collector-mtls/chainsaw-test.yaml @@ -0,0 +1,34 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + creationTimestamp: null + name: targetallocator-collector-mtls +spec: + steps: + - name: step-00 + try: + - apply: + template: true + file: 00-install.yaml + - assert: + file: 00-assert.yaml + catch: + - podLogs: + selector: app.kubernetes.io/managed-by=opentelemetry-operator + - name: step-01 + try: + - apply: + file: 01-install.yaml + - assert: + file: 01-assert.yaml + - name: step-02 + try: + - apply: + template: true + file: 02-install.yaml + - assert: + file: 02-assert.yaml + catch: + - podLogs: + selector: app.kubernetes.io/managed-by=opentelemetry-operator \ No newline at end of file