diff --git a/.gitignore b/.gitignore index 093e58e..dd6f54b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Binaries for programs and plugins -pull-secret -pull-secret-mvp +/pull-secret +/pull-secret-mvp *.exe *.exe~ *.dll diff --git a/charts/pull-secret/Chart.yaml b/charts/pull-secret/Chart.yaml index ab540dc..90016eb 100644 --- a/charts/pull-secret/Chart.yaml +++ b/charts/pull-secret/Chart.yaml @@ -4,10 +4,14 @@ description: HyperFleet Pull Secret Adapter - Manages pull secrets in GCP Secret type: application version: 0.1.0 appVersion: "1.0" +maintainers: + - name: HyperFleet Team + email: hyperfleet-team@redhat.com keywords: - hyperfleet + - adapter - pull-secret - gcp - secret-manager -maintainers: - - name: HyperFleet Team + - kubernetes +home: https://github.com/openshift-hyperfleet/adapter-pull-secret diff --git a/charts/pull-secret/README.md b/charts/pull-secret/README.md index f89a0af..6ead2e5 100644 --- a/charts/pull-secret/README.md +++ b/charts/pull-secret/README.md @@ -1,6 +1,27 @@ # Pull Secret Adapter Helm Chart -This Helm chart deploys the HyperFleet Pull Secret Adapter as a Kubernetes Job on GKE. +This Helm chart deploys the HyperFleet Pull Secret Adapter using the Adapter Framework pattern. The adapter listens to PubSub messages and dynamically creates jobs to store pull secrets in GCP Secret Manager. + +## Architecture + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ Adapter Framework (Deployment) │ +│ - Listens to PubSub messages │ +│ - Creates Jobs based on AdapterConfig │ +└────────────────────────┬─────────────────────────────────────────┘ + │ + ▼ (creates dynamically) +┌──────────────────────────────────────────────────────────────────┐ +│ Pull Secret Job (per cluster) │ +│ ┌─────────────────────┐ ┌─────────────────────┐ │ +│ │ pull-secret │ │ status-reporter │ │ +│ │ (main container) │ │ (sidecar) │ │ +│ └─────────────────────┘ └─────────────────────┘ │ +│ │ │ │ +│ └───── shared volume ─────┘ │ +└──────────────────────────────────────────────────────────────────┘ +``` ## Prerequisites @@ -16,20 +37,28 @@ This Helm chart deploys the HyperFleet Pull Secret Adapter as a Kubernetes Job o --project=YOUR_PROJECT_ID ``` -3. **Workload Identity configured** - - Service Account: `your-service-account@your-project.iam.gserviceaccount.com` - - Workload Pool: `your-project.svc.id.goog` +3. **GCP Pub/Sub configured** + - Topic for adapter messages + - Subscription for the adapter + +4. **Workload Identity configured** + - Using `principalSet://` for Workload Identity Federation + - No ServiceAccount annotation required (modern approach) ## Installation ### Quick Start -Deploy with default values: +Deploy with required Pub/Sub configuration: ```bash -helm install pullsecret-job ./charts/pull-secret \ +helm install pull-secret-adapter ./charts/pull-secret \ --namespace hyperfleet-system \ - --create-namespace + --create-namespace \ + --set broker.googlepubsub.projectId=my-project \ + --set broker.googlepubsub.topic=hyperfleet-events \ + --set broker.googlepubsub.subscription=pull-secret-adapter-sub \ + --set hyperfleetApi.baseUrl=https://api.hyperfleet.example.com ``` ### Custom Values @@ -37,13 +66,14 @@ helm install pullsecret-job ./charts/pull-secret \ Deploy with custom configuration: ```bash -helm install pullsecret-job ./charts/pull-secret \ +helm install pull-secret-adapter ./charts/pull-secret \ --namespace hyperfleet-system \ --create-namespace \ - --set env.gcpProjectId=my-project \ - --set env.clusterId=my-cluster-123 \ - --set env.pullSecretData='{"auths":{...}}' \ - --set image.tag=latest + --set broker.googlepubsub.projectId=my-project \ + --set broker.googlepubsub.topic=hyperfleet-events \ + --set broker.googlepubsub.subscription=pull-secret-adapter-sub \ + --set hyperfleetApi.baseUrl=https://api.hyperfleet.example.com \ + --set pullSecretAdapter.image.tag=v1.0.0 ``` ### Using a Values File @@ -51,88 +81,165 @@ helm install pullsecret-job ./charts/pull-secret \ Create a custom values file (`my-values.yaml`): ```yaml -env: - gcpProjectId: "my-gcp-project" - clusterId: "my-cluster-123" - secretName: "hyperfleet-my-cluster-123-pull-secret" - pullSecretData: '{"auths":{"registry.example.com":{"auth":"...","email":"user@example.com"}}}' - -serviceAccount: - gcpServiceAccount: "my-service-account@my-project.iam.gserviceaccount.com" - -image: - tag: "v1.0.0" +broker: + type: "googlepubsub" + googlepubsub: + projectId: "my-gcp-project" + topic: "hyperfleet-events" + subscription: "pull-secret-adapter-sub" + +hyperfleetApi: + baseUrl: "https://api.hyperfleet.example.com" + version: "v1" + +pullSecretAdapter: + image: + tag: "v1.0.0" ``` Then install: ```bash -helm install pullsecret-job ./charts/pull-secret \ +helm install pull-secret-adapter ./charts/pull-secret \ --namespace hyperfleet-system \ --create-namespace \ -f my-values.yaml ``` +## Umbrella Chart Integration + +This chart supports integration with the `hyperfleet-chart` umbrella chart. + +### Adding to hyperfleet-chart + +In your umbrella chart's `Chart.yaml`: + +```yaml +dependencies: + - name: pull-secret + version: "0.1.0" + repository: "git+https://github.com/openshift-hyperfleet/adapter-pull-secret@charts/pull-secret?ref=main" + condition: pull-secret.enabled +``` + +### Global Image Override + +When deployed via umbrella chart, you can set a global image registry: + +```yaml +global: + image: + registry: "quay.io/my-org" # Overrides all subchart image registries + +pull-secret: + enabled: true + broker: + googlepubsub: + projectId: "my-project" + topic: "hyperfleet-events" + subscription: "pull-secret-adapter-sub" +``` + ## Configuration The following table lists the configurable parameters: | Parameter | Description | Default | |-----------|-------------|---------| -| `namespace` | Kubernetes namespace | `hyperfleet-system` | -| `job.name` | Job name | `pullsecret-job` | -| `job.backoffLimit` | Number of retries on failure | `3` | -| `job.ttlSecondsAfterFinished` | Cleanup delay after completion | `3600` (1 hour) | -| `image.repository` | Container image repository | `quay.io/hyperfleet/pull-secret` | -| `image.tag` | Container image tag | `latest` | +| **Global** | | | +| `global.image.registry` | Global image registry override | `""` | +| `nameOverride` | Override chart name | `""` | +| `fullnameOverride` | Override full release name | `""` | +| `replicaCount` | Number of adapter framework replicas | `1` | +| **Adapter Framework Image** | | | +| `image.registry` | Adapter framework image registry | `registry.ci.openshift.org` | +| `image.repository` | Adapter framework image repository | `ci/hyperfleet-adapter` | +| `image.tag` | Adapter framework image tag | `latest` | | `image.pullPolicy` | Image pull policy | `Always` | -| `serviceAccount.name` | Kubernetes ServiceAccount name | `pullsecret-adapter` | -| `serviceAccount.gcpServiceAccount` | GCP service account for Workload Identity | `your-service-account@your-project.iam.gserviceaccount.com` | -| `env.gcpProjectId` | GCP project ID | `your-gcp-project` | -| `env.clusterId` | Cluster identifier | `your-cluster-id` | -| `env.secretName` | Secret name in GCP Secret Manager | `hyperfleet-your-cluster-id-pull-secret` | -| `env.pullSecretData` | Pull secret JSON data (required) | `{"auths":{...}}` | -| `resources.requests.cpu` | CPU request | `100m` | -| `resources.requests.memory` | Memory request | `128Mi` | -| `resources.limits.cpu` | CPU limit | `500m` | -| `resources.limits.memory` | Memory limit | `512Mi` | +| `imagePullSecrets` | Image pull secrets | `[]` | +| **ServiceAccount** | | | +| `serviceAccount.create` | Create ServiceAccount | `true` | +| `serviceAccount.name` | ServiceAccount name | `""` (auto-generated) | +| `serviceAccount.annotations` | ServiceAccount annotations | `{}` | +| **RBAC** | | | +| `rbac.create` | Create RBAC resources | `true` | +| **Logging** | | | +| `logging.level` | Log level (debug, info, warn, error) | `info` | +| `logging.format` | Log format (text, json) | `text` | +| `logging.output` | Log output (stdout, stderr) | `stderr` | +| **Broker** | | | +| `broker.type` | Broker type (googlepubsub, rabbitmq) | `googlepubsub` | +| `broker.googlepubsub.projectId` | GCP project ID for Pub/Sub | `""` | +| `broker.googlepubsub.topic` | Pub/Sub topic name | `""` | +| `broker.googlepubsub.subscription` | Pub/Sub subscription name | `""` | +| `broker.googlepubsub.deadLetterTopic` | Dead letter topic (optional) | `""` | +| `broker.subscriber.parallelism` | Message processing parallelism | `1` | +| **HyperFleet API** | | | +| `hyperfleetApi.baseUrl` | HyperFleet API base URL | `""` | +| `hyperfleetApi.version` | HyperFleet API version | `v1` | +| **Pull Secret Adapter** | | | +| `pullSecretAdapter.image.registry` | Job container image registry | `quay.io/openshift-hyperfleet` | +| `pullSecretAdapter.image.repository` | Job container image repository | `pull-secret` | +| `pullSecretAdapter.image.tag` | Job container image tag | `latest` | +| `pullSecretAdapter.statusReporterImage` | Status reporter sidecar image | `registry.ci.openshift.org/ci/status-reporter:latest` | +| `pullSecretAdapter.resultsPath` | Shared result path | `/results/adapter-result.json` | +| `pullSecretAdapter.maxWaitTimeSeconds` | Max job wait time | `300` | +| `pullSecretAdapter.logLevel` | Job container log level | `info` | +| **Scheduling** | | | +| `nodeSelector` | Node selector | `{}` | +| `tolerations` | Tolerations | `[]` | +| `affinity` | Affinity rules | `{}` | +| `env` | Additional environment variables | `[]` | + +## How It Works + +1. **PubSub Message**: HyperFleet sends a message with cluster info (`clusterId`, `projectId`, `pullSecretData`) +2. **Adapter Framework**: Receives the message and creates a Job based on the AdapterConfig +3. **Pull Secret Job**: Stores the pull secret in GCP Secret Manager +4. **Status Reporter**: Reports job status back to HyperFleet API ## Usage ### Monitoring -Check job status: +Check deployment status: +```bash +helm status pull-secret-adapter -n hyperfleet-system +kubectl get deployment -n hyperfleet-system +kubectl get pods -n hyperfleet-system +``` + +View adapter logs: ```bash -helm status pullsecret-job -n hyperfleet-system -kubectl get job pullsecret-job -n hyperfleet-system +kubectl logs -f deployment/pull-secret-adapter -n hyperfleet-system ``` -View logs: +View job logs (for a specific cluster): ```bash -kubectl logs -f job/pullsecret-job -n hyperfleet-system +kubectl logs -f job/pull-secret-- -n ``` ### Upgrading -Upgrade the deployment with new values: +Upgrade the deployment: ```bash -helm upgrade pullsecret-job ./charts/pull-secret \ +helm upgrade pull-secret-adapter ./charts/pull-secret \ --namespace hyperfleet-system \ - --set image.tag=v1.1.0 + --set pullSecretAdapter.image.tag=v1.1.0 ``` ### Uninstalling -Remove the job: +Remove the adapter: ```bash -helm uninstall pullsecret-job -n hyperfleet-system +helm uninstall pull-secret-adapter -n hyperfleet-system ``` ## Dry Run Mode -Test without creating secrets: +Test without deploying: ```bash -helm install pullsecret-job ./charts/pull-secret \ +helm install pull-secret-adapter ./charts/pull-secret \ --namespace hyperfleet-system \ --dry-run --debug ``` @@ -141,26 +248,26 @@ helm install pullsecret-job ./charts/pull-secret \ ### View rendered templates ```bash -helm template pullsecret-job ./charts/pull-secret +helm template pull-secret-adapter ./charts/pull-secret ``` ### Check deployment issues ```bash -kubectl describe job pullsecret-job -n hyperfleet-system +kubectl describe deployment pull-secret-adapter -n hyperfleet-system kubectl get events -n hyperfleet-system --sort-by='.lastTimestamp' ``` ### Authentication errors -Verify Workload Identity binding: +Verify Workload Identity: ```bash # Check ServiceAccount -kubectl get sa pullsecret-adapter -n hyperfleet-system -o yaml +kubectl get sa -n hyperfleet-system -# Check GCP IAM binding -gcloud iam service-accounts get-iam-policy \ - your-service-account@your-project.iam.gserviceaccount.com \ - --project=your-project +# Verify IAM binding (using principalSet) +gcloud projects get-iam-policy YOUR_PROJECT_ID \ + --flatten="bindings[].members" \ + --filter="bindings.members:principalSet://" ``` ## Development @@ -191,3 +298,5 @@ helm package ./charts/pull-secret - [Helm Documentation](https://helm.sh/docs/) - [GKE Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) - [Kubernetes Jobs](https://kubernetes.io/docs/concepts/workloads/controllers/job/) +- [HyperFleet Chart](https://github.com/openshift-hyperfleet/hyperfleet-chart) +- [Adapter Framework Pattern](https://github.com/openshift-hyperfleet/adapter-validation-gcp) diff --git a/charts/pull-secret/configs/pull-secret-adapter.yaml b/charts/pull-secret/configs/pull-secret-adapter.yaml new file mode 100644 index 0000000..4ff6f79 --- /dev/null +++ b/charts/pull-secret/configs/pull-secret-adapter.yaml @@ -0,0 +1,105 @@ +# HyperFleet Pull Secret Adapter Configuration +# +# This adapter creates a job to store pull secrets in GCP Secret Manager +# for HyperFleet clusters. +apiVersion: hyperfleet.redhat.com/v1alpha1 +kind: AdapterConfig +metadata: + name: pull-secret-adapter + namespace: hyperfleet-system + labels: + hyperfleet.io/adapter-type: pull-secret + hyperfleet.io/component: adapter + hyperfleet.io/provider: gcp +spec: + adapter: + version: "0.1.0" + # ============================================================================ + # HyperFleet API Configuration + # ============================================================================ + hyperfleetApi: + timeout: 2s + retryAttempts: 3 + # ============================================================================ + # Kubernetes Configuration + # ============================================================================ + kubernetes: + apiVersion: "batch/v1" + # ============================================================================ + # Parameters + # ============================================================================ + params: + - name: "hyperfleetApiBaseUrl" + source: "env.HYPERFLEET_API_BASE_URL" + type: "string" + description: "Base URL for the HyperFleet API" + required: true + - name: "hyperfleetApiVersion" + source: "env.HYPERFLEET_API_VERSION" + type: "string" + default: "v1" + description: "API version to use" + - name: "clusterId" + source: "event.id" + type: "string" + description: "Unique identifier for the target cluster" + required: true + - name: "projectId" + source: "event.projectId" + type: "string" + description: "GCP project ID where secrets will be stored" + required: true + - name: "statusReporterImage" + source: "env.STATUS_REPORTER_IMAGE" + type: "string" + default: "registry.ci.openshift.org/ci/status-reporter:latest" + description: "Container image for the status reporter" + # Pull Secret Adapter configuration + - name: "pullSecretImage" + source: "env.PULL_SECRET_IMAGE" + type: "string" + default: "quay.io/openshift-hyperfleet/pull-secret:latest" + description: "Pull secret adapter container image" + - name: "pullSecretData" + source: "event.pullSecretData" + type: "string" + description: "Pull secret JSON data to store" + required: true + - name: "secretName" + source: "event.secretName" + type: "string" + description: "Secret name in GCP Secret Manager (optional, auto-generated if empty)" + - name: "resultPath" + source: "env.RESULTS_PATH" + type: "string" + default: "/results/adapter-result.json" + description: "Adapter shared result path with status reporter" + - name: "maxWaitTimeSeconds" + source: "env.MAX_WAIT_TIME_SECONDS" + type: "string" + default: "300" + description: "Maximum time to wait for job completion" + - name: "logLevel" + source: "env.LOG_LEVEL" + type: "string" + default: "info" + description: "Log level for containers (debug, info, warn, error)" + - name: "adapterTaskServiceAccount" + source: "env.ADAPTER_TASK_SERVICE_ACCOUNT" + type: "string" + default: "pull-secret-adapter-task-sa" + description: "Kubernetes ServiceAccount name for the adapter task" + - name: "managedByResourceName" + source: "env.MANAGED_BY_RESOURCE_NAME" + type: "string" + default: "pull-secret-adapter" + description: "The value for hyperfleet.io/managed-by" + # ============================================================================ + # Preconditions + # ============================================================================ + preconditions: [] + # ============================================================================ + # Resource Template + # ============================================================================ + resourceTemplate: + path: "/etc/adapter/job-template.yaml" diff --git a/charts/pull-secret/configs/pull-secret-job-adapter-task.yaml b/charts/pull-secret/configs/pull-secret-job-adapter-task.yaml new file mode 100644 index 0000000..83cdc5a --- /dev/null +++ b/charts/pull-secret/configs/pull-secret-job-adapter-task.yaml @@ -0,0 +1,132 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: "pull-secret-{{ .clusterId | lower }}-{{ .generationId }}" + namespace: "{{ .clusterId | lower }}" + labels: + hyperfleet.io/cluster-id: "{{ .clusterId }}" + hyperfleet.io/managed-by: "{{ .managedByResourceName }}" + hyperfleet.io/resource-type: "pull-secret-job" + app: pull-secret + annotations: + hyperfleet.io/generation: "{{ .generationId }}" +spec: + backoffLimit: 0 + # Maximum time to wait for k8s job completion (maxWaitTimeSeconds + 10 second buffer) + activeDeadlineSeconds: 310 + template: + metadata: + labels: + app: pull-secret + hyperfleet.io/cluster-id: "{{ .clusterId }}" + spec: + # Created before the job is created, as specified in the adapter configuration. + serviceAccountName: "{{ .adapterTaskServiceAccount }}" + restartPolicy: Never + securityContext: + fsGroup: 1000 + runAsNonRoot: true + runAsUser: 1000 + volumes: + - name: results + emptyDir: {} + # Required for readOnlyRootFilesystem: true - provides writable /tmp + - name: tmp + emptyDir: {} + containers: + # Pull Secret Adapter Container + - name: pull-secret + image: "{{ .pullSecretImage }}" + imagePullPolicy: Always + command: + - /usr/local/bin/pull-secret + args: + - run-job + - pull-secret + env: + # Required + - name: GCP_PROJECT_ID + value: "{{ .projectId }}" + - name: CLUSTER_ID + value: "{{ .clusterId }}" + - name: SECRET_NAME + value: "{{ if .secretName }}{{ .secretName }}{{ else }}hyperfleet-{{ .clusterId }}-pull-secret{{ end }}" + - name: PULL_SECRET_DATA + value: "{{ .pullSecretData }}" + - name: RESULTS_PATH + value: "{{ .resultPath }}" + # Logging + - name: LOG_LEVEL + value: "{{ .logLevel }}" + volumeMounts: + - name: results + mountPath: /results + - name: tmp + mountPath: /tmp + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "500m" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + capabilities: + drop: + - ALL + + # Status reporter sidecar + - name: status-reporter + image: "{{ .statusReporterImage }}" + imagePullPolicy: Always + env: + # Required environment variables + - name: JOB_NAME + value: "pull-secret-{{ .clusterId | lower }}-{{ .generationId }}" + - name: JOB_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + # Optional configuration + - name: RESULTS_PATH + value: "{{ .resultPath }}" + - name: POLL_INTERVAL_SECONDS + value: "2" + - name: MAX_WAIT_TIME_SECONDS + value: "{{ .maxWaitTimeSeconds }}" + - name: CONDITION_TYPE + value: "Available" + - name: LOG_LEVEL + value: "{{ .logLevel }}" + - name: ADAPTER_CONTAINER_NAME + value: "pull-secret" + volumeMounts: + - name: results + mountPath: /results + resources: + requests: + memory: "64Mi" + cpu: "100m" + limits: + memory: "128Mi" + cpu: "200m" + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + capabilities: + drop: + - ALL diff --git a/charts/pull-secret/templates/NOTES.txt b/charts/pull-secret/templates/NOTES.txt new file mode 100644 index 0000000..9181c14 --- /dev/null +++ b/charts/pull-secret/templates/NOTES.txt @@ -0,0 +1,91 @@ +============================================================ + HyperFleet Pull Secret Adapter +============================================================ + +The Pull Secret Adapter has been deployed successfully! + +Architecture: + - Adapter Framework (Deployment) listens to PubSub messages + - Creates Jobs dynamically to store pull secrets in GCP Secret Manager + - Status Reporter sidecar reports status back to HyperFleet API + +Release: {{ .Release.Name }} +Namespace: {{ .Release.Namespace }} + +============================================================ + COMPONENTS +============================================================ + +Adapter Framework Deployment: + Name: {{ include "pull-secret.fullname" . }} + Replicas: {{ .Values.replicaCount }} + +ServiceAccount: + Name: {{ include "pull-secret.serviceAccountName" . }} + +============================================================ + BROKER CONFIGURATION +============================================================ +{{- if .Values.broker.type }} +Broker Type: {{ .Values.broker.type }} +{{- if eq .Values.broker.type "googlepubsub" }} + Project ID: {{ .Values.broker.googlepubsub.projectId }} + Topic: {{ .Values.broker.googlepubsub.topic }} + Subscription: {{ .Values.broker.googlepubsub.subscription }} +{{- end }} +{{- else }} +WARNING: No broker configured! The adapter will not receive any messages. +{{- end }} + +============================================================ + IMAGES +============================================================ + +Adapter Framework: + {{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }} + +Pull Secret Job: + {{ include "pull-secret.adapterTaskImage" . }} + +Status Reporter: + {{ .Values.pullSecretAdapter.statusReporterImage }} + +============================================================ + MONITORING +============================================================ + +Check deployment status: + kubectl get deployment {{ include "pull-secret.fullname" . }} -n {{ .Release.Namespace }} + kubectl get pods -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "pull-secret.name" . }} + +View adapter logs: + kubectl logs -f deployment/{{ include "pull-secret.fullname" . }} -n {{ .Release.Namespace }} + +View dynamically created jobs: + kubectl get jobs -n -l hyperfleet.io/managed-by={{ include "pull-secret.fullname" . }} + +============================================================ + UPGRADE / UNINSTALL +============================================================ + +Upgrade: + helm upgrade {{ .Release.Name }} ./charts/pull-secret -n {{ .Release.Namespace }} + +Uninstall: + helm uninstall {{ .Release.Name }} -n {{ .Release.Namespace }} + +============================================================ + TROUBLESHOOTING +============================================================ + +1. Check adapter framework logs: + kubectl logs -f deployment/{{ include "pull-secret.fullname" . }} -n {{ .Release.Namespace }} + +2. Check PubSub subscription: + gcloud pubsub subscriptions describe {{ .Values.broker.googlepubsub.subscription }} --project={{ .Values.broker.googlepubsub.projectId }} + +3. Verify ServiceAccount permissions: + kubectl get clusterrolebinding {{ include "pull-secret.fullname" . }} -o yaml + +4. Check for events: + kubectl get events -n {{ .Release.Namespace }} --sort-by='.lastTimestamp' diff --git a/charts/pull-secret/templates/_helpers.tpl b/charts/pull-secret/templates/_helpers.tpl index f64b21c..6eb790e 100644 --- a/charts/pull-secret/templates/_helpers.tpl +++ b/charts/pull-secret/templates/_helpers.tpl @@ -7,6 +7,8 @@ Expand the name of the chart. {{/* Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. */}} {{- define "pull-secret.fullname" -}} {{- if .Values.fullnameOverride }} @@ -38,6 +40,8 @@ helm.sh/chart: {{ include "pull-secret.chart" . }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/component: adapter +hyperfleet.io/adapter-type: pull-secret {{- end }} {{/* @@ -46,12 +50,44 @@ Selector labels {{- define "pull-secret.selectorLabels" -}} app.kubernetes.io/name: {{ include "pull-secret.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} -app: {{ .Values.labels.app }} {{- end }} {{/* Create the name of the service account to use */}} {{- define "pull-secret.serviceAccountName" -}} -{{- default "pullsecret-adapter" .Values.serviceAccount.name }} +{{- if .Values.serviceAccount.create }} +{{- default (include "pull-secret.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Create the ConfigMap name for adapter configuration +*/}} +{{- define "pull-secret.configMapName" -}} +{{- printf "%s-config" (include "pull-secret.fullname" .) }} +{{- end }} + +{{/* +Create the ConfigMap name for broker configuration +*/}} +{{- define "pull-secret.brokerConfigMapName" -}} +{{- printf "%s-broker" (include "pull-secret.fullname" .) }} +{{- end }} + +{{/* +Create the adapter task (job) image reference with global override support. +*/}} +{{- define "pull-secret.adapterTaskImage" -}} +{{- $registry := .Values.pullSecretAdapter.image.registry }} +{{- if .Values.global }} +{{- if .Values.global.image }} +{{- if .Values.global.image.registry }} +{{- $registry = .Values.global.image.registry }} +{{- end }} +{{- end }} +{{- end }} +{{- printf "%s/%s:%s" $registry .Values.pullSecretAdapter.image.repository (.Values.pullSecretAdapter.image.tag | default .Chart.AppVersion) }} {{- end }} diff --git a/charts/pull-secret/templates/configmap-app.yaml b/charts/pull-secret/templates/configmap-app.yaml new file mode 100644 index 0000000..c0680bf --- /dev/null +++ b/charts/pull-secret/templates/configmap-app.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "pull-secret.configMapName" . }} + labels: + {{- include "pull-secret.labels" . | nindent 4 }} +data: + adapter.yaml: | +{{ .Files.Get "configs/pull-secret-adapter.yaml" | indent 4 }} + job-template.yaml: | +{{ .Files.Get "configs/pull-secret-job-adapter-task.yaml" | indent 4 }} diff --git a/charts/pull-secret/templates/configmap-broker.yaml b/charts/pull-secret/templates/configmap-broker.yaml new file mode 100644 index 0000000..de571b1 --- /dev/null +++ b/charts/pull-secret/templates/configmap-broker.yaml @@ -0,0 +1,30 @@ +{{- if .Values.broker.type }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "pull-secret.brokerConfigMapName" . }} + labels: + {{- include "pull-secret.labels" . | nindent 4 }} +data: + {{- if .Values.broker.yaml }} + broker.yaml: | +{{ .Values.broker.yaml | indent 4 }} + {{- else }} + broker.yaml: | + type: {{ .Values.broker.type }} + {{- if eq .Values.broker.type "googlepubsub" }} + googlepubsub: + projectId: {{ .Values.broker.googlepubsub.projectId | quote }} + topic: {{ .Values.broker.googlepubsub.topic | quote }} + subscription: {{ .Values.broker.googlepubsub.subscription | quote }} + {{- if .Values.broker.googlepubsub.deadLetterTopic }} + deadLetterTopic: {{ .Values.broker.googlepubsub.deadLetterTopic | quote }} + {{- end }} + {{- else if eq .Values.broker.type "rabbitmq" }} + rabbitmq: + url: {{ .Values.broker.rabbitmq.url | quote }} + {{- end }} + subscriber: + parallelism: {{ .Values.broker.subscriber.parallelism | default 1 }} + {{- end }} +{{- end }} diff --git a/charts/pull-secret/templates/deployment.yaml b/charts/pull-secret/templates/deployment.yaml new file mode 100644 index 0000000..6920873 --- /dev/null +++ b/charts/pull-secret/templates/deployment.yaml @@ -0,0 +1,141 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "pull-secret.fullname" . }} + labels: + {{- include "pull-secret.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "pull-secret.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap-app.yaml") . | sha256sum }} + {{- if .Values.broker.type }} + checksum/broker-config: {{ include (print $.Template.BasePath "/configmap-broker.yaml") . | sha256sum }} + {{- end }} + labels: + {{- include "pull-secret.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "pull-secret.serviceAccountName" . }} + securityContext: + fsGroup: 65532 + runAsNonRoot: true + runAsUser: 65532 + containers: + - name: {{ .Chart.Name }} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + seccompProfile: + type: RuntimeDefault + image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /app/adapter + args: + - serve + env: + - name: LOG_LEVEL + value: {{ .Values.logging.level | default "info" | quote }} + - name: LOG_FORMAT + value: {{ .Values.logging.format | default "text" | quote }} + - name: LOG_OUTPUT + value: {{ .Values.logging.output | default "stderr" | quote }} + {{- with .Values.env }} + {{- range . }} + - name: {{ .name }} + {{- if .value }} + value: {{ .value | quote }} + {{- else if .valueFrom }} + valueFrom: + {{- toYaml .valueFrom | nindent 16 }} + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.hyperfleetApi.baseUrl }} + - name: HYPERFLEET_API_BASE_URL + value: {{ .Values.hyperfleetApi.baseUrl | quote }} + {{- end }} + - name: HYPERFLEET_API_VERSION + value: {{ .Values.hyperfleetApi.version | default "v1" | quote }} + - name: ADAPTER_CONFIG_PATH + value: /etc/adapter/adapter.yaml + {{- if .Values.broker.type }} + - name: BROKER_CONFIG_FILE + value: /etc/broker/broker.yaml + {{- if eq .Values.broker.type "googlepubsub" }} + - name: BROKER_SUBSCRIPTION_ID + value: {{ .Values.broker.googlepubsub.subscription | quote }} + - name: BROKER_TOPIC + value: {{ .Values.broker.googlepubsub.topic | quote }} + - name: GCP_PROJECT_ID + value: {{ .Values.broker.googlepubsub.projectId | quote }} + {{- end }} + {{- end }} + # Pull Secret Adapter specific environment variables + - name: STATUS_REPORTER_IMAGE + value: {{ .Values.pullSecretAdapter.statusReporterImage | quote }} + - name: PULL_SECRET_IMAGE + value: {{ include "pull-secret.adapterTaskImage" . | quote }} + - name: RESULTS_PATH + value: {{ .Values.pullSecretAdapter.resultsPath | quote }} + - name: MAX_WAIT_TIME_SECONDS + value: {{ .Values.pullSecretAdapter.maxWaitTimeSeconds | quote }} + - name: MANAGED_BY_RESOURCE_NAME + value: {{ include "pull-secret.fullname" . | quote }} + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 100m + memory: 128Mi + volumeMounts: + - name: tmp + mountPath: /tmp + - name: adapter-config + mountPath: /etc/adapter + readOnly: true + {{- if .Values.broker.type }} + - name: broker-config + mountPath: /etc/broker/broker.yaml + subPath: broker.yaml + readOnly: true + {{- end }} + volumes: + # Required for readOnlyRootFilesystem: true - provides writable /tmp + - name: tmp + emptyDir: {} + - name: adapter-config + configMap: + name: {{ include "pull-secret.configMapName" . }} + {{- if .Values.broker.type }} + - name: broker-config + configMap: + name: {{ include "pull-secret.brokerConfigMapName" . }} + items: + - key: broker.yaml + path: broker.yaml + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/pull-secret/templates/job.yaml b/charts/pull-secret/templates/job.yaml deleted file mode 100644 index 4c19d49..0000000 --- a/charts/pull-secret/templates/job.yaml +++ /dev/null @@ -1,35 +0,0 @@ -apiVersion: batch/v1 -kind: Job -metadata: - name: {{ .Values.job.name }} - namespace: {{ .Values.namespace }} - labels: - {{- include "pull-secret.labels" . | nindent 4 }} - job-type: {{ .Values.labels.jobType }} -spec: - backoffLimit: {{ .Values.job.backoffLimit }} - ttlSecondsAfterFinished: {{ .Values.job.ttlSecondsAfterFinished }} - template: - metadata: - labels: - {{- include "pull-secret.selectorLabels" . | nindent 8 }} - spec: - serviceAccountName: {{ include "pull-secret.serviceAccountName" . }} - restartPolicy: {{ .Values.job.restartPolicy }} - containers: - - name: pull-secret - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - env: - - name: GCP_PROJECT_ID - value: {{ .Values.env.gcpProjectId | quote }} - - name: CLUSTER_ID - value: {{ .Values.env.clusterId | quote }} - - name: SECRET_NAME - value: {{ .Values.env.secretName | quote }} - - name: PULL_SECRET_DATA - value: {{ .Values.env.pullSecretData | quote }} - resources: - {{- toYaml .Values.resources | nindent 10 }} - securityContext: - {{- toYaml .Values.securityContext | nindent 10 }} diff --git a/charts/pull-secret/templates/rbac.yaml b/charts/pull-secret/templates/rbac.yaml new file mode 100644 index 0000000..188ad9f --- /dev/null +++ b/charts/pull-secret/templates/rbac.yaml @@ -0,0 +1,60 @@ +{{- if .Values.rbac.create }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "pull-secret.fullname" . }} + labels: + {{- include "pull-secret.labels" . | nindent 4 }} +rules: + # Read namespaces to verify target namespace exists + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list", "watch"] + + # Manage ServiceAccounts for adapter task jobs + - apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + + # Read pods to check adapter task job pod status + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list", "watch"] + + # Manage RBAC for adapter task jobs (Role and RoleBinding) + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles", "rolebindings"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + + # Manage adapter task Jobs + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + + # Read and update Job status (needed by status reporter sidecar) + - apiGroups: ["batch"] + resources: ["jobs/status"] + verbs: ["get", "update", "patch"] + + # Secret management permissions - required for pull secret operations + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch"] + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "pull-secret.fullname" . }} + labels: + {{- include "pull-secret.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "pull-secret.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "pull-secret.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/pull-secret/templates/serviceaccount.yaml b/charts/pull-secret/templates/serviceaccount.yaml index 70a85aa..fdfa79d 100644 --- a/charts/pull-secret/templates/serviceaccount.yaml +++ b/charts/pull-secret/templates/serviceaccount.yaml @@ -1,9 +1,12 @@ +{{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "pull-secret.serviceAccountName" . }} - namespace: {{ .Values.namespace }} labels: {{- include "pull-secret.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} annotations: - iam.gke.io/gcp-service-account: {{ .Values.serviceAccount.gcpServiceAccount }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/pull-secret/values.yaml b/charts/pull-secret/values.yaml index 326db90..15d6a64 100644 --- a/charts/pull-secret/values.yaml +++ b/charts/pull-secret/values.yaml @@ -1,76 +1,139 @@ -# Default values for pull-secret adapter +# Pull Secret Adapter - Helm Values +# +# This adapter manages pull secrets in GCP Secret Manager for HyperFleet clusters. +# It uses the Adapter Framework to listen to PubSub messages and create jobs. +# +# For umbrella chart integration, set global.image.registry to override image registry. -# Namespace where the job will be deployed -namespace: hyperfleet-system +# ============================================================================= +# Global Overrides (for umbrella chart integration) +# ============================================================================= +# When deployed as part of hyperfleet-chart umbrella, these can be set globally +global: + image: + registry: "" # Override registry for all images (e.g., "quay.io/croche") -# Job configuration -job: - # Name of the Kubernetes Job - name: pullsecret-job +# ============================================================================= +# Chart Identity +# ============================================================================= +nameOverride: "" +fullnameOverride: "" - # Number of times to retry the job on failure - backoffLimit: 3 +replicaCount: 1 - # Time in seconds after job completion before cleanup (1 hour) - ttlSecondsAfterFinished: 3600 - - # Job restart policy - restartPolicy: Never - -# Image configuration +# ============================================================================= +# Image Configuration (Adapter Framework) +# ============================================================================= image: - repository: quay.io/hyperfleet/pull-secret - tag: latest + # Adapter Framework image + registry: registry.ci.openshift.org + repository: ci/hyperfleet-adapter + tag: "latest" pullPolicy: Always -# ServiceAccount configuration +imagePullSecrets: [] + +# ============================================================================= +# ServiceAccount Configuration +# ============================================================================= serviceAccount: - # Name of the Kubernetes ServiceAccount - name: pullsecret-adapter - - # GCP service account email for Workload Identity - # IMPORTANT: Replace with your actual GCP service account - gcpServiceAccount: your-service-account@your-project.iam.gserviceaccount.com - -# Environment variables -env: - # GCP project ID where secrets will be stored - # IMPORTANT: Replace with your actual GCP project ID - gcpProjectId: "your-gcp-project" - - # Cluster identifier - # IMPORTANT: Replace with your actual cluster ID - clusterId: "your-cluster-id" - - # Secret name in GCP Secret Manager (auto-derived if not provided) - # This will be auto-generated as: hyperfleet-{clusterId}-pull-secret - secretName: "hyperfleet-your-cluster-id-pull-secret" - - # Pull secret JSON data (must be provided) - # IMPORTANT: Replace with your actual pull secret data - # This is a dummy example - use your real OpenShift pull secret - pullSecretData: '{"auths":{"registry.example.com":{"auth":"ZXhhbXBsZTpleGFtcGxl","email":"user@example.com"}}}' - -# Resource limits -resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 500m - memory: 512Mi - -# Security context -securityContext: - runAsNonRoot: true - runAsUser: 1000 - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - capabilities: - drop: - - ALL - -# Labels -labels: - app: pullsecret-adapter - jobType: pull-secret + # Create a ServiceAccount for the adapter framework deployment + create: true + # Name of the ServiceAccount (auto-generated if empty) + name: "" + # Annotations (optional, only needed for legacy GSA-based Workload Identity) + # Modern approach uses direct principal binding - no annotation needed + # Legacy: iam.gke.io/gcp-service-account: GSA_NAME@PROJECT_ID.iam.gserviceaccount.com + annotations: {} + +# ============================================================================= +# RBAC Configuration +# ============================================================================= +rbac: + # Create RBAC resources (ClusterRole and ClusterRoleBinding) + create: true + # When enabled, grants minimal permissions needed for adapter: + # - Read namespaces + # - Manage ServiceAccounts, Roles, RoleBindings, Jobs + # - Read pods and update Job status + +# ============================================================================= +# Logging Configuration +# ============================================================================= +logging: + level: info # Options: debug, info, warn, error + format: text # Options: text, json + output: stderr # Options: stdout, stderr + +# ============================================================================= +# Broker Configuration (required for adapter to function) +# ============================================================================= +broker: + type: "googlepubsub" # "googlepubsub" or "rabbitmq" + + # Google Pub/Sub (required when type: googlepubsub) + googlepubsub: + projectId: "" # GCP project ID for Pub/Sub + topic: "" # Pub/Sub topic name + subscription: "" # Pub/Sub subscription name + deadLetterTopic: "" # Optional: Dead letter topic + + # RabbitMQ (required when type: rabbitmq) + rabbitmq: + url: "" # amqp://user:pass@host:5672/ + + subscriber: + parallelism: 1 + + # Raw YAML override (bypasses structured config above) + yaml: "" + +# ============================================================================= +# HyperFleet API Configuration +# ============================================================================= +hyperfleetApi: + # Base URL for HyperFleet API (for status reporting) + baseUrl: "" + # API version + version: "v1" + +# ============================================================================= +# Pull Secret Adapter Configuration +# ============================================================================= +pullSecretAdapter: + # Pull Secret adapter task image + image: + registry: quay.io/openshift-hyperfleet + repository: pull-secret + tag: "latest" + + # Status reporter sidecar image + statusReporterImage: "registry.ci.openshift.org/ci/status-reporter:latest" + + # Shared result path for status reporter + resultsPath: "/results/adapter-result.json" + + # Maximum time to wait for job completion (seconds) + maxWaitTimeSeconds: "300" + + # Log level for job containers + logLevel: "info" + +# ============================================================================= +# Platform-specific Scheduling +# ============================================================================= +nodeSelector: {} +tolerations: [] +affinity: {} + +# ============================================================================= +# Additional Environment Variables +# ============================================================================= +env: [] + # - name: MY_VAR + # value: "my-value" + # - name: MY_SECRET + # valueFrom: + # secretKeyRef: + # name: my-secret + # key: key