From ced10043540c4cc9c39676aa8c26c0ea273bd797 Mon Sep 17 00:00:00 2001 From: desmax74 Date: Mon, 11 May 2026 18:26:19 +0200 Subject: [PATCH 1/3] Tokenize Credential Request Signed-off-by: desmax74 --- CLAUDE.md | 80 +++ Dockerfile.rhtpa-operator.rh | 2 +- bundle.Dockerfile | 2 +- .../rhtpa-operator.clusterserviceversion.yaml | 2 +- bundle/metadata/annotations.yaml | 2 +- .../rhtpa-operator.clusterserviceversion.yaml | 2 +- config/rbac/clusterrole.yaml | 8 + config/rbac/clusterrolebinding_cco.yaml | 15 + config/rbac/kustomization.yaml | 2 + devel/cco/credentialRequest.yaml | 24 + extensions/README.md | 100 ++++ .../templates/credentialrequest.yaml | 30 + .../templates/helpers/_cco.tpl | 75 +++ .../templates/helpers/_storage.tpl | 16 + .../services/importer/030-Deployment.yaml | 3 + .../services/server/030-Deployment.yaml | 3 + .../values.schema.json | 68 ++- .../values.schema.yaml | 69 ++- .../values.yaml | 14 + test/e2e/cco_helm_rendering_test.go | 534 ++++++++++++++++++ test/e2e/helpers_test.go | 7 + test/e2e/operator_health_test.go | 2 +- test/fixtures/aws_cco_cr.yaml | 54 ++ test/fixtures/aws_cco_default_cr.yaml | 54 ++ test/fixtures/aws_cco_manual_cr.yaml | 55 ++ test/fixtures/aws_cco_passthrough_cr.yaml | 54 ++ test/fixtures/gcp_cco_cr.yaml | 51 ++ test/integration/cco_rbac_test.go | 140 +++++ test/integration/helm_chart_test.go | 51 ++ 29 files changed, 1506 insertions(+), 13 deletions(-) create mode 100644 config/rbac/clusterrole.yaml create mode 100644 config/rbac/clusterrolebinding_cco.yaml create mode 100644 devel/cco/credentialRequest.yaml create mode 100644 extensions/README.md create mode 100644 helm-charts/redhat-trusted-profile-analyzer/templates/credentialrequest.yaml create mode 100644 helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_cco.tpl create mode 100644 test/e2e/cco_helm_rendering_test.go create mode 100644 test/fixtures/aws_cco_cr.yaml create mode 100644 test/fixtures/aws_cco_default_cr.yaml create mode 100644 test/fixtures/aws_cco_manual_cr.yaml create mode 100644 test/fixtures/aws_cco_passthrough_cr.yaml create mode 100644 test/fixtures/gcp_cco_cr.yaml create mode 100644 test/integration/cco_rbac_test.go diff --git a/CLAUDE.md b/CLAUDE.md index 16783b9b..79d5bb45 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -158,6 +158,86 @@ The TrustedProfileAnalyzer CR spec uses `x-kubernetes-preserve-unknown-fields: t - `metrics.enabled`: Enable metrics collection - `tracing.enabled`: Enable distributed tracing +## Cloud Credential Operator (CCO) Integration + +The operator supports OpenShift Cloud Credential Operator integration for automatic cloud credential provisioning. CCO eliminates the need to manually create and manage S3 access keys by delegating credential lifecycle to the platform. + +### Enabling CCO + +Set `cloudProvider` in the CR spec. This is the master toggle — when absent, no CCO resources are created and credentials must be supplied manually via `storage.accessKey`/`storage.secretKey`. + +```yaml +spec: + cloudProvider: aws # "aws" or "gcp" + ccoMode: mint # optional: "default", "mint", "passthrough", or "manual" + cloudCredentials: + aws: + statementEntries: + - effect: Allow + action: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"] + resource: "*" +``` + +When `cloudProvider` is set: +- A `CredentialsRequest` resource is created in the `openshift-cloud-credential-operator` namespace +- CCO provisions a Secret named `-cloud-creds` in the deployment namespace +- `storage.accessKey` and `storage.secretKey` become **optional** (auto-populated from the CCO secret) + +### Disabling CCO + +Remove or leave `cloudProvider` unset in the CR spec. When unset: +- No `CredentialsRequest` is created +- No CCO volumes or environment variables are injected into pods +- S3 credentials must be provided explicitly via `storage.accessKey` and `storage.secretKey` + +### CCO Modes + +| Mode | `ccoMode` value | Description | +|------|----------------|-------------| +| **Default** | `default` | CCO auto-determines provisioning method. | +| **Mint** | `mint` | CCO creates new IAM credentials with least-privilege permissions from `statementEntries`. | +| **Passthrough** | `passthrough` | CCO copies cluster admin credentials to the target namespace. | +| **Manual** | `manual` | Credentials pre-provisioned via `ccoctl` tool (STS/WIF). Requires `stsIAMRoleARN` for AWS. | + +### Manual Mode (STS) + +Manual mode uses short-lived token-based authentication instead of static access keys. It requires additional configuration: + +```yaml +spec: + cloudProvider: aws + ccoMode: manual + cloudCredentials: + aws: + statementEntries: + - effect: Allow + action: ["s3:*"] + resource: "*" + stsIAMRoleARN: "arn:aws:iam::123456789012:role/trustify-s3-role" +``` + +When manual mode is active, the operator automatically: +- Mounts a projected ServiceAccount token at `/var/run/secrets/openshift/serviceaccount` +- Mounts the CCO credentials secret at `/var/run/secrets/cloud` +- Sets `AWS_SHARED_CREDENTIALS_FILE`, `AWS_WEB_IDENTITY_TOKEN_FILE`, and `AWS_ROLE_ARN` environment variables +- Omits `TRUSTD_S3_ACCESS_KEY` / `TRUSTD_S3_SECRET_KEY` (the AWS SDK uses STS instead) + +### Key Files + +| File | Purpose | +|------|---------| +| `helm-charts/.../templates/credentialrequest.yaml` | CredentialsRequest template (conditional on `cloudProvider`) | +| `helm-charts/.../templates/helpers/_cco.tpl` | Helper templates for manual mode volumes/mounts/env vars | +| `helm-charts/.../templates/helpers/_storage.tpl` | S3 env vars — branches on CCO vs manual credentials | +| `config/rbac/clusterrole.yaml` | ClusterRole granting access to `credentialsrequests` API | +| `config/rbac/clusterrolebinding_cco.yaml` | Binds the CCO ClusterRole to the operator ServiceAccount | +| `test/fixtures/aws_cco_*.yaml` | Example CRs for each CCO mode | +| `test/e2e/cco_helm_rendering_test.go` | E2E tests for CCO template rendering | + +### RBAC + +The operator requires a ClusterRole with permissions on `cloudcredential.openshift.io/credentialsrequests` (create, delete, get, list, patch, update, watch). This is configured in `config/rbac/clusterrole.yaml` and bound via `config/rbac/clusterrolebinding_cco.yaml`. + ## Linting The project uses golangci-lint with configuration in `.golangci.yml`. Enabled linters include: diff --git a/Dockerfile.rhtpa-operator.rh b/Dockerfile.rhtpa-operator.rh index 5ec2c0ba..61a2aa46 100644 --- a/Dockerfile.rhtpa-operator.rh +++ b/Dockerfile.rhtpa-operator.rh @@ -51,7 +51,7 @@ LABEL features.operators.openshift.io/proxy-aware="false" LABEL features.operators.openshift.io/cnf="false" LABEL features.operators.openshift.io/csi="false" LABEL features.operators.openshift.io/tls-profiles="false" -LABEL features.operators.openshift.io/token-auth-aws="false" +LABEL features.operators.openshift.io/token-auth-aws="true" LABEL features.operators.openshift.io/token-auth-azure="false" LABEL features.operators.openshift.io/token-auth-gcp="false" diff --git a/bundle.Dockerfile b/bundle.Dockerfile index 1b2a6946..08e47138 100644 --- a/bundle.Dockerfile +++ b/bundle.Dockerfile @@ -23,7 +23,7 @@ LABEL features.operators.openshift.io/proxy-aware="false" LABEL features.operators.openshift.io/cnf="false" LABEL features.operators.openshift.io/csi="false" LABEL features.operators.openshift.io/tls-profiles="false" -LABEL features.operators.openshift.io/token-auth-aws="false" +LABEL features.operators.openshift.io/token-auth-aws="true" LABEL features.operators.openshift.io/token-auth-azure="false" LABEL features.operators.openshift.io/token-auth-gcp="false" # Core bundle labels. diff --git a/bundle/manifests/rhtpa-operator.clusterserviceversion.yaml b/bundle/manifests/rhtpa-operator.clusterserviceversion.yaml index 13d81daa..e9fb9df0 100644 --- a/bundle/manifests/rhtpa-operator.clusterserviceversion.yaml +++ b/bundle/manifests/rhtpa-operator.clusterserviceversion.yaml @@ -103,7 +103,7 @@ metadata: features.operators.openshift.io/fips-compliant: "false" features.operators.openshift.io/proxy-aware: "false" features.operators.openshift.io/tls-profiles: "false" - features.operators.openshift.io/token-auth-aws: "false" + features.operators.openshift.io/token-auth-aws: "true" features.operators.openshift.io/token-auth-azure: "false" features.operators.openshift.io/token-auth-gcp: "false" operators.openshift.io/valid-subscription: '["Red Hat Trusted Profile Analyzer"]' diff --git a/bundle/metadata/annotations.yaml b/bundle/metadata/annotations.yaml index 0e1d916e..be3b9d4e 100644 --- a/bundle/metadata/annotations.yaml +++ b/bundle/metadata/annotations.yaml @@ -22,6 +22,6 @@ annotations: features.operators.openshift.io/cnf: "false" features.operators.openshift.io/csi: "false" features.operators.openshift.io/tls-profiles: "false" - features.operators.openshift.io/token-auth-aws: "false" + features.operators.openshift.io/token-auth-aws: "true" features.operators.openshift.io/token-auth-azure: "false" features.operators.openshift.io/token-auth-gcp: "false" diff --git a/config/manifests/bases/rhtpa-operator.clusterserviceversion.yaml b/config/manifests/bases/rhtpa-operator.clusterserviceversion.yaml index c6f115c0..a1acc908 100644 --- a/config/manifests/bases/rhtpa-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/rhtpa-operator.clusterserviceversion.yaml @@ -11,7 +11,7 @@ metadata: features.operators.openshift.io/fips-compliant: "false" features.operators.openshift.io/proxy-aware: "false" features.operators.openshift.io/tls-profiles: "false" - features.operators.openshift.io/token-auth-aws: "false" + features.operators.openshift.io/token-auth-aws: "true" features.operators.openshift.io/token-auth-azure: "false" features.operators.openshift.io/token-auth-gcp: "false" operators.openshift.io/valid-subscription: '["Red Hat Trusted Profile Analyzer"]' diff --git a/config/rbac/clusterrole.yaml b/config/rbac/clusterrole.yaml new file mode 100644 index 00000000..e3b8134b --- /dev/null +++ b/config/rbac/clusterrole.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: cco-credentialsrequest-access +rules: + - apiGroups: ["cloudcredential.openshift.io"] + resources: ["credentialsrequests"] + verbs: ["create", "delete", "get", "list", "patch", "update", "watch"] diff --git a/config/rbac/clusterrolebinding_cco.yaml b/config/rbac/clusterrolebinding_cco.yaml new file mode 100644 index 00000000..82d13e0a --- /dev/null +++ b/config/rbac/clusterrolebinding_cco.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: rhtpa-operator + app.kubernetes.io/managed-by: kustomize + name: cco-credentialsrequest-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cco-credentialsrequest-access +subjects: + - kind: ServiceAccount + name: rhtpa-operator-controller-manager + namespace: placeholder diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 162b99f4..71823f14 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -15,6 +15,8 @@ resources: - rolebinding_job.yaml - role_cluster_ingress.yaml - role_cluster_ingress_binding.yaml +- clusterrole.yaml +- clusterrolebinding_cco.yaml # The following RBAC configurations are used to protect # the metrics endpoint with authn/authz. These configurations # ensure that only authorized users and service accounts diff --git a/devel/cco/credentialRequest.yaml b/devel/cco/credentialRequest.yaml new file mode 100644 index 00000000..4f349f08 --- /dev/null +++ b/devel/cco/credentialRequest.yaml @@ -0,0 +1,24 @@ +apiVersion: cloudcredential.openshift.io/v1 +kind: CredentialsRequest +metadata: + name: my-operator-credentials + namespace: openshift-cloud-credential-operator +spec: + secretRef: + name: my-cloud-creds + namespace: my-operator-namespace + providerSpec: + apiVersion: cloudcredential.openshift.io/v1 + kind: AWSProviderSpec # AWS + statementEntries: + - effect: Allow + action: + - "s3:GetObject" + - "s3:PutObject" + - "s3:DeleteObject" + - "s3:ListBucket" + - "s3:GetBucketLocation" + - "s3:ListBucketMultipartUploads" + - "s3:AbortMultipartUpload" + - "s3:ListMultipartUploadParts" + resource: "*" diff --git a/extensions/README.md b/extensions/README.md new file mode 100644 index 00000000..5b7f57c9 --- /dev/null +++ b/extensions/README.md @@ -0,0 +1,100 @@ +# Tokenize with Cloud Credentials Operator + +Cloud Credentials Operator (CCO) is installed by default on OCP. +To check the CCO status + +```console +oc get clusteroperator cloud-credential +``` +it shows something like + +```console +NAME VERSION AVAILABLE PROGRESSING DEGRADED SINCE +cloud-credential 4.x.x True False False ... +``` +Pod status +```console +oc get pods -n openshift-cloud-credential-operator +``` + +Credential requests checks +```console +oc get credentialsrequests -n openshift-cloud-credential-operator +``` + +CCO details +```console +oc describe clusteroperator cloud-credential +``` + +CCO Modality +```console +oc get cloudcredential cluster -o yaml +``` +On spec.credentialsMode will be the configured setting (Mint, +Passthrough, Manual, or empty for default). + +## How the Operator interacts with the CCO + +1. Operator declares permissions needed in a CredentialsRequest CR in the namespace openshift-cloud-credential-operator + +```console + apiVersion: cloudcredential.openshift.io/v1 + kind: CredentialsRequest + metadata: + name: my-operator-credentials + namespace: openshift-cloud-credential-operator + spec: + secretRef: + name: my-cloud-creds + namespace: my-operator-namespace + providerSpec: + apiVersion: cloudcredential.openshift.io/v1 + kind: AWSProviderSpec # esempio per AWS + statementEntries: + - effect: Allow + action: + - "s3:GetObject" + - "s3:PutObject" + resource: "*" +``` + +2. CCO processes the CR and create a Kubernetes Secret with the cloud credentails + in the namespace specified in spec.secretRef. + +3. Operator reads the Secret and uses the credentials to interact with the cloud API. + The Operator must tolerate the non-immediate availability of the Secret because it takes time to create. + +## How to integrate the CCO with the Helm Chart Operator + +1. Define Credential Request in the chart +2. +2. Configure Deployment to use the secret created by the CCO + +3. Handling the delay of the creation of the secret + first approach: init container +```console + initContainers: + - name: wait-for-creds + image: registry.redhat.io/openshift4/ose-cli + command: + - /bin/bash + - -c + - | + until oc get secret {{ .Release.Name }}-cloud-creds -n {{ + .Release.Namespace }} 2>/dev/null; do + echo "Waiting for cloud credentials..." + sleep 5 + done +``` + second approach Retry in the Operator's code, but this is available on a full Go Operator + +4. Support different cloud providers with values configurations + +5. RBAC needed to create the CredentialsRequest +6. Support Manual Mode (STS/WIF): if the cluster uses STS mode, CCO doesn’t create the secret automatically. + In STS mode the user must : + 1. Extract CredentialsRequest from chart + 2. Use ccoctl tool to generate the credentials + 3. Create the secrets manually before installing the chart. + diff --git a/helm-charts/redhat-trusted-profile-analyzer/templates/credentialrequest.yaml b/helm-charts/redhat-trusted-profile-analyzer/templates/credentialrequest.yaml new file mode 100644 index 00000000..f5508325 --- /dev/null +++ b/helm-charts/redhat-trusted-profile-analyzer/templates/credentialrequest.yaml @@ -0,0 +1,30 @@ +{{- if .Values.cloudProvider }} +apiVersion: cloudcredential.openshift.io/v1 +kind: CredentialsRequest +metadata: + name: {{ .Release.Name }}-cloud-creds + namespace: openshift-cloud-credential-operator +spec: + secretRef: + name: {{ .Release.Name }}-cloud-creds + namespace: {{ .Release.Namespace }} + {{- if eq (toString .Values.ccoMode) "manual" }} + cloudTokenPath: /var/run/secrets/openshift/serviceaccount/token + {{- end }} + {{- if eq .Values.cloudProvider "aws" }} + providerSpec: + apiVersion: cloudcredential.openshift.io/v1 + kind: AWSProviderSpec + statementEntries: + {{- toYaml .Values.cloudCredentials.aws.statementEntries | nindent 6 }} + {{- if eq (toString .Values.ccoMode) "manual" }} + stsIAMRoleARN: {{ .Values.cloudCredentials.aws.stsIAMRoleARN | default "" | quote }} + {{- end }} + {{- else if eq .Values.cloudProvider "gcp" }} + providerSpec: + apiVersion: cloudcredential.openshift.io/v1 + kind: GCPProviderSpec + predefinedRoles: + {{- toYaml .Values.cloudCredentials.gcp.permissions | nindent 6 }} + {{- end }} +{{- end }} diff --git a/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_cco.tpl b/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_cco.tpl new file mode 100644 index 00000000..73b7dbaa --- /dev/null +++ b/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_cco.tpl @@ -0,0 +1,75 @@ +{{/* +Cloud Credentials Operator (CCO) helpers for manual/STS/WIF mode. + +These templates emit volumes, volume mounts, and environment variables +needed when ccoMode is "manual". For mint/passthrough/default modes +they emit nothing — credentials are handled via _storage.tpl. + +Arguments (dict): + * root - . +*/}} + +{{/* +Returns "true" when CCO manual mode is active (cloudProvider set AND ccoMode is "manual"). +*/}} +{{- define "trustification.cco.isManualMode" -}} +{{- if and .root.Values.cloudProvider (eq (toString .root.Values.ccoMode) "manual") -}}true{{- end -}} +{{- end -}} + +{{/* +Returns the CCO secret name. +*/}} +{{- define "trustification.cco.secretName" -}} +{{ .root.Release.Name }}-cloud-creds +{{- end -}} + +{{/* +Volumes for CCO manual mode: CCO credentials secret + projected SA token. +*/}} +{{- define "trustification.cco.volumes" -}} +{{- if eq (include "trustification.cco.isManualMode" .) "true" }} +- name: cloud-credentials + secret: + secretName: {{ include "trustification.cco.secretName" . }} +- name: bound-sa-token + projected: + sources: + - serviceAccountToken: + audience: openshift + expirationSeconds: 3600 + path: token +{{- end }} +{{- end -}} + +{{/* +Volume mounts for CCO manual mode. +*/}} +{{- define "trustification.cco.volumeMounts" -}} +{{- if eq (include "trustification.cco.isManualMode" .) "true" }} +- name: cloud-credentials + mountPath: /var/run/secrets/cloud + readOnly: true +- name: bound-sa-token + mountPath: /var/run/secrets/openshift/serviceaccount + readOnly: true +{{- end }} +{{- end -}} + +{{/* +Environment variables for CCO manual mode, branched by cloud provider. +For mint/passthrough/default modes this emits nothing. +*/}} +{{- define "trustification.cco.envVars" -}} +{{- if eq (include "trustification.cco.isManualMode" .) "true" }} +{{- if eq .root.Values.cloudProvider "aws" }} +- name: AWS_SHARED_CREDENTIALS_FILE + value: /var/run/secrets/cloud/credentials +- name: AWS_WEB_IDENTITY_TOKEN_FILE + value: /var/run/secrets/openshift/serviceaccount/token +{{- with .root.Values.cloudCredentials.aws.stsIAMRoleARN }} +- name: AWS_ROLE_ARN + value: {{ . | quote }} +{{- end }} +{{- end }} +{{- end }} +{{- end -}} diff --git a/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_storage.tpl b/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_storage.tpl index e86f9753..a499b36e 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_storage.tpl +++ b/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_storage.tpl @@ -50,10 +50,26 @@ Arguments (dict): - name: TRUSTD_STORAGE_STRATEGY value: s3 +{{- if not (eq (include "trustification.cco.isManualMode" .) "true") }} - name: TRUSTD_S3_ACCESS_KEY + {{- if and .root.Values.cloudProvider (not .storage.accessKey) }} + valueFrom: + secretKeyRef: + name: {{ .root.Release.Name }}-cloud-creds + key: aws_access_key_id + {{- else }} {{- include "trustification.common.envVarValue" .storage.accessKey | nindent 2 }} + {{- end }} - name: TRUSTD_S3_SECRET_KEY + {{- if and .root.Values.cloudProvider (not .storage.secretKey) }} + valueFrom: + secretKeyRef: + name: {{ .root.Release.Name }}-cloud-creds + key: aws_secret_access_key + {{- else }} {{- include "trustification.common.envVarValue" .storage.secretKey | nindent 2 }} + {{- end }} +{{- end }} - name: TRUSTD_S3_REGION {{- include "trustification.common.envVarValue" .storage.region | nindent 2 }} - name: TRUSTD_S3_BUCKET diff --git a/helm-charts/redhat-trusted-profile-analyzer/templates/services/importer/030-Deployment.yaml b/helm-charts/redhat-trusted-profile-analyzer/templates/services/importer/030-Deployment.yaml index cc2be05f..42dbf73b 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/templates/services/importer/030-Deployment.yaml +++ b/helm-charts/redhat-trusted-profile-analyzer/templates/services/importer/030-Deployment.yaml @@ -49,6 +49,7 @@ spec: {{- include "trustification.application.infrastructure.envVars" $mod | nindent 12 }} {{- include "trustification.postgres.envVars" (dict "root" . "database" .Values.database) | nindent 12 }} {{- include "trustification.storage.envVars" $mod | nindent 12 }} + {{- include "trustification.cco.envVars" $mod | nindent 12 }} ports: {{- include "trustification.application.infrastructure.podPorts" $mod | nindent 12 }} @@ -57,6 +58,7 @@ spec: - name: workdir mountPath: /data/workdir {{- include "trustification.storage.volumeMount" $mod | nindent 12 }} + {{- include "trustification.cco.volumeMounts" $mod | nindent 12 }} {{- include "trustification.application.extraVolumeMounts" $mod | nindent 12 }} volumes: @@ -75,6 +77,7 @@ spec: requests: storage: {{ $mod.module.workingDirectory.size | quote }} {{- include "trustification.storage.volume" $mod | nindent 8 }} + {{- include "trustification.cco.volumes" $mod | nindent 8 }} {{- include "trustification.application.extraVolumes" $mod | nindent 8 }} {{ end }} diff --git a/helm-charts/redhat-trusted-profile-analyzer/templates/services/server/030-Deployment.yaml b/helm-charts/redhat-trusted-profile-analyzer/templates/services/server/030-Deployment.yaml index db9bd8fd..a0f726ff 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/templates/services/server/030-Deployment.yaml +++ b/helm-charts/redhat-trusted-profile-analyzer/templates/services/server/030-Deployment.yaml @@ -51,6 +51,7 @@ spec: {{- include "trustification.application.httpServer.envVars" $mod | nindent 12 }} {{- include "trustification.postgres.envVars" (dict "root" . "database" .Values.database) | nindent 12 }} {{- include "trustification.storage.envVars" $mod | nindent 12 }} + {{- include "trustification.cco.envVars" $mod | nindent 12 }} {{- include "trustification.oidc.swaggerUi" $mod | nindent 12 }} - name: UI_ISSUER_URL @@ -93,12 +94,14 @@ spec: {{- include "trustification.application.httpServerVolumesMounts" $mod | nindent 12 }} {{- include "trustification.authenticator.volumeMount" $mod | nindent 12 }} {{- include "trustification.storage.volumeMount" $mod | nindent 12 }} + {{- include "trustification.cco.volumeMounts" $mod | nindent 12 }} {{- include "trustification.application.extraVolumeMounts" $mod | nindent 12 }} volumes: {{- include "trustification.application.httpServerVolumes" $mod | nindent 8 }} {{- include "trustification.authenticator.volume" $mod | nindent 8 }} {{- include "trustification.storage.volume" $mod | nindent 8 }} + {{- include "trustification.cco.volumes" $mod | nindent 8 }} {{- include "trustification.application.extraVolumes" $mod | nindent 8 }} {{ end }} diff --git a/helm-charts/redhat-trusted-profile-analyzer/values.schema.json b/helm-charts/redhat-trusted-profile-analyzer/values.schema.json index efbfb1f8..9ce36989 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/values.schema.json +++ b/helm-charts/redhat-trusted-profile-analyzer/values.schema.json @@ -385,6 +385,70 @@ }, "prometheus": { "$ref": "#/definitions/Feature" + }, + "ccoMode": { + "type": "string", + "enum": [ + "default", + "mint", + "passthrough", + "manual" + ], + "description": "Cloud Credential Operator mode. Controls how cloud credentials are provisioned.\n- default: CCO auto-determines the provisioning method\n- mint: CCO creates new IAM credentials from permissions in CredentialsRequest\n- passthrough: CCO copies admin credentials to the target namespace\n- manual: Credentials are pre-provisioned via ccoctl (STS/WIF). Pods are automatically configured with projected SA token volumes and SDK env vars.\n" + }, + "cloudProvider": { + "type": "string", + "enum": [ + "aws", + "gcp" + ], + "description": "Set the cloud provider to enable Cloud Credentials Operator (CCO) integration.\nWhen set, a CredentialsRequest will be created and the resulting secret will be\nused for S3 storage credentials (accessKey and secretKey become optional).\n" + }, + "cloudCredentials": { + "type": "object", + "description": "Cloud-specific credential configuration for the Cloud Credentials Operator.\n", + "properties": { + "aws": { + "type": "object", + "properties": { + "statementEntries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "effect": { + "type": "string" + }, + "action": { + "type": "array", + "items": { + "type": "string" + } + }, + "resource": { + "type": "string" + } + } + } + }, + "stsIAMRoleARN": { + "type": "string", + "description": "The ARN of the IAM role for STS-based authentication (manual mode only).\n" + } + } + }, + "gcp": { + "type": "object", + "properties": { + "permissions": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } } }, "allOf": [ @@ -971,12 +1035,10 @@ } }, "S3StorageConfig": { - "description": "Configure an S3 compatible object storage.\n", + "description": "Configure an S3 compatible object storage.\nWhen cloudProvider is set, accessKey and secretKey are automatically populated\nfrom the CCO-provisioned secret and do not need to be specified.\n", "type": "object", "required": [ "type", - "accessKey", - "secretKey", "bucket", "region" ], diff --git a/helm-charts/redhat-trusted-profile-analyzer/values.schema.yaml b/helm-charts/redhat-trusted-profile-analyzer/values.schema.yaml index 5c4f74ba..85782964 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/values.schema.yaml +++ b/helm-charts/redhat-trusted-profile-analyzer/values.schema.yaml @@ -64,6 +64,65 @@ properties: description: | Control the usage of the OpenShift service CA. + ccoMode: + type: string + enum: + - default + - mint + - passthrough + - manual + description: | + Cloud Credential Operator mode. Controls how cloud credentials are provisioned. + - default: CCO auto-determines the provisioning method + - mint: CCO creates new IAM credentials from permissions in CredentialsRequest + - passthrough: CCO copies admin credentials to the target namespace + - manual: Credentials are pre-provisioned via ccoctl (STS/WIF). Pods are + automatically configured with projected SA token volumes and SDK env vars. + + cloudProvider: + type: string + enum: + - aws + - gcp + description: | + Set the cloud provider to enable Cloud Credentials Operator (CCO) integration. + When set, a CredentialsRequest will be created and the resulting secret will be + used for S3 storage credentials (accessKey and secretKey become optional). + + cloudCredentials: + type: object + description: | + Cloud-specific credential configuration for the Cloud Credentials Operator. + properties: + aws: + type: object + properties: + statementEntries: + type: array + items: + type: object + properties: + effect: + type: string + enum: [Allow, Deny] + action: + type: array + items: + type: string + resource: + type: string + stsIAMRoleARN: + type: string + description: | + The ARN of the IAM role for STS-based authentication (manual mode only). + gcp: + type: object + properties: + permissions: + type: array + items: + type: string + oidc: $ref: "#/definitions/Oidc" @@ -742,11 +801,11 @@ definitions: S3StorageConfig: description: | Configure an S3 compatible object storage. + When cloudProvider is set, accessKey and secretKey are automatically populated + from the CCO-provisioned secret and do not need to be specified. type: object required: - type - - accessKey - - secretKey - bucket - region additionalProperties: false @@ -757,11 +816,13 @@ definitions: - s3 accessKey: description: | - The access key/username to the storage resource + The access key/username to the storage resource. + Optional when cloudProvider is set (auto-populated from CCO secret). $ref: "#/definitions/ValueOrRef" secretKey: description: | - The secret key/password to the storage resource + The secret key/password to the storage resource. + Optional when cloudProvider is set (auto-populated from CCO secret). $ref: "#/definitions/ValueOrRef" bucket: type: string diff --git a/helm-charts/redhat-trusted-profile-analyzer/values.yaml b/helm-charts/redhat-trusted-profile-analyzer/values.yaml index 169c8710..ee2bd15d 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/values.yaml +++ b/helm-charts/redhat-trusted-profile-analyzer/values.yaml @@ -17,6 +17,20 @@ storage: {} database: {} +# ccoMode: "" # default | mint | passthrough | manual + +# cloudProvider: # aws | gcp — set to enable CCO-managed credentials for S3 storage +cloudCredentials: {} + #aws: + # statementEntries: + # - effect: Allow + # action: [ "s3:*" ] + # resource: "*" + # stsIAMRoleARN: "" # Required for manual mode (STS) + #gcp: + # permissions: + # - storage.objects.get + openshift: useServiceCa: true diff --git a/test/e2e/cco_helm_rendering_test.go b/test/e2e/cco_helm_rendering_test.go new file mode 100644 index 00000000..7cfe1dc5 --- /dev/null +++ b/test/e2e/cco_helm_rendering_test.go @@ -0,0 +1,534 @@ +/* +Copyright 2026. + +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 e2e + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +const ( + fieldCloudProvider = "cloudProvider" + fieldCloudCredentials = "cloudCredentials" + fieldCcoMode = "ccoMode" + kindCredentialsReq = "CredentialsRequest" + ccoSecretName = "test-release-cloud-creds" + ccoNamespace = "openshift-cloud-credential-operator" + testBucket = "trustify-storage" + testRegionUSEast1 = "us-east-1" + testSTSRoleARN = "arn:aws:iam::123456789012:role/trustify-s3-role" + cloudProviderAWS = "aws" + cloudProviderGCP = "gcp" +) + +func testDatabaseValues() map[string]interface{} { + return map[string]interface{}{ + "host": "postgres.test.svc", + fieldName: "testdb", + "username": "testuser", + "password": "testpass", + } +} + +func awsValues() map[string]interface{} { + return map[string]interface{}{ + fieldAppDomain: testAppDomain, + fieldCloudProvider: cloudProviderAWS, + fieldCloudCredentials: map[string]interface{}{ + cloudProviderAWS: map[string]interface{}{ + "statementEntries": []interface{}{ + map[string]interface{}{ + "effect": "Allow", + "action": []interface{}{"s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"}, + "resource": "*", + }, + }, + }, + }, + fieldStorage: map[string]interface{}{ + fieldType: "s3", + fieldBucket: testBucket, + fieldRegion: testRegionUSEast1, + }, + fieldDatabase: testDatabaseValues(), + fieldMetrics: map[string]interface{}{fieldEnabled: false}, + fieldTracing: map[string]interface{}{fieldEnabled: false}, + } +} + +func gcpValues() map[string]interface{} { + return map[string]interface{}{ + fieldAppDomain: testAppDomain, + fieldCloudProvider: cloudProviderGCP, + fieldCloudCredentials: map[string]interface{}{ + "gcp": map[string]interface{}{ + "permissions": []interface{}{ + "storage.objects.get", + "storage.objects.create", + "storage.objects.delete", + "storage.buckets.get", + }, + }, + }, + fieldStorage: map[string]interface{}{ + fieldType: "s3", + fieldBucket: testBucket, + fieldRegion: "us-central1", + }, + fieldDatabase: testDatabaseValues(), + fieldMetrics: map[string]interface{}{fieldEnabled: false}, + fieldTracing: map[string]interface{}{fieldEnabled: false}, + } +} + +func awsManualValues() map[string]interface{} { + return map[string]interface{}{ + fieldAppDomain: testAppDomain, + fieldCloudProvider: cloudProviderAWS, + fieldCcoMode: "manual", + fieldCloudCredentials: map[string]interface{}{ + cloudProviderAWS: map[string]interface{}{ + "statementEntries": []interface{}{ + map[string]interface{}{ + "effect": "Allow", + "action": []interface{}{"s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket"}, + "resource": "*", + }, + }, + "stsIAMRoleARN": testSTSRoleARN, + }, + }, + fieldStorage: map[string]interface{}{ + fieldType: "s3", + fieldBucket: testBucket, + fieldRegion: testRegionUSEast1, + }, + fieldDatabase: testDatabaseValues(), + fieldMetrics: map[string]interface{}{fieldEnabled: false}, + fieldTracing: map[string]interface{}{fieldEnabled: false}, + } +} + +func awsPassthroughValues() map[string]interface{} { + v := awsValues() + v[fieldCcoMode] = "passthrough" + return v +} + +func awsDefaultValues() map[string]interface{} { + v := awsValues() + v[fieldCcoMode] = "default" + return v +} + +// parseYAMLDoc parses a YAML document via JSON round-trip so that numeric +// types are float64 (compatible with k8s unstructured helpers). +func parseYAMLDoc(doc string) (map[string]interface{}, error) { + var intermediate interface{} + if err := yaml.Unmarshal([]byte(doc), &intermediate); err != nil { + return nil, err + } + jsonBytes, err := json.Marshal(intermediate) + if err != nil { + return nil, err + } + var result map[string]interface{} + if err := json.Unmarshal(jsonBytes, &result); err != nil { + return nil, err + } + return result, nil +} + +func findCredentialsRequest(t *testing.T, docs []string) map[string]interface{} { + t.Helper() + for _, doc := range docs { + obj, err := parseYAMLDoc(doc) + if err != nil { + continue + } + if obj["kind"] == kindCredentialsReq { + return obj + } + } + return nil +} + +func nestedString(obj map[string]interface{}, keys ...string) string { + current := obj + for i, key := range keys { + if i == len(keys)-1 { + v, _ := current[key].(string) + return v + } + next, ok := current[key].(map[string]interface{}) + if !ok { + return "" + } + current = next + } + return "" +} + +func TestHelmRenderAWSCredentialsRequest(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsValues()) + docs := splitYAMLDocs(rendered) + + cr := findCredentialsRequest(t, docs) + require.NotNil(t, cr, "CredentialsRequest should be rendered for AWS") + + metadata, _ := cr[fieldMetadata].(map[string]interface{}) + require.NotNil(t, metadata, "metadata should exist") + assert.Equal(t, ccoSecretName, metadata[fieldName], "CredentialsRequest name should match release") + assert.Equal(t, ccoNamespace, metadata[fieldNamespace], "CredentialsRequest should be in CCO namespace") + + spec, _ := cr[fieldSpec].(map[string]interface{}) + require.NotNil(t, spec, "spec should exist") + + assert.Equal(t, ccoSecretName, nestedString(spec, "secretRef", fieldName), "secretRef.name should match") + + providerSpec, _ := spec["providerSpec"].(map[string]interface{}) + require.NotNil(t, providerSpec, "providerSpec should exist") + assert.Equal(t, "AWSProviderSpec", providerSpec["kind"], "providerSpec should be AWSProviderSpec") + assert.Equal(t, "cloudcredential.openshift.io/v1", + providerSpec[fieldAPIVersion], "providerSpec apiVersion should match") + + entries, ok := providerSpec["statementEntries"].([]interface{}) + assert.True(t, ok, "statementEntries should be a list") + assert.NotEmpty(t, entries, "statementEntries should not be empty") + + assert.NotContains(t, rendered, "GCPProviderSpec", "AWS config should not contain GCP provider") + assert.NotContains(t, rendered, "predefinedRoles", "AWS config should not contain predefinedRoles") +} + +func TestHelmRenderGCPCredentialsRequest(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, gcpValues()) + docs := splitYAMLDocs(rendered) + + cr := findCredentialsRequest(t, docs) + require.NotNil(t, cr, "CredentialsRequest should be rendered for GCP") + + metadata, _ := cr[fieldMetadata].(map[string]interface{}) + require.NotNil(t, metadata, "metadata should exist") + assert.Equal(t, ccoSecretName, metadata[fieldName], "CredentialsRequest name should match release") + assert.Equal(t, ccoNamespace, metadata[fieldNamespace], "CredentialsRequest should be in CCO namespace") + + spec, _ := cr[fieldSpec].(map[string]interface{}) + require.NotNil(t, spec, "spec should exist") + + providerSpec, _ := spec["providerSpec"].(map[string]interface{}) + require.NotNil(t, providerSpec, "providerSpec should exist") + assert.Equal(t, "GCPProviderSpec", providerSpec["kind"], "providerSpec should be GCPProviderSpec") + + roles, ok := providerSpec["predefinedRoles"].([]interface{}) + assert.True(t, ok, "predefinedRoles should be a list") + assert.NotEmpty(t, roles, "predefinedRoles should not be empty") + + assert.NotContains(t, rendered, "AWSProviderSpec", "GCP config should not contain AWS provider") + assert.NotContains(t, rendered, "statementEntries", "GCP config should not contain statementEntries") +} + +func TestHelmRenderNoCredentialsRequestWithoutCloudProvider(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + values := map[string]interface{}{ + fieldAppDomain: testAppDomain, + fieldStorage: map[string]interface{}{ + fieldType: "s3", + fieldBucket: "test-bucket", + fieldRegion: testRegionUSEast1, + "accessKey": "test-key", + "secretKey": "test-secret", + }, + fieldDatabase: testDatabaseValues(), + fieldMetrics: map[string]interface{}{fieldEnabled: false}, + fieldTracing: map[string]interface{}{fieldEnabled: false}, + } + + rendered := renderHelmChart(t, chartPath, values) + assert.NotEmpty(t, rendered, "chart should render without cloudProvider") + + docs := splitYAMLDocs(rendered) + cr := findCredentialsRequest(t, docs) + assert.Nil(t, cr, "CredentialsRequest should NOT be rendered without cloudProvider") +} + +func TestHelmRenderStorageEnvVarsWithCCO(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsValues()) + + assert.Contains(t, rendered, ccoSecretName, + "rendered output should reference CCO secret") + assert.Contains(t, rendered, "aws_access_key_id", + "rendered output should reference aws_access_key_id key") + assert.Contains(t, rendered, "aws_secret_access_key", + "rendered output should reference aws_secret_access_key key") + + docs := splitYAMLDocs(rendered) + for _, doc := range docs { + obj, err := parseYAMLDoc(doc) + if err != nil { + continue + } + if obj["kind"] != "Deployment" { + continue + } + metadata, _ := obj[fieldMetadata].(map[string]interface{}) + if metadata == nil || !strings.Contains(metadata[fieldName].(string), fieldServer) { + continue + } + + spec, _ := obj[fieldSpec].(map[string]interface{}) + template, _ := spec["template"].(map[string]interface{}) + templateSpec, _ := template[fieldSpec].(map[string]interface{}) + containers, _ := templateSpec["containers"].([]interface{}) + require.NotEmpty(t, containers, "server deployment should have containers") + + container, _ := containers[0].(map[string]interface{}) + envList, _ := container["env"].([]interface{}) + require.NotEmpty(t, envList, "container should have env vars") + + accessKeyFound := false + secretKeyFound := false + for _, e := range envList { + env, ok := e.(map[string]interface{}) + if !ok { + continue + } + envName, _ := env[fieldName].(string) + if envName == "TRUSTD_S3_ACCESS_KEY" { + refName := nestedString(env, "valueFrom", "secretKeyRef", fieldName) + refKey := nestedString(env, "valueFrom", "secretKeyRef", "key") + assert.Equal(t, ccoSecretName, refName, "ACCESS_KEY should reference CCO secret") + assert.Equal(t, "aws_access_key_id", refKey, "ACCESS_KEY should use correct key") + accessKeyFound = true + } + if envName == "TRUSTD_S3_SECRET_KEY" { + refName := nestedString(env, "valueFrom", "secretKeyRef", fieldName) + refKey := nestedString(env, "valueFrom", "secretKeyRef", "key") + assert.Equal(t, ccoSecretName, refName, "SECRET_KEY should reference CCO secret") + assert.Equal(t, "aws_secret_access_key", refKey, "SECRET_KEY should use correct key") + secretKeyFound = true + } + } + assert.True(t, accessKeyFound, "TRUSTD_S3_ACCESS_KEY env var should be present") + assert.True(t, secretKeyFound, "TRUSTD_S3_SECRET_KEY env var should be present") + return + } + t.Fatal("server deployment not found in rendered output") +} + +func TestHelmRenderStorageEnvVarsWithoutCCO(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + values := map[string]interface{}{ + fieldAppDomain: testAppDomain, + fieldStorage: map[string]interface{}{ + fieldType: "s3", + fieldBucket: testBucket, + fieldRegion: testRegionUSEast1, + "accessKey": "explicit-access-key", + "secretKey": "explicit-secret-key", + }, + fieldDatabase: testDatabaseValues(), + fieldMetrics: map[string]interface{}{fieldEnabled: false}, + fieldTracing: map[string]interface{}{fieldEnabled: false}, + } + + rendered := renderHelmChart(t, chartPath, values) + + assert.NotContains(t, rendered, "cloud-creds", + "rendered output should NOT reference CCO secret when cloudProvider is not set") + assert.Contains(t, rendered, "explicit-access-key", + "rendered output should contain explicit access key") + assert.Contains(t, rendered, "explicit-secret-key", + "rendered output should contain explicit secret key") +} + +func TestHelmRenderAWSManualCredentialsRequest(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsManualValues()) + docs := splitYAMLDocs(rendered) + + cr := findCredentialsRequest(t, docs) + require.NotNil(t, cr, "CredentialsRequest should be rendered for AWS manual mode") + + spec, _ := cr[fieldSpec].(map[string]interface{}) + require.NotNil(t, spec, "spec should exist") + + assert.Equal(t, "/var/run/secrets/openshift/serviceaccount/token", + spec["cloudTokenPath"], "cloudTokenPath should be set for manual mode") + + providerSpec, _ := spec["providerSpec"].(map[string]interface{}) + require.NotNil(t, providerSpec, "providerSpec should exist") + assert.Equal(t, "AWSProviderSpec", providerSpec["kind"]) + assert.Equal(t, testSTSRoleARN, providerSpec["stsIAMRoleARN"], + "stsIAMRoleARN should be set for manual mode") +} + +func TestHelmRenderAWSManualDeploymentVolumes(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsManualValues()) + + assert.Contains(t, rendered, "cloud-credentials", + "manual mode should include cloud-credentials volume") + assert.Contains(t, rendered, "bound-sa-token", + "manual mode should include bound-sa-token volume") + assert.Contains(t, rendered, "/var/run/secrets/cloud", + "manual mode should mount cloud credentials") + assert.Contains(t, rendered, "/var/run/secrets/openshift/serviceaccount", + "manual mode should mount projected SA token") +} + +func TestHelmRenderAWSManualEnvVars(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsManualValues()) + + assert.Contains(t, rendered, "AWS_SHARED_CREDENTIALS_FILE", + "manual mode should set AWS_SHARED_CREDENTIALS_FILE") + assert.Contains(t, rendered, "AWS_WEB_IDENTITY_TOKEN_FILE", + "manual mode should set AWS_WEB_IDENTITY_TOKEN_FILE") + assert.Contains(t, rendered, "AWS_ROLE_ARN", + "manual mode should set AWS_ROLE_ARN") + assert.Contains(t, rendered, testSTSRoleARN, + "AWS_ROLE_ARN should contain the STS role ARN value") +} + +func TestHelmRenderManualModeNoS3Keys(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsManualValues()) + + assert.NotContains(t, rendered, "TRUSTD_S3_ACCESS_KEY", + "manual mode should NOT set TRUSTD_S3_ACCESS_KEY") + assert.NotContains(t, rendered, "TRUSTD_S3_SECRET_KEY", + "manual mode should NOT set TRUSTD_S3_SECRET_KEY") + assert.NotContains(t, rendered, "aws_access_key_id", + "manual mode should NOT reference aws_access_key_id secret key") +} + +func TestHelmRenderMintModeHasS3Keys(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsValues()) + + assert.Contains(t, rendered, "TRUSTD_S3_ACCESS_KEY", + "mint mode should set TRUSTD_S3_ACCESS_KEY") + assert.Contains(t, rendered, "TRUSTD_S3_SECRET_KEY", + "mint mode should set TRUSTD_S3_SECRET_KEY") + assert.Contains(t, rendered, "aws_access_key_id", + "mint mode should reference aws_access_key_id from CCO secret") + + assert.NotContains(t, rendered, "AWS_WEB_IDENTITY_TOKEN_FILE", + "mint mode should NOT set AWS_WEB_IDENTITY_TOKEN_FILE") + assert.NotContains(t, rendered, "bound-sa-token", + "mint mode should NOT include bound-sa-token volume") +} + +func TestHelmRenderAWSPassthroughCredentialsRequest(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsPassthroughValues()) + docs := splitYAMLDocs(rendered) + + cr := findCredentialsRequest(t, docs) + require.NotNil(t, cr, "CredentialsRequest should be rendered for passthrough mode") + + spec, _ := cr[fieldSpec].(map[string]interface{}) + require.NotNil(t, spec, "spec should exist") + + assert.Nil(t, spec["cloudTokenPath"], + "passthrough mode should NOT set cloudTokenPath") + + providerSpec, _ := spec["providerSpec"].(map[string]interface{}) + require.NotNil(t, providerSpec, "providerSpec should exist") + assert.Equal(t, "AWSProviderSpec", providerSpec["kind"]) + assert.Empty(t, providerSpec["stsIAMRoleARN"], + "passthrough mode should NOT set stsIAMRoleARN") + + assert.Contains(t, rendered, "TRUSTD_S3_ACCESS_KEY", + "passthrough mode should set TRUSTD_S3_ACCESS_KEY from CCO secret") + assert.Contains(t, rendered, "aws_access_key_id", + "passthrough mode should reference aws_access_key_id from CCO secret") +} + +func TestHelmRenderAWSDefaultCredentialsRequest(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsDefaultValues()) + docs := splitYAMLDocs(rendered) + + cr := findCredentialsRequest(t, docs) + require.NotNil(t, cr, "CredentialsRequest should be rendered for default mode") + + spec, _ := cr[fieldSpec].(map[string]interface{}) + require.NotNil(t, spec, "spec should exist") + + assert.Nil(t, spec["cloudTokenPath"], + "default mode should NOT set cloudTokenPath") + + assert.Contains(t, rendered, "TRUSTD_S3_ACCESS_KEY", + "default mode should set TRUSTD_S3_ACCESS_KEY from CCO secret") +} diff --git a/test/e2e/helpers_test.go b/test/e2e/helpers_test.go index 41006237..945cd9c9 100644 --- a/test/e2e/helpers_test.go +++ b/test/e2e/helpers_test.go @@ -61,6 +61,13 @@ const ( fieldImporter = "importer" fieldIngress = "ingress" fieldHelm = "helm" + fieldStorage = "storage" + fieldDatabase = "database" + fieldMetrics = "metrics" + fieldTracing = "tracing" + fieldType = "type" + fieldBucket = "bucket" + fieldRegion = "region" // Common test skip messages. skipE2ETest = "skipping e2e test in short mode" diff --git a/test/e2e/operator_health_test.go b/test/e2e/operator_health_test.go index 57d5146e..f861e040 100644 --- a/test/e2e/operator_health_test.go +++ b/test/e2e/operator_health_test.go @@ -275,7 +275,7 @@ func TestOperatorMetricsEndpoint(t *testing.T) { assert.NotEmpty(t, service.Spec.Ports, "metrics service should have ports") for _, port := range service.Spec.Ports { - if port.Name == "https" || port.Name == "metrics" { + if port.Name == "https" || port.Name == fieldMetrics { t.Logf("Metrics port: %s -> %d", port.Name, port.Port) } } diff --git a/test/fixtures/aws_cco_cr.yaml b/test/fixtures/aws_cco_cr.yaml new file mode 100644 index 00000000..b64d79cd --- /dev/null +++ b/test/fixtures/aws_cco_cr.yaml @@ -0,0 +1,54 @@ +apiVersion: rhtpa.io/v1 +kind: TrustedProfileAnalyzer +metadata: + name: aws-cco-instance + namespace: test-namespace +spec: + appDomain: test.example.com + replicas: 1 + cloudProvider: aws + ccoMode: mint + cloudCredentials: + aws: + statementEntries: + - effect: Allow + action: + - "s3:GetObject" + - "s3:PutObject" + - "s3:DeleteObject" + - "s3:ListBucket" + resource: "*" + storage: + type: s3 + bucket: trustify-storage + region: us-east-1 + modules: + server: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + importer: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + oidc: + clients: + frontend: + clientId: test-frontend + cli: + clientSecret: test-secret + database: + host: postgres.test.svc + name: testdb + username: testuser + password: testpass + metrics: + enabled: false + tracing: + enabled: false diff --git a/test/fixtures/aws_cco_default_cr.yaml b/test/fixtures/aws_cco_default_cr.yaml new file mode 100644 index 00000000..a809aa04 --- /dev/null +++ b/test/fixtures/aws_cco_default_cr.yaml @@ -0,0 +1,54 @@ +apiVersion: rhtpa.io/v1 +kind: TrustedProfileAnalyzer +metadata: + name: aws-cco-default-instance + namespace: test-namespace +spec: + appDomain: test.example.com + replicas: 1 + cloudProvider: aws + ccoMode: default + cloudCredentials: + aws: + statementEntries: + - effect: Allow + action: + - "s3:GetObject" + - "s3:PutObject" + - "s3:DeleteObject" + - "s3:ListBucket" + resource: "*" + storage: + type: s3 + bucket: trustify-storage + region: us-east-1 + modules: + server: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + importer: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + oidc: + clients: + frontend: + clientId: test-frontend + cli: + clientSecret: test-secret + database: + host: postgres.test.svc + name: testdb + username: testuser + password: testpass + metrics: + enabled: false + tracing: + enabled: false diff --git a/test/fixtures/aws_cco_manual_cr.yaml b/test/fixtures/aws_cco_manual_cr.yaml new file mode 100644 index 00000000..6d2fbbd8 --- /dev/null +++ b/test/fixtures/aws_cco_manual_cr.yaml @@ -0,0 +1,55 @@ +apiVersion: rhtpa.io/v1 +kind: TrustedProfileAnalyzer +metadata: + name: aws-cco-manual-instance + namespace: test-namespace +spec: + appDomain: test.example.com + replicas: 1 + cloudProvider: aws + ccoMode: manual + cloudCredentials: + aws: + statementEntries: + - effect: Allow + action: + - "s3:GetObject" + - "s3:PutObject" + - "s3:DeleteObject" + - "s3:ListBucket" + resource: "*" + stsIAMRoleARN: "arn:aws:iam::123456789012:role/trustify-s3-role" + storage: + type: s3 + bucket: trustify-storage + region: us-east-1 + modules: + server: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + importer: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + oidc: + clients: + frontend: + clientId: test-frontend + cli: + clientSecret: test-secret + database: + host: postgres.test.svc + name: testdb + username: testuser + password: testpass + metrics: + enabled: false + tracing: + enabled: false diff --git a/test/fixtures/aws_cco_passthrough_cr.yaml b/test/fixtures/aws_cco_passthrough_cr.yaml new file mode 100644 index 00000000..6ee1b428 --- /dev/null +++ b/test/fixtures/aws_cco_passthrough_cr.yaml @@ -0,0 +1,54 @@ +apiVersion: rhtpa.io/v1 +kind: TrustedProfileAnalyzer +metadata: + name: aws-cco-passthrough-instance + namespace: test-namespace +spec: + appDomain: test.example.com + replicas: 1 + cloudProvider: aws + ccoMode: passthrough + cloudCredentials: + aws: + statementEntries: + - effect: Allow + action: + - "s3:GetObject" + - "s3:PutObject" + - "s3:DeleteObject" + - "s3:ListBucket" + resource: "*" + storage: + type: s3 + bucket: trustify-storage + region: us-east-1 + modules: + server: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + importer: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + oidc: + clients: + frontend: + clientId: test-frontend + cli: + clientSecret: test-secret + database: + host: postgres.test.svc + name: testdb + username: testuser + password: testpass + metrics: + enabled: false + tracing: + enabled: false diff --git a/test/fixtures/gcp_cco_cr.yaml b/test/fixtures/gcp_cco_cr.yaml new file mode 100644 index 00000000..089ffd59 --- /dev/null +++ b/test/fixtures/gcp_cco_cr.yaml @@ -0,0 +1,51 @@ +apiVersion: rhtpa.io/v1 +kind: TrustedProfileAnalyzer +metadata: + name: gcp-cco-instance + namespace: test-namespace +spec: + appDomain: test.example.com + replicas: 1 + cloudProvider: gcp + ccoMode: mint + cloudCredentials: + gcp: + permissions: + - storage.objects.get + - storage.objects.create + - storage.objects.delete + - storage.buckets.get + storage: + type: s3 + bucket: trustify-storage + region: us-central1 + modules: + server: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + importer: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + oidc: + clients: + frontend: + clientId: test-frontend + cli: + clientSecret: test-secret + database: + host: postgres.test.svc + name: testdb + username: testuser + password: testpass + metrics: + enabled: false + tracing: + enabled: false diff --git a/test/integration/cco_rbac_test.go b/test/integration/cco_rbac_test.go new file mode 100644 index 00000000..a713d22b --- /dev/null +++ b/test/integration/cco_rbac_test.go @@ -0,0 +1,140 @@ +/* +Copyright 2026. + +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 integration + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "gopkg.in/yaml.v3" +) + +type ClusterRoleBinding struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata struct { + Name string `yaml:"name"` + Labels map[string]string `yaml:"labels"` + } `yaml:"metadata"` + RoleRef struct { + APIGroup string `yaml:"apiGroup"` + Kind string `yaml:"kind"` + Name string `yaml:"name"` + } `yaml:"roleRef"` + Subjects []struct { + Kind string `yaml:"kind"` + Name string `yaml:"name"` + Namespace string `yaml:"namespace"` + } `yaml:"subjects"` +} + +func TestCCOClusterRoleExists(t *testing.T) { + rolePath := filepath.Join("config", "rbac", "clusterrole.yaml") + + data, err := os.ReadFile(rolePath) + require.NoError(t, err, "CCO ClusterRole file should exist and be readable") + + var role Role + err = yaml.Unmarshal(data, &role) + require.NoError(t, err, "CCO ClusterRole should be valid YAML") + + assert.Equal(t, "rbac.authorization.k8s.io/v1", role.APIVersion, "should use correct API version") + assert.Equal(t, "ClusterRole", role.Kind, "should be a ClusterRole") + assert.Equal(t, "cco-credentialsrequest-access", role.Metadata.Name, "name should match") +} + +func TestCCOClusterRolePermissions(t *testing.T) { + rolePath := filepath.Join("config", "rbac", "clusterrole.yaml") + + data, err := os.ReadFile(rolePath) + require.NoError(t, err, "CCO ClusterRole should be readable") + + var role Role + err = yaml.Unmarshal(data, &role) + require.NoError(t, err, "CCO ClusterRole should be valid YAML") + + foundCCORule := false + for _, rule := range role.Rules { + if contains(rule.APIGroups, "cloudcredential.openshift.io") && + contains(rule.Resources, "credentialsrequests") { + foundCCORule = true + requiredVerbs := []string{"create", "get", "list", "watch", "update", "delete"} + for _, verb := range requiredVerbs { + assert.Contains(t, rule.Verbs, verb, + "CCO ClusterRole should have '%s' verb on credentialsrequests", verb) + } + } + } + + assert.True(t, foundCCORule, + "CCO ClusterRole should have a rule for cloudcredential.openshift.io/credentialsrequests") +} + +func TestCCOClusterRoleBindingExists(t *testing.T) { + bindingPath := filepath.Join("config", "rbac", "clusterrolebinding_cco.yaml") + + data, err := os.ReadFile(bindingPath) + require.NoError(t, err, "CCO ClusterRoleBinding file should exist and be readable") + + var binding ClusterRoleBinding + err = yaml.Unmarshal(data, &binding) + require.NoError(t, err, "CCO ClusterRoleBinding should be valid YAML") + + assert.Equal(t, "rbac.authorization.k8s.io/v1", binding.APIVersion, "should use correct API version") + assert.Equal(t, "ClusterRoleBinding", binding.Kind, "should be a ClusterRoleBinding") + assert.Equal(t, "cco-credentialsrequest-rolebinding", binding.Metadata.Name, "name should match") + + assert.Equal(t, "rbac.authorization.k8s.io", binding.RoleRef.APIGroup, "roleRef apiGroup should match") + assert.Equal(t, "ClusterRole", binding.RoleRef.Kind, "roleRef kind should be ClusterRole") + assert.Equal(t, "cco-credentialsrequest-access", binding.RoleRef.Name, "roleRef name should match the CCO ClusterRole") +} + +func TestCCOClusterRoleBindingSubjects(t *testing.T) { + bindingPath := filepath.Join("config", "rbac", "clusterrolebinding_cco.yaml") + + data, err := os.ReadFile(bindingPath) + require.NoError(t, err, "CCO ClusterRoleBinding should be readable") + + var binding ClusterRoleBinding + err = yaml.Unmarshal(data, &binding) + require.NoError(t, err, "CCO ClusterRoleBinding should be valid YAML") + + require.NotEmpty(t, binding.Subjects, "ClusterRoleBinding should have at least one subject") + + assert.Equal(t, "ServiceAccount", binding.Subjects[0].Kind, + "subject should be a ServiceAccount") + assert.Equal(t, "rhtpa-operator-controller-manager", binding.Subjects[0].Name, + "subject should reference the operator's service account") +} + +func TestCCOResourcesInKustomization(t *testing.T) { + kustomizationPath := filepath.Join("config", "rbac", "kustomization.yaml") + + data, err := os.ReadFile(kustomizationPath) + require.NoError(t, err, "kustomization.yaml should be readable") + + content := string(data) + assert.True(t, strings.Contains(content, "clusterrole.yaml"), + "kustomization.yaml should include clusterrole.yaml") + assert.True(t, strings.Contains(content, "clusterrolebinding_cco.yaml"), + "kustomization.yaml should include clusterrolebinding_cco.yaml") +} diff --git a/test/integration/helm_chart_test.go b/test/integration/helm_chart_test.go index e55d316d..5117bda6 100644 --- a/test/integration/helm_chart_test.go +++ b/test/integration/helm_chart_test.go @@ -178,3 +178,54 @@ func TestHelmIgnoreExists(t *testing.T) { _, err := os.Stat(helmignorePath) assert.NoError(t, err, ".helmignore should exist") } + +func TestValuesSchemaContainsCCOFields(t *testing.T) { + schemaPath := filepath.Join("helm-charts", "redhat-trusted-profile-analyzer", "values.schema.json") + + data, err := os.ReadFile(schemaPath) + require.NoError(t, err, "values.schema.json should be readable") + + var schema map[string]interface{} + err = yaml.Unmarshal(data, &schema) + require.NoError(t, err, "values.schema.json should be valid JSON") + + properties, ok := schema["properties"].(map[string]interface{}) + require.True(t, ok, "schema should have properties") + + assert.Contains(t, properties, "cloudProvider", "schema should define cloudProvider") + assert.Contains(t, properties, "ccoMode", "schema should define ccoMode") + assert.Contains(t, properties, "cloudCredentials", "schema should define cloudCredentials") + + cloudProvider, ok := properties["cloudProvider"].(map[string]interface{}) + require.True(t, ok, "cloudProvider should be an object") + assert.Equal(t, "string", cloudProvider["type"], "cloudProvider should be a string type") + + cpEnum, ok := cloudProvider["enum"].([]interface{}) + require.True(t, ok, "cloudProvider should have enum values") + assert.Contains(t, cpEnum, "aws", "cloudProvider enum should include aws") + assert.Contains(t, cpEnum, "gcp", "cloudProvider enum should include gcp") + + ccoMode, ok := properties["ccoMode"].(map[string]interface{}) + require.True(t, ok, "ccoMode should be an object") + assert.Equal(t, "string", ccoMode["type"], "ccoMode should be a string type") + + modeEnum, ok := ccoMode["enum"].([]interface{}) + require.True(t, ok, "ccoMode should have enum values") + assert.Contains(t, modeEnum, "default", "ccoMode enum should include default") + assert.Contains(t, modeEnum, "mint", "ccoMode enum should include mint") + assert.Contains(t, modeEnum, "passthrough", "ccoMode enum should include passthrough") + assert.Contains(t, modeEnum, "manual", "ccoMode enum should include manual") + + cloudCreds, ok := properties["cloudCredentials"].(map[string]interface{}) + require.True(t, ok, "cloudCredentials should be an object") + ccProps, ok := cloudCreds["properties"].(map[string]interface{}) + require.True(t, ok, "cloudCredentials should have properties") + assert.Contains(t, ccProps, "aws", "cloudCredentials should have aws") + assert.Contains(t, ccProps, "gcp", "cloudCredentials should have gcp") + + awsCreds, ok := ccProps["aws"].(map[string]interface{}) + require.True(t, ok, "aws credentials should be an object") + awsProps, ok := awsCreds["properties"].(map[string]interface{}) + require.True(t, ok, "aws should have properties") + assert.Contains(t, awsProps, "stsIAMRoleARN", "aws should have stsIAMRoleARN property") +} From e36cbbc24714c020c1a9e815077960f86e0d2f01 Mon Sep 17 00:00:00 2001 From: desmax74 Date: Tue, 23 Jun 2026 12:00:19 +0200 Subject: [PATCH 2/3] rds Signed-off-by: desmax74 --- CLAUDE.md | 40 +++- .../templates/helpers/_cco.tpl | 42 ++++ .../templates/helpers/_postgres.tpl | 12 ++ .../init/migrate-database/020-Job.yaml | 4 + .../services/importer/030-Deployment.yaml | 1 + .../services/server/030-Deployment.yaml | 1 + .../values.schema.json | 156 +++++++------- .../values.schema.yaml | 21 +- .../values.yaml | 4 + test/e2e/cco_helm_rendering_test.go | 195 ++++++++++++++++++ test/fixtures/aws_cco_manual_rds_cr.yaml | 58 ++++++ test/fixtures/aws_cco_rds_cr.yaml | 57 +++++ 12 files changed, 520 insertions(+), 71 deletions(-) create mode 100644 test/fixtures/aws_cco_manual_rds_cr.yaml create mode 100644 test/fixtures/aws_cco_rds_cr.yaml diff --git a/CLAUDE.md b/CLAUDE.md index 79d5bb45..6e710f8a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -222,16 +222,54 @@ When manual mode is active, the operator automatically: - Sets `AWS_SHARED_CREDENTIALS_FILE`, `AWS_WEB_IDENTITY_TOKEN_FILE`, and `AWS_ROLE_ARN` environment variables - Omits `TRUSTD_S3_ACCESS_KEY` / `TRUSTD_S3_SECRET_KEY` (the AWS SDK uses STS instead) +### RDS IAM Authentication via CCO + +The operator supports using CCO-provisioned credentials for RDS IAM authentication, eliminating the need for static database passwords. This is controlled by `ccoRds.enabled` and requires `cloudProvider` to be set. + +```yaml +spec: + cloudProvider: aws + ccoRds: + enabled: true + region: us-east-1 + cloudCredentials: + aws: + statementEntries: + - effect: Allow + action: ["s3:*", "rds-db:connect"] + resource: "*" + database: + host: mydb.cluster-xyz.us-east-1.rds.amazonaws.com + name: trustify + username: trustify_user + # password is NOT required when ccoRds.enabled is true +``` + +When `ccoRds.enabled` is true: +- `TRUSTD_DB_IAM_AUTH=true` is set on all trustd pods +- `TRUSTD_DB_REGION` is set from `ccoRds.region` +- `database.password` becomes optional (omitted from env vars) +- SSL mode is forced to `require` (RDS IAM auth mandates TLS) +- For **mint/passthrough/default** modes: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` are populated from the CCO secret +- For **manual** (STS) mode: the existing STS volumes/env vars are sufficient — no extra credentials needed + +When `ccoRds.enabled` is false or absent, behavior is unchanged — `database.password` is required and SSL mode uses the configured value. + +**Note:** The `create-database` and `create-importers` init jobs use `psql` directly and cannot generate RDS IAM tokens. These jobs continue requiring static credentials via `createDatabase.database`. The `migrate-database` job runs `trustd db migrate` and supports RDS IAM auth. + ### Key Files | File | Purpose | |------|---------| | `helm-charts/.../templates/credentialrequest.yaml` | CredentialsRequest template (conditional on `cloudProvider`) | -| `helm-charts/.../templates/helpers/_cco.tpl` | Helper templates for manual mode volumes/mounts/env vars | +| `helm-charts/.../templates/helpers/_cco.tpl` | Helper templates for manual mode volumes/mounts/env vars and RDS IAM auth | | `helm-charts/.../templates/helpers/_storage.tpl` | S3 env vars — branches on CCO vs manual credentials | +| `helm-charts/.../templates/helpers/_postgres.tpl` | Database env vars — branches on ccoRds for password/SSL | | `config/rbac/clusterrole.yaml` | ClusterRole granting access to `credentialsrequests` API | | `config/rbac/clusterrolebinding_cco.yaml` | Binds the CCO ClusterRole to the operator ServiceAccount | | `test/fixtures/aws_cco_*.yaml` | Example CRs for each CCO mode | +| `test/fixtures/aws_cco_rds_cr.yaml` | Example CR for RDS IAM auth (mint mode) | +| `test/fixtures/aws_cco_manual_rds_cr.yaml` | Example CR for RDS IAM auth (manual/STS mode) | | `test/e2e/cco_helm_rendering_test.go` | E2E tests for CCO template rendering | ### RBAC diff --git a/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_cco.tpl b/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_cco.tpl index 73b7dbaa..18ecadd6 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_cco.tpl +++ b/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_cco.tpl @@ -73,3 +73,45 @@ For mint/passthrough/default modes this emits nothing. {{- end }} {{- end }} {{- end -}} + +{{/* +Returns "true" when CCO RDS IAM authentication is active +(cloudProvider set AND ccoRds.enabled is true). +*/}} +{{- define "trustification.cco.rds.isEnabled" -}} +{{- if and .root.Values.cloudProvider .root.Values.ccoRds -}} + {{- if .root.Values.ccoRds.enabled -}}true{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Environment variables for RDS IAM authentication via CCO. +Emits TRUSTD_DB_IAM_AUTH, TRUSTD_DB_REGION, and — for non-manual modes — +AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from the CCO secret so the +AWS SDK can generate RDS auth tokens. + +Arguments (dict): + * root - . +*/}} +{{- define "trustification.cco.rds.envVars" -}} +{{- if eq (include "trustification.cco.rds.isEnabled" .) "true" }} +- name: TRUSTD_DB_IAM_AUTH + value: "true" +{{- with .root.Values.ccoRds.region }} +- name: TRUSTD_DB_REGION + value: {{ . | quote }} +{{- end }} +{{- if not (eq (include "trustification.cco.isManualMode" .) "true") }} +- name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: {{ include "trustification.cco.secretName" . }} + key: aws_access_key_id +- name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ include "trustification.cco.secretName" . }} + key: aws_secret_access_key +{{- end }} +{{- end }} +{{- end -}} diff --git a/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_postgres.tpl b/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_postgres.tpl index 3187d27c..55c92fda 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_postgres.tpl +++ b/helm-charts/redhat-trusted-profile-analyzer/templates/helpers/_postgres.tpl @@ -16,11 +16,17 @@ Arguments (dict): {{- include "trustification.common.requiredEnvVarValue" (dict "value" .database.name "msg" "Missing value for database name" ) | nindent 2 }} - name: {{ .prefix | default "" }}PGUSER {{- include "trustification.common.requiredEnvVarValue" (dict "value" .database.username "msg" "Missing value for database username" ) | nindent 2 }} +{{- if not (eq (include "trustification.cco.rds.isEnabled" (dict "root" .root)) "true") }} - name: {{ .prefix | default "" }}PGPASSWORD {{- include "trustification.common.requiredEnvVarValue" (dict "value" .database.password "msg" "Missing value for database password" ) | nindent 2 }} +{{- end }} - name: {{ .prefix | default "" }}PGSSLMODE +{{- if eq (include "trustification.cco.rds.isEnabled" (dict "root" .root)) "true" }} + value: require +{{- else }} value: {{ .database.sslMode | default "allow" }} {{- end }} +{{- end }} {{/* Postgres configuration env-vars. @@ -40,10 +46,16 @@ Arguments (dict): {{- include "trustification.common.requiredEnvVarValue" (dict "value" .database.name "msg" "Missing value for database name" ) | nindent 2 }} - name: {{ .prefix | default "TRUSTD_DB_" }}USER {{- include "trustification.common.requiredEnvVarValue" (dict "value" .database.username "msg" "Missing value for database username" ) | nindent 2 }} +{{- if not (eq (include "trustification.cco.rds.isEnabled" (dict "root" .root)) "true") }} - name: {{ .prefix | default "TRUSTD_DB_" }}PASSWORD {{- include "trustification.common.requiredEnvVarValue" (dict "value" .database.password "msg" "Missing value for database password" ) | nindent 2 }} +{{- end }} - name: {{ .prefix | default "TRUSTD_DB_" }}SSLMODE +{{- if eq (include "trustification.cco.rds.isEnabled" (dict "root" .root)) "true" }} + value: require +{{- else }} value: {{ .database.sslMode | default "allow" }} +{{- end }} {{- with .database.minimumConnections }} - name: {{ .prefix | default "TRUSTD_DB_" }}MIN_CONN diff --git a/helm-charts/redhat-trusted-profile-analyzer/templates/init/migrate-database/020-Job.yaml b/helm-charts/redhat-trusted-profile-analyzer/templates/init/migrate-database/020-Job.yaml index a5629d8c..053ed484 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/templates/init/migrate-database/020-Job.yaml +++ b/helm-charts/redhat-trusted-profile-analyzer/templates/init/migrate-database/020-Job.yaml @@ -34,6 +34,7 @@ spec: {{- include "trustification.application.pod" $mod | nindent 6 }} volumes: + {{- include "trustification.cco.volumes" $mod | nindent 8 }} {{- include "trustification.application.extraVolumes" $mod | nindent 8 }} containers: @@ -43,9 +44,12 @@ spec: env: {{- include "trustification.postgres.envVars" ( dict "root" . "database" ( merge ( required "Using migrateDatabase requires setting .Values.migrateDatabase" (deepCopy .Values.migrateDatabase) ) ( deepCopy .Values.database ) ) ) | nindent 12 }} + {{- include "trustification.cco.envVars" $mod | nindent 12 }} + {{- include "trustification.cco.rds.envVars" $mod | nindent 12 }} volumeMounts: {{- include "trustification.application.extraVolumeMounts" $mod | nindent 12 }} + {{- include "trustification.cco.volumeMounts" $mod | nindent 12 }} command: - trustd diff --git a/helm-charts/redhat-trusted-profile-analyzer/templates/services/importer/030-Deployment.yaml b/helm-charts/redhat-trusted-profile-analyzer/templates/services/importer/030-Deployment.yaml index 42dbf73b..e6f03503 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/templates/services/importer/030-Deployment.yaml +++ b/helm-charts/redhat-trusted-profile-analyzer/templates/services/importer/030-Deployment.yaml @@ -50,6 +50,7 @@ spec: {{- include "trustification.postgres.envVars" (dict "root" . "database" .Values.database) | nindent 12 }} {{- include "trustification.storage.envVars" $mod | nindent 12 }} {{- include "trustification.cco.envVars" $mod | nindent 12 }} + {{- include "trustification.cco.rds.envVars" $mod | nindent 12 }} ports: {{- include "trustification.application.infrastructure.podPorts" $mod | nindent 12 }} diff --git a/helm-charts/redhat-trusted-profile-analyzer/templates/services/server/030-Deployment.yaml b/helm-charts/redhat-trusted-profile-analyzer/templates/services/server/030-Deployment.yaml index a0f726ff..9cc45f1a 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/templates/services/server/030-Deployment.yaml +++ b/helm-charts/redhat-trusted-profile-analyzer/templates/services/server/030-Deployment.yaml @@ -52,6 +52,7 @@ spec: {{- include "trustification.postgres.envVars" (dict "root" . "database" .Values.database) | nindent 12 }} {{- include "trustification.storage.envVars" $mod | nindent 12 }} {{- include "trustification.cco.envVars" $mod | nindent 12 }} + {{- include "trustification.cco.rds.envVars" $mod | nindent 12 }} {{- include "trustification.oidc.swaggerUi" $mod | nindent 12 }} - name: UI_ISSUER_URL diff --git a/helm-charts/redhat-trusted-profile-analyzer/values.schema.json b/helm-charts/redhat-trusted-profile-analyzer/values.schema.json index 9ce36989..cb92857d 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/values.schema.json +++ b/helm-charts/redhat-trusted-profile-analyzer/values.schema.json @@ -61,6 +61,89 @@ } } }, + "ccoMode": { + "type": "string", + "enum": [ + "default", + "mint", + "passthrough", + "manual" + ], + "description": "Cloud Credential Operator mode. Controls how cloud credentials are provisioned.\n- default: CCO auto-determines the provisioning method\n- mint: CCO creates new IAM credentials from permissions in CredentialsRequest\n- passthrough: CCO copies admin credentials to the target namespace\n- manual: Credentials are pre-provisioned via ccoctl (STS/WIF). Pods are\n automatically configured with projected SA token volumes and SDK env vars.\n" + }, + "cloudProvider": { + "type": "string", + "enum": [ + "aws", + "gcp" + ], + "description": "Set the cloud provider to enable Cloud Credentials Operator (CCO) integration.\nWhen set, a CredentialsRequest will be created and the resulting secret will be\nused for S3 storage credentials (accessKey and secretKey become optional).\n" + }, + "ccoRds": { + "type": "object", + "additionalProperties": false, + "description": "Enable RDS IAM authentication via Cloud Credential Operator.\nWhen enabled, the database password is not required \u2014 pods authenticate\nto RDS using IAM credentials provisioned by CCO.\nRequires cloudProvider to be set.\n", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable RDS IAM authentication. When true, database password is\nomitted and SSL mode is forced to 'require'.\n" + }, + "region": { + "type": "string", + "description": "AWS region of the RDS instance. Used by the application to\ngenerate RDS IAM auth tokens.\n" + } + } + }, + "cloudCredentials": { + "type": "object", + "description": "Cloud-specific credential configuration for the Cloud Credentials Operator.\n", + "properties": { + "aws": { + "type": "object", + "properties": { + "statementEntries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "effect": { + "type": "string", + "enum": [ + "Allow", + "Deny" + ] + }, + "action": { + "type": "array", + "items": { + "type": "string" + } + }, + "resource": { + "type": "string" + } + } + } + }, + "stsIAMRoleARN": { + "type": "string", + "description": "The ARN of the IAM role for STS-based authentication (manual mode only).\n" + } + } + }, + "gcp": { + "type": "object", + "properties": { + "permissions": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, "oidc": { "$ref": "#/definitions/Oidc" }, @@ -385,70 +468,6 @@ }, "prometheus": { "$ref": "#/definitions/Feature" - }, - "ccoMode": { - "type": "string", - "enum": [ - "default", - "mint", - "passthrough", - "manual" - ], - "description": "Cloud Credential Operator mode. Controls how cloud credentials are provisioned.\n- default: CCO auto-determines the provisioning method\n- mint: CCO creates new IAM credentials from permissions in CredentialsRequest\n- passthrough: CCO copies admin credentials to the target namespace\n- manual: Credentials are pre-provisioned via ccoctl (STS/WIF). Pods are automatically configured with projected SA token volumes and SDK env vars.\n" - }, - "cloudProvider": { - "type": "string", - "enum": [ - "aws", - "gcp" - ], - "description": "Set the cloud provider to enable Cloud Credentials Operator (CCO) integration.\nWhen set, a CredentialsRequest will be created and the resulting secret will be\nused for S3 storage credentials (accessKey and secretKey become optional).\n" - }, - "cloudCredentials": { - "type": "object", - "description": "Cloud-specific credential configuration for the Cloud Credentials Operator.\n", - "properties": { - "aws": { - "type": "object", - "properties": { - "statementEntries": { - "type": "array", - "items": { - "type": "object", - "properties": { - "effect": { - "type": "string" - }, - "action": { - "type": "array", - "items": { - "type": "string" - } - }, - "resource": { - "type": "string" - } - } - } - }, - "stsIAMRoleARN": { - "type": "string", - "description": "The ARN of the IAM role for STS-based authentication (manual mode only).\n" - } - } - }, - "gcp": { - "type": "object", - "properties": { - "permissions": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } } }, "allOf": [ @@ -817,7 +836,7 @@ }, "userInfo": { "type": "boolean", - "description": "Value for the `OIDC_USER_INFO` boolean environment variable in the server deployment.\n", + "description": "Value for the `OIDC_USER_INFO` environment variable in the server deployment\n", "default": true }, "clients": { @@ -1051,11 +1070,11 @@ ] }, "accessKey": { - "description": "The access key/username to the storage resource\n", + "description": "The access key/username to the storage resource.\nOptional when cloudProvider is set (auto-populated from CCO secret).\n", "$ref": "#/definitions/ValueOrRef" }, "secretKey": { - "description": "The secret key/password to the storage resource\n", + "description": "The secret key/password to the storage resource.\nOptional when cloudProvider is set (auto-populated from CCO secret).\n", "$ref": "#/definitions/ValueOrRef" }, "bucket": { @@ -1098,8 +1117,7 @@ "required": [ "host", "name", - "username", - "password" + "username" ], "properties": { "host": { diff --git a/helm-charts/redhat-trusted-profile-analyzer/values.schema.yaml b/helm-charts/redhat-trusted-profile-analyzer/values.schema.yaml index 85782964..a1252d43 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/values.schema.yaml +++ b/helm-charts/redhat-trusted-profile-analyzer/values.schema.yaml @@ -89,6 +89,26 @@ properties: When set, a CredentialsRequest will be created and the resulting secret will be used for S3 storage credentials (accessKey and secretKey become optional). + ccoRds: + type: object + additionalProperties: false + description: | + Enable RDS IAM authentication via Cloud Credential Operator. + When enabled, the database password is not required — pods authenticate + to RDS using IAM credentials provisioned by CCO. + Requires cloudProvider to be set. + properties: + enabled: + type: boolean + description: | + Enable RDS IAM authentication. When true, database password is + omitted and SSL mode is forced to 'require'. + region: + type: string + description: | + AWS region of the RDS instance. Used by the application to + generate RDS IAM auth tokens. + cloudCredentials: type: object description: | @@ -861,7 +881,6 @@ definitions: - host - name - username - - password properties: host: $ref: "#/definitions/ValueOrRef" diff --git a/helm-charts/redhat-trusted-profile-analyzer/values.yaml b/helm-charts/redhat-trusted-profile-analyzer/values.yaml index ee2bd15d..01f7dece 100644 --- a/helm-charts/redhat-trusted-profile-analyzer/values.yaml +++ b/helm-charts/redhat-trusted-profile-analyzer/values.yaml @@ -21,6 +21,10 @@ database: {} # cloudProvider: # aws | gcp — set to enable CCO-managed credentials for S3 storage cloudCredentials: {} + +# ccoRds: # Uncomment to enable RDS IAM authentication via CCO +# enabled: false +# region: "" # AWS region of the RDS instance #aws: # statementEntries: # - effect: Allow diff --git a/test/e2e/cco_helm_rendering_test.go b/test/e2e/cco_helm_rendering_test.go index 7cfe1dc5..115cf5b0 100644 --- a/test/e2e/cco_helm_rendering_test.go +++ b/test/e2e/cco_helm_rendering_test.go @@ -30,12 +30,14 @@ const ( fieldCloudProvider = "cloudProvider" fieldCloudCredentials = "cloudCredentials" fieldCcoMode = "ccoMode" + fieldCcoRds = "ccoRds" kindCredentialsReq = "CredentialsRequest" ccoSecretName = "test-release-cloud-creds" ccoNamespace = "openshift-cloud-credential-operator" testBucket = "trustify-storage" testRegionUSEast1 = "us-east-1" testSTSRoleARN = "arn:aws:iam::123456789012:role/trustify-s3-role" + testRdsHost = "testdb.cluster-xyz.us-east-1.rds.amazonaws.com" cloudProviderAWS = "aws" cloudProviderGCP = "gcp" ) @@ -511,6 +513,199 @@ func TestHelmRenderAWSPassthroughCredentialsRequest(t *testing.T) { "passthrough mode should reference aws_access_key_id from CCO secret") } +func rdsIamDatabaseValues() map[string]interface{} { + return map[string]interface{}{ + "host": testRdsHost, + fieldName: "testdb", + "username": "testuser", + } +} + +func awsRdsValues() map[string]interface{} { + v := awsValues() + v[fieldCcoRds] = map[string]interface{}{ + fieldEnabled: true, + fieldRegion: testRegionUSEast1, + } + v[fieldDatabase] = rdsIamDatabaseValues() + return v +} + +func awsManualRdsValues() map[string]interface{} { + v := awsManualValues() + v[fieldCcoRds] = map[string]interface{}{ + fieldEnabled: true, + fieldRegion: testRegionUSEast1, + } + v[fieldDatabase] = rdsIamDatabaseValues() + return v +} + +func TestHelmRenderRdsIamEnabled(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsRdsValues()) + + assert.Contains(t, rendered, "TRUSTD_DB_IAM_AUTH", + "RDS IAM mode should set TRUSTD_DB_IAM_AUTH") + assert.Contains(t, rendered, "TRUSTD_DB_REGION", + "RDS IAM mode should set TRUSTD_DB_REGION") + assert.NotContains(t, rendered, "TRUSTD_DB_PASSWORD", + "RDS IAM mode should NOT set TRUSTD_DB_PASSWORD") +} + +func TestHelmRenderRdsIamDisabled(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsValues()) + + assert.NotContains(t, rendered, "TRUSTD_DB_IAM_AUTH", + "without ccoRds, TRUSTD_DB_IAM_AUTH should NOT be set") + assert.NotContains(t, rendered, "TRUSTD_DB_REGION", + "without ccoRds, TRUSTD_DB_REGION should NOT be set") + assert.Contains(t, rendered, "TRUSTD_DB_PASSWORD", + "without ccoRds, TRUSTD_DB_PASSWORD should be set") +} + +func TestHelmRenderRdsIamNonManualAWSCredentials(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsRdsValues()) + docs := splitYAMLDocs(rendered) + + for _, doc := range docs { + obj, err := parseYAMLDoc(doc) + if err != nil { + continue + } + if obj["kind"] != "Deployment" { + continue + } + metadata, _ := obj[fieldMetadata].(map[string]interface{}) + if metadata == nil || !strings.Contains(metadata[fieldName].(string), fieldServer) { + continue + } + + spec, _ := obj[fieldSpec].(map[string]interface{}) + template, _ := spec["template"].(map[string]interface{}) + templateSpec, _ := template[fieldSpec].(map[string]interface{}) + containers, _ := templateSpec["containers"].([]interface{}) + require.NotEmpty(t, containers, "server deployment should have containers") + + container, _ := containers[0].(map[string]interface{}) + envList, _ := container["env"].([]interface{}) + require.NotEmpty(t, envList, "container should have env vars") + + accessKeyFound := false + secretKeyFound := false + for _, e := range envList { + env, ok := e.(map[string]interface{}) + if !ok { + continue + } + envName, _ := env[fieldName].(string) + if envName == "AWS_ACCESS_KEY_ID" { + refName := nestedString(env, "valueFrom", "secretKeyRef", fieldName) + refKey := nestedString(env, "valueFrom", "secretKeyRef", "key") + assert.Equal(t, ccoSecretName, refName, "AWS_ACCESS_KEY_ID should reference CCO secret") + assert.Equal(t, "aws_access_key_id", refKey, "AWS_ACCESS_KEY_ID should use correct key") + accessKeyFound = true + } + if envName == "AWS_SECRET_ACCESS_KEY" { + refName := nestedString(env, "valueFrom", "secretKeyRef", fieldName) + refKey := nestedString(env, "valueFrom", "secretKeyRef", "key") + assert.Equal(t, ccoSecretName, refName, "AWS_SECRET_ACCESS_KEY should reference CCO secret") + assert.Equal(t, "aws_secret_access_key", refKey, "AWS_SECRET_ACCESS_KEY should use correct key") + secretKeyFound = true + } + } + assert.True(t, accessKeyFound, "AWS_ACCESS_KEY_ID env var should be present for RDS IAM in mint mode") + assert.True(t, secretKeyFound, "AWS_SECRET_ACCESS_KEY env var should be present for RDS IAM in mint mode") + return + } + t.Fatal("server deployment not found in rendered output") +} + +func TestHelmRenderRdsIamManualMode(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsManualRdsValues()) + + assert.Contains(t, rendered, "TRUSTD_DB_IAM_AUTH", + "manual RDS IAM mode should set TRUSTD_DB_IAM_AUTH") + assert.Contains(t, rendered, "TRUSTD_DB_REGION", + "manual RDS IAM mode should set TRUSTD_DB_REGION") + assert.NotContains(t, rendered, "TRUSTD_DB_PASSWORD", + "manual RDS IAM mode should NOT set TRUSTD_DB_PASSWORD") + + assert.Contains(t, rendered, "AWS_WEB_IDENTITY_TOKEN_FILE", + "manual mode should still set AWS_WEB_IDENTITY_TOKEN_FILE") + assert.Contains(t, rendered, "AWS_ROLE_ARN", + "manual mode should still set AWS_ROLE_ARN") +} + +func TestHelmRenderRdsIamSSLMode(t *testing.T) { + if testing.Short() { + t.Skip(skipE2ETest) + } + + chartPath := getChartPath(t) + rendered := renderHelmChart(t, chartPath, awsRdsValues()) + docs := splitYAMLDocs(rendered) + + for _, doc := range docs { + obj, err := parseYAMLDoc(doc) + if err != nil { + continue + } + if obj["kind"] != "Deployment" { + continue + } + metadata, _ := obj[fieldMetadata].(map[string]interface{}) + if metadata == nil || !strings.Contains(metadata[fieldName].(string), fieldServer) { + continue + } + + spec, _ := obj[fieldSpec].(map[string]interface{}) + template, _ := spec["template"].(map[string]interface{}) + templateSpec, _ := template[fieldSpec].(map[string]interface{}) + containers, _ := templateSpec["containers"].([]interface{}) + require.NotEmpty(t, containers, "server deployment should have containers") + + container, _ := containers[0].(map[string]interface{}) + envList, _ := container["env"].([]interface{}) + require.NotEmpty(t, envList, "container should have env vars") + + for _, e := range envList { + env, ok := e.(map[string]interface{}) + if !ok { + continue + } + envName, _ := env[fieldName].(string) + if envName == "TRUSTD_DB_SSLMODE" { + envValue, _ := env["value"].(string) + assert.Equal(t, "require", envValue, + "RDS IAM mode should force TRUSTD_DB_SSLMODE to require") + return + } + } + t.Fatal("TRUSTD_DB_SSLMODE not found in server deployment env vars") + } + t.Fatal("server deployment not found in rendered output") +} + func TestHelmRenderAWSDefaultCredentialsRequest(t *testing.T) { if testing.Short() { t.Skip(skipE2ETest) diff --git a/test/fixtures/aws_cco_manual_rds_cr.yaml b/test/fixtures/aws_cco_manual_rds_cr.yaml new file mode 100644 index 00000000..7e58c7fe --- /dev/null +++ b/test/fixtures/aws_cco_manual_rds_cr.yaml @@ -0,0 +1,58 @@ +apiVersion: rhtpa.io/v1 +kind: TrustedProfileAnalyzer +metadata: + name: aws-cco-manual-rds-instance + namespace: test-namespace +spec: + appDomain: test.example.com + replicas: 1 + cloudProvider: aws + ccoMode: manual + ccoRds: + enabled: true + region: us-east-1 + cloudCredentials: + aws: + statementEntries: + - effect: Allow + action: + - "s3:GetObject" + - "s3:PutObject" + - "s3:DeleteObject" + - "s3:ListBucket" + - "rds-db:connect" + resource: "*" + stsIAMRoleARN: "arn:aws:iam::123456789012:role/trustify-s3-rds-role" + storage: + type: s3 + bucket: trustify-storage + region: us-east-1 + modules: + server: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + importer: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + oidc: + clients: + frontend: + clientId: test-frontend + cli: + clientSecret: test-secret + database: + host: testdb.cluster-xyz.us-east-1.rds.amazonaws.com + name: testdb + username: testuser + metrics: + enabled: false + tracing: + enabled: false diff --git a/test/fixtures/aws_cco_rds_cr.yaml b/test/fixtures/aws_cco_rds_cr.yaml new file mode 100644 index 00000000..e7ee00a4 --- /dev/null +++ b/test/fixtures/aws_cco_rds_cr.yaml @@ -0,0 +1,57 @@ +apiVersion: rhtpa.io/v1 +kind: TrustedProfileAnalyzer +metadata: + name: aws-cco-rds-instance + namespace: test-namespace +spec: + appDomain: test.example.com + replicas: 1 + cloudProvider: aws + ccoMode: mint + ccoRds: + enabled: true + region: us-east-1 + cloudCredentials: + aws: + statementEntries: + - effect: Allow + action: + - "s3:GetObject" + - "s3:PutObject" + - "s3:DeleteObject" + - "s3:ListBucket" + - "rds-db:connect" + resource: "*" + storage: + type: s3 + bucket: trustify-storage + region: us-east-1 + modules: + server: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + importer: + enabled: true + replicas: 1 + resources: + requests: + cpu: 100m + memory: 128Mi + oidc: + clients: + frontend: + clientId: test-frontend + cli: + clientSecret: test-secret + database: + host: testdb.cluster-xyz.us-east-1.rds.amazonaws.com + name: testdb + username: testuser + metrics: + enabled: false + tracing: + enabled: false From c8cefaf3e718fd6cb74ea4669dcd9bb38788357a Mon Sep 17 00:00:00 2001 From: desmax74 Date: Tue, 23 Jun 2026 12:10:06 +0200 Subject: [PATCH 3/3] golint fix Signed-off-by: desmax74 --- test/e2e/cco_helm_rendering_test.go | 172 +++++++++++----------------- test/e2e/helm_rendering_test.go | 7 +- 2 files changed, 73 insertions(+), 106 deletions(-) diff --git a/test/e2e/cco_helm_rendering_test.go b/test/e2e/cco_helm_rendering_test.go index 115cf5b0..a054b11a 100644 --- a/test/e2e/cco_helm_rendering_test.go +++ b/test/e2e/cco_helm_rendering_test.go @@ -174,6 +174,64 @@ func findCredentialsRequest(t *testing.T, docs []string) map[string]interface{} return nil } +func assertServerDeploymentCCOEnvVars( + t *testing.T, docs []string, + accessKeyEnv, secretKeyEnv, accessKeyMsg, secretKeyMsg string, +) { + t.Helper() + for _, doc := range docs { + obj, err := parseYAMLDoc(doc) + if err != nil { + continue + } + if obj["kind"] != kindDeploymentResource { + continue + } + metadata, _ := obj[fieldMetadata].(map[string]interface{}) + if metadata == nil || !strings.Contains(metadata[fieldName].(string), fieldServer) { + continue + } + + spec, _ := obj[fieldSpec].(map[string]interface{}) + template, _ := spec["template"].(map[string]interface{}) + templateSpec, _ := template[fieldSpec].(map[string]interface{}) + containers, _ := templateSpec["containers"].([]interface{}) + require.NotEmpty(t, containers, "server deployment should have containers") + + container, _ := containers[0].(map[string]interface{}) + envList, _ := container["env"].([]interface{}) + require.NotEmpty(t, envList, "container should have env vars") + + accessKeyFound := false + secretKeyFound := false + for _, e := range envList { + env, ok := e.(map[string]interface{}) + if !ok { + continue + } + envName, _ := env[fieldName].(string) + if envName == accessKeyEnv { + refName := nestedString(env, "valueFrom", "secretKeyRef", fieldName) + refKey := nestedString(env, "valueFrom", "secretKeyRef", "key") + assert.Equal(t, ccoSecretName, refName, accessKeyEnv+" should reference CCO secret") + assert.Equal(t, "aws_access_key_id", refKey, accessKeyEnv+" should use correct key") + accessKeyFound = true + } + if envName == secretKeyEnv { + refName := nestedString(env, "valueFrom", "secretKeyRef", fieldName) + refKey := nestedString(env, "valueFrom", "secretKeyRef", "key") + assert.Equal(t, ccoSecretName, refName, secretKeyEnv+" should reference CCO secret") + assert.Equal(t, "aws_secret_access_key", refKey, secretKeyEnv+" should use correct key") + secretKeyFound = true + } + } + assert.True(t, accessKeyFound, accessKeyMsg) + assert.True(t, secretKeyFound, secretKeyMsg) + return + } + t.Fatal("server deployment not found in rendered output") +} + func nestedString(obj map[string]interface{}, keys ...string) string { current := obj for i, key := range keys { @@ -302,57 +360,11 @@ func TestHelmRenderStorageEnvVarsWithCCO(t *testing.T) { "rendered output should reference aws_secret_access_key key") docs := splitYAMLDocs(rendered) - for _, doc := range docs { - obj, err := parseYAMLDoc(doc) - if err != nil { - continue - } - if obj["kind"] != "Deployment" { - continue - } - metadata, _ := obj[fieldMetadata].(map[string]interface{}) - if metadata == nil || !strings.Contains(metadata[fieldName].(string), fieldServer) { - continue - } - - spec, _ := obj[fieldSpec].(map[string]interface{}) - template, _ := spec["template"].(map[string]interface{}) - templateSpec, _ := template[fieldSpec].(map[string]interface{}) - containers, _ := templateSpec["containers"].([]interface{}) - require.NotEmpty(t, containers, "server deployment should have containers") - - container, _ := containers[0].(map[string]interface{}) - envList, _ := container["env"].([]interface{}) - require.NotEmpty(t, envList, "container should have env vars") - - accessKeyFound := false - secretKeyFound := false - for _, e := range envList { - env, ok := e.(map[string]interface{}) - if !ok { - continue - } - envName, _ := env[fieldName].(string) - if envName == "TRUSTD_S3_ACCESS_KEY" { - refName := nestedString(env, "valueFrom", "secretKeyRef", fieldName) - refKey := nestedString(env, "valueFrom", "secretKeyRef", "key") - assert.Equal(t, ccoSecretName, refName, "ACCESS_KEY should reference CCO secret") - assert.Equal(t, "aws_access_key_id", refKey, "ACCESS_KEY should use correct key") - accessKeyFound = true - } - if envName == "TRUSTD_S3_SECRET_KEY" { - refName := nestedString(env, "valueFrom", "secretKeyRef", fieldName) - refKey := nestedString(env, "valueFrom", "secretKeyRef", "key") - assert.Equal(t, ccoSecretName, refName, "SECRET_KEY should reference CCO secret") - assert.Equal(t, "aws_secret_access_key", refKey, "SECRET_KEY should use correct key") - secretKeyFound = true - } - } - assert.True(t, accessKeyFound, "TRUSTD_S3_ACCESS_KEY env var should be present") - assert.True(t, secretKeyFound, "TRUSTD_S3_SECRET_KEY env var should be present") - return - } - t.Fatal("server deployment not found in rendered output") + assertServerDeploymentCCOEnvVars(t, docs, + "TRUSTD_S3_ACCESS_KEY", "TRUSTD_S3_SECRET_KEY", + "TRUSTD_S3_ACCESS_KEY env var should be present", + "TRUSTD_S3_SECRET_KEY env var should be present", + ) } func TestHelmRenderStorageEnvVarsWithoutCCO(t *testing.T) { @@ -582,57 +594,11 @@ func TestHelmRenderRdsIamNonManualAWSCredentials(t *testing.T) { rendered := renderHelmChart(t, chartPath, awsRdsValues()) docs := splitYAMLDocs(rendered) - for _, doc := range docs { - obj, err := parseYAMLDoc(doc) - if err != nil { - continue - } - if obj["kind"] != "Deployment" { - continue - } - metadata, _ := obj[fieldMetadata].(map[string]interface{}) - if metadata == nil || !strings.Contains(metadata[fieldName].(string), fieldServer) { - continue - } - - spec, _ := obj[fieldSpec].(map[string]interface{}) - template, _ := spec["template"].(map[string]interface{}) - templateSpec, _ := template[fieldSpec].(map[string]interface{}) - containers, _ := templateSpec["containers"].([]interface{}) - require.NotEmpty(t, containers, "server deployment should have containers") - - container, _ := containers[0].(map[string]interface{}) - envList, _ := container["env"].([]interface{}) - require.NotEmpty(t, envList, "container should have env vars") - - accessKeyFound := false - secretKeyFound := false - for _, e := range envList { - env, ok := e.(map[string]interface{}) - if !ok { - continue - } - envName, _ := env[fieldName].(string) - if envName == "AWS_ACCESS_KEY_ID" { - refName := nestedString(env, "valueFrom", "secretKeyRef", fieldName) - refKey := nestedString(env, "valueFrom", "secretKeyRef", "key") - assert.Equal(t, ccoSecretName, refName, "AWS_ACCESS_KEY_ID should reference CCO secret") - assert.Equal(t, "aws_access_key_id", refKey, "AWS_ACCESS_KEY_ID should use correct key") - accessKeyFound = true - } - if envName == "AWS_SECRET_ACCESS_KEY" { - refName := nestedString(env, "valueFrom", "secretKeyRef", fieldName) - refKey := nestedString(env, "valueFrom", "secretKeyRef", "key") - assert.Equal(t, ccoSecretName, refName, "AWS_SECRET_ACCESS_KEY should reference CCO secret") - assert.Equal(t, "aws_secret_access_key", refKey, "AWS_SECRET_ACCESS_KEY should use correct key") - secretKeyFound = true - } - } - assert.True(t, accessKeyFound, "AWS_ACCESS_KEY_ID env var should be present for RDS IAM in mint mode") - assert.True(t, secretKeyFound, "AWS_SECRET_ACCESS_KEY env var should be present for RDS IAM in mint mode") - return - } - t.Fatal("server deployment not found in rendered output") + assertServerDeploymentCCOEnvVars(t, docs, + "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", + "AWS_ACCESS_KEY_ID env var should be present for RDS IAM in mint mode", + "AWS_SECRET_ACCESS_KEY env var should be present for RDS IAM in mint mode", + ) } func TestHelmRenderRdsIamManualMode(t *testing.T) { @@ -670,7 +636,7 @@ func TestHelmRenderRdsIamSSLMode(t *testing.T) { if err != nil { continue } - if obj["kind"] != "Deployment" { + if obj["kind"] != kindDeploymentResource { continue } metadata, _ := obj[fieldMetadata].(map[string]interface{}) diff --git a/test/e2e/helm_rendering_test.go b/test/e2e/helm_rendering_test.go index cd700a59..047767b8 100644 --- a/test/e2e/helm_rendering_test.go +++ b/test/e2e/helm_rendering_test.go @@ -34,8 +34,9 @@ import ( ) const ( - testAppDomain = "test.example.com" - kindDeployment = "kind: Deployment" + testAppDomain = "test.example.com" + kindDeployment = "kind: Deployment" + kindDeploymentResource = "Deployment" ) func TestHelmChartRenderWithMinimalValues(t *testing.T) { @@ -111,7 +112,7 @@ func TestHelmChartRenderServerModule(t *testing.T) { if err := yaml.Unmarshal([]byte(doc), &obj.Object); err != nil { continue } - if obj.GetKind() != "Deployment" || obj.GetName() != fieldServer { + if obj.GetKind() != kindDeploymentResource || obj.GetName() != fieldServer { continue } serverDeploymentFound = true