From 3443e4ed929f50a1b379caf4589fa4acad049397 Mon Sep 17 00:00:00 2001 From: Min Zhang Date: Tue, 10 Feb 2026 14:56:55 -0500 Subject: [PATCH] feat: Externalize ZTVP charts: quay-registry Signed-off-by: Min Zhang --- .github/linters/.checkov.yaml | 23 +++- .github/workflows/superlinter.yml | 2 + .prettierignore | 2 + .trivyignore | 8 ++ .yamllint | 12 +++ Chart.yaml | 14 ++- Makefile | 12 ++- README.md | 51 ++++++++- README.md.gotmpl | 6 ++ templates/.keep | 0 templates/object-bucket-claim.yaml | 11 ++ templates/quay-config-bundle-secret.yaml | 45 ++++++++ templates/quay-registry.yaml | 38 +++++++ templates/quay-s3-credentials-job.yaml | 112 ++++++++++++++++++++ templates/quay-s3-setup-serviceaccount.yaml | 81 ++++++++++++++ values.yaml | 42 +++++++- 16 files changed, 440 insertions(+), 19 deletions(-) create mode 100644 .prettierignore create mode 100644 .trivyignore create mode 100644 .yamllint delete mode 100644 templates/.keep create mode 100644 templates/object-bucket-claim.yaml create mode 100644 templates/quay-config-bundle-secret.yaml create mode 100644 templates/quay-registry.yaml create mode 100644 templates/quay-s3-credentials-job.yaml create mode 100644 templates/quay-s3-setup-serviceaccount.yaml diff --git a/.github/linters/.checkov.yaml b/.github/linters/.checkov.yaml index fe4590f..d9415e5 100644 --- a/.github/linters/.checkov.yaml +++ b/.github/linters/.checkov.yaml @@ -5,8 +5,21 @@ directory: skip-path: - tests skip-check: - - CKV_K8S_49 # Minimize wildcard use in Roles and ClusterRoles - - CKV_K8S_155 # Minimize ClusterRoles that grant control over validating or mutating admission webhook configurations - - CKV_K8S_156 # Minimize ClusterRoles that grant permissions to approve CertificateSigningRequests - - CKV_K8S_157 # Minimize Roles and ClusterRoles that grant permissions to bind RoleBindings or ClusterRoleBindings - - CKV_K8S_158 # Minimize Roles and ClusterRoles that grant permissions to escalate Roles or ClusterRoles + # CKV_K8S_49: Minimize wildcard use in Roles and ClusterRoles + - CKV_K8S_49 + # CKV_K8S_155: ClusterRoles for admission webhook configurations + - CKV_K8S_155 + # CKV_K8S_156: ClusterRoles to approve CertificateSigningRequests + - CKV_K8S_156 + # CKV_K8S_157: Roles/ClusterRoles to bind RoleBindings or ClusterRoleBindings + - CKV_K8S_157 + # CKV_K8S_158: Roles/ClusterRoles to escalate Roles or ClusterRoles + - CKV_K8S_158 + # CKV_K8S_38: S3 setup job needs SA token for oc CLI + - CKV_K8S_38 + # CKV_K8S_40: Job uses OpenShift namespace default UID (high UID assigned at runtime, no anyuid needed) + - CKV_K8S_40 + # CKV_K8S_43: Image digest; chart uses tag, digest is deployment-specific + - CKV_K8S_43 + # CKV_SECRET_6: False positives - templates use placeholders, script refs to cluster secrets only + - CKV_SECRET_6 diff --git a/.github/workflows/superlinter.yml b/.github/workflows/superlinter.yml index bb67637..9a7b27e 100644 --- a/.github/workflows/superlinter.yml +++ b/.github/workflows/superlinter.yml @@ -14,3 +14,5 @@ jobs: with: sl_env: | VALIDATE_BIOME_FORMAT=false + # Exclude Helm chart templates (contain {{ }}; not valid YAML) + FILTER_REGEX_EXCLUDE=.*/templates/.* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f1f03b3 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +# Helm template files contain {{ }} and are not plain YAML +templates/ diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 0000000..f012e06 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,8 @@ +# AVD-KSV-0125: registry.redhat.io trusted; S3 job uses ose-cli from Red Hat +AVD-KSV-0125 +# AVD-KSV-0113: S3 setup Role intentionally needs secret get/create/patch for quay config bundle +AVD-KSV-0113 +# AVD-KSV-0020: Job uses OpenShift namespace default UID at runtime (high UID, no anyuid needed) +AVD-KSV-0020 +# AVD-KSV-0021: Job uses OpenShift namespace default GID at runtime +AVD-KSV-0021 diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..0ea5d73 --- /dev/null +++ b/.yamllint @@ -0,0 +1,12 @@ +extends: default +ignore: + - templates/ + - templates/** + - "**/templates/**" +rules: + document-start: disable + line-length: + max: 80 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 1 diff --git a/Chart.yaml b/Chart.yaml index 4fa33cc..278cc40 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -1,6 +1,10 @@ apiVersion: v2 -description: A Helm chart to serve as the Validated Patterns Template -keywords: - - pattern -name: vp-template -version: 0.0.1 +name: quay-registry +description: Red Hat Quay Registry Resources +type: application +version: 0.1.0 +appVersion: "3.9" +home: https://github.com/validatedpatterns/quay-chart +maintainers: + - name: Zero Trust Validated Patterns Team + email: ztvp-arch-group@redhat.com diff --git a/Makefile b/Makefile index 319317a..f0ffb4c 100644 --- a/Makefile +++ b/Makefile @@ -36,8 +36,10 @@ test: helm-lint helm-unittest ## Runs helm lint and unit tests .PHONY: super-linter super-linter: ## Runs super linter locally rm -rf .mypy_cache - podman run -e RUN_LOCAL=true -e USE_FIND_ALGORITHM=true \ - -e VALIDATE_BIOME_FORMAT=false \ - -v $(PWD):/tmp/lint:rw,z \ - -w /tmp/lint \ - ghcr.io/super-linter/super-linter:slim-v8 + podman run -e RUN_LOCAL=true -e USE_FIND_ALGORITHM=true \ + -e VALIDATE_BIOME_FORMAT=false \ + -e "FILTER_REGEX_EXCLUDE=.*/templates/.*" \ + -e VALIDATE_GITHUB_ACTIONS_ZIZMOR=false \ + -v $(PWD):/tmp/lint:rw,z \ + -w /tmp/lint \ + ghcr.io/super-linter/super-linter:slim-v8 diff --git a/README.md b/README.md index b7aa861..895e2f0 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,58 @@ -# vp-template +# quay-registry -![Version: 0.0.1](https://img.shields.io/badge/Version-0.0.1-informational?style=flat-square) + -A Helm chart to serve as the Validated Patterns Template +![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 3.9](https://img.shields.io/badge/AppVersion-3.9-informational?style=flat-square) + + + + + +Red Hat Quay Registry Resources + + This chart is used to serve as the template for Validated Patterns Charts ## Notable changes +**Homepage:** + +## Maintainers + +| Name | Email | Url | +| ---------------------------------- | ---------------------------- | --- | +| Zero Trust Validated Patterns Team | | | + + + +## Values + +| Key | Type | Default | Description | +| -------------------------------------------- | ------ | ---------------------------------------------- | ----------- | +| job.image | string | `"registry.redhat.io/openshift4/ose-cli:4.15"` | | +| job.resources.limits.cpu | string | `"500m"` | | +| job.resources.limits.memory | string | `"256Mi"` | | +| job.resources.requests.cpu | string | `"50m"` | | +| job.resources.requests.memory | string | `"128Mi"` | | +| objectStorage.objectBucketClaim.bucketName | string | `"quay-datastore"` | | +| objectStorage.objectBucketClaim.name | string | `"quay-bucket"` | | +| objectStorage.objectBucketClaim.storageClass | string | `"openshift-storage.noobaa.io"` | | +| quay.configBundleSecret.deploy | bool | `true` | | +| quay.configBundleSecret.name | string | `"quay-init-config-bundle-secret"` | | +| quay.namespace | string | `"quay-enterprise"` | | +| quay.setup.admin.email | string | `"quayadmin@example.com"` | | +| quay.setup.admin.name | string | `"quayadmin"` | | +| quay.setup.user.email | string | `"developer1@myorg.com"` | | +| quay.setup.user.name | string | `"developer1"` | | +| quay.storage.clairpostgres.size | string | `"50Gi"` | | +| quay.storage.postgres.size | string | `"50Gi"` | | +| quay_config.org.email | string | `"devel@myorg.com"` | | +| quay_config.org.name | string | `"devel"` | | +| quay_config.repo | string | `"example"` | | + + + --- Autogenerated from chart metadata using [helm-docs v1.14.2](https://github.com/norwoodj/helm-docs/releases/v1.14.2) diff --git a/README.md.gotmpl b/README.md.gotmpl index bf3762f..c426f84 100644 --- a/README.md.gotmpl +++ b/README.md.gotmpl @@ -1,9 +1,13 @@ {{ template "chart.header" . }} {{ template "chart.deprecationWarning" . }} + {{ template "chart.badgesSection" . }} + + {{ template "chart.description" . }} + This chart is used to serve as the template for Validated Patterns Charts @@ -17,6 +21,8 @@ This chart is used to serve as the template for Validated Patterns Charts {{ template "chart.requirementsSection" . }} + {{ template "chart.valuesSection" . }} + {{ template "helm-docs.versionFooter" . }} diff --git a/templates/.keep b/templates/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/templates/object-bucket-claim.yaml b/templates/object-bucket-claim.yaml new file mode 100644 index 0000000..5965a5c --- /dev/null +++ b/templates/object-bucket-claim.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: objectbucket.io/v1alpha1 +kind: ObjectBucketClaim +metadata: + name: {{ .Values.objectStorage.objectBucketClaim.name }} + namespace: {{ .Values.quay.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "5" # Create OBC after NooBaa system is ready +spec: + generateBucketName: {{ .Values.objectStorage.objectBucketClaim.bucketName }} + storageClassName: {{ .Values.objectStorage.objectBucketClaim.storageClass }} diff --git a/templates/quay-config-bundle-secret.yaml b/templates/quay-config-bundle-secret.yaml new file mode 100644 index 0000000..bcec831 --- /dev/null +++ b/templates/quay-config-bundle-secret.yaml @@ -0,0 +1,45 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Values.quay.configBundleSecret.name }} + namespace: {{ .Values.quay.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "7" # Layer 1: Create base config secret (template) +type: Opaque +stringData: + config.yaml: | + FEATURE_USER_INITIALIZE: true + FEATURE_GENERAL_OCI_SUPPORT: true + BROWSER_API_CALLS_XHR_ONLY: false + SUPER_USERS: + - {{ .Values.quay.setup.admin.name }} + FEATURE_USER_CREATION: true + ALLOW_PULLS_WITHOUT_STRICT_LOGGING: false + AUTHENTICATION_TYPE: Database + DEFAULT_TAG_EXPIRATION: 2w + ENTERPRISE_LOGO_URL: /static/img/RH_Logo_Quay_Black_UX-horizontal.svg + FEATURE_BUILD_SUPPORT: false + FEATURE_DIRECT_LOGIN: true + FEATURE_MAILING: false + REGISTRY_TITLE: Red Hat Quay + REGISTRY_TITLE_SHORT: Quay + TEAM_RESYNC_STALE_TIME: 60m + TESTING: false + CREATE_NAMESPACE_ON_PUSH: true + # Proxy all storage traffic through Quay instead of direct client access + FEATURE_PROXY_STORAGE: true + # NooBaa Multicloud Object Gateway (MCG) S3-compatible storage configuration + # This is a template - actual secret with real credentials is created by quay-s3-credentials-setup job + DISTRIBUTED_STORAGE_CONFIG: + default: + - RHOCSStorage + - hostname: s3.openshift-storage.svc.cluster.local + port: 443 + is_secure: true + storage_path: /datastorage/registry + access_key: PLACEHOLDER_ACCESS_KEY + secret_key: PLACEHOLDER_SECRET_KEY + bucket_name: PLACEHOLDER_BUCKET_NAME + DISTRIBUTED_STORAGE_DEFAULT_LOCATIONS: [] + DISTRIBUTED_STORAGE_PREFERENCE: + - default diff --git a/templates/quay-registry.yaml b/templates/quay-registry.yaml new file mode 100644 index 0000000..4a7f06f --- /dev/null +++ b/templates/quay-registry.yaml @@ -0,0 +1,38 @@ +apiVersion: quay.redhat.com/v1 +kind: QuayRegistry +metadata: + name: quay-registry + namespace: {{ .Values.quay.namespace | default "quay-enterprise" }} + annotations: + argocd.argoproj.io/sync-wave: "10" # Layer 1: Deploy Quay Registry + argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true +spec: + components: + - kind: clair + managed: true + - kind: horizontalpodautoscaler + managed: true + - kind: mirror + managed: true + - kind: monitoring + managed: true + - kind: postgres + managed: true + overrides: + volumeSize: {{ .Values.quay.storage.postgres.size }} + - kind: redis + managed: true + - kind: objectstorage + managed: false + - kind: route + managed: true + - kind: tls + managed: true + - kind: quay + managed: true + - kind: clairpostgres + managed: true + overrides: + volumeSize: {{ .Values.quay.storage.clairpostgres.size }} + # Use the secret created by the S3 job (with real credentials), not the Git-managed template + configBundleSecret: quay-config-with-s3 diff --git a/templates/quay-s3-credentials-job.yaml b/templates/quay-s3-credentials-job.yaml new file mode 100644 index 0000000..51a515e --- /dev/null +++ b/templates/quay-s3-credentials-job.yaml @@ -0,0 +1,112 @@ +--- +apiVersion: batch/v1 +kind: Job +metadata: + name: quay-s3-credentials-setup + namespace: {{ .Values.quay.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "8" # Layer 1: Setup S3 credentials +spec: + template: + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + serviceAccountName: quay-s3-setup + # Job needs SA token for oc CLI (CKV_K8S_38 skipped via checkov config) + automountServiceAccountToken: true + containers: + - name: setup-s3-credentials + image: {{ .Values.job.image | default "registry.redhat.io/openshift4/ose-cli:4.15" }} + imagePullPolicy: Always + securityContext: + allowPrivilegeEscalation: false + runAsNonRoot: true + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + resources: + {{- with .Values.job.resources }} + {{- toYaml . | nindent 10 }} + {{- else }} + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + {{- end }} + volumeMounts: + - name: tmp + mountPath: /tmp + command: + - /bin/bash + - -c + - | + set -e + echo "Setting up S3 credentials for Quay from NooBaa MCG ObjectBucketClaim..." + + echo "Using oc for Kubernetes API access..." + + # Check if ObjectBucketClaim exists and is bound + echo "Checking ObjectBucketClaim quay-bucket status..." + oc get objectbucketclaim quay-bucket -n {{ .Values.quay.namespace }} + + # Wait for ObjectBucketClaim to be in Bound state + echo "Waiting for ObjectBucketClaim quay-bucket to be Bound (timeout: 10 minutes)..." + oc wait --for=jsonpath='{.status.phase}'=Bound objectbucketclaim/quay-bucket -n {{ .Values.quay.namespace }} --timeout=600s || { + echo "ERROR: ObjectBucketClaim failed to reach Bound state within timeout" + oc describe objectbucketclaim quay-bucket -n {{ .Values.quay.namespace }} + exit 1 + } + + # Use the actual secret and configmap names (not the objectBucketName from spec) + CONFIG_MAP="quay-bucket" + SECRET_NAME="quay-bucket" + + echo "ConfigMap: $CONFIG_MAP" + echo "Secret: $SECRET_NAME" + + # Extract S3 credentials from Quay namespace + ACCESS_KEY=$(oc get secret $SECRET_NAME -n {{ .Values.quay.namespace }} -o jsonpath='{.data.AWS_ACCESS_KEY_ID}' | base64 -d) + SECRET_KEY=$(oc get secret $SECRET_NAME -n {{ .Values.quay.namespace }} -o jsonpath='{.data.AWS_SECRET_ACCESS_KEY}' | base64 -d) + BUCKET_NAME=$(oc get configmap $CONFIG_MAP -n {{ .Values.quay.namespace }} -o jsonpath='{.data.BUCKET_NAME}') + S3_ENDPOINT=$(oc get configmap $CONFIG_MAP -n {{ .Values.quay.namespace }} -o jsonpath='{.data.BUCKET_HOST}') + + echo "Retrieved S3 credentials successfully" + echo "Bucket: $BUCKET_NAME" + echo "Endpoint: $S3_ENDPOINT" + + # With FEATURE_PROXY_STORAGE enabled, all traffic goes through Quay proxy + echo "Using RHOCSStorage with proxy mode - clients will access storage through Quay" + + # Get the template config secret (with placeholders) + oc get secret {{ .Values.quay.configBundleSecret.name }} -n {{ .Values.quay.namespace }} -o jsonpath='{.data.config\.yaml}' | base64 -d > /tmp/config.yaml + + # Replace placeholders with actual values using a different delimiter to handle special characters + sed -i "s|PLACEHOLDER_ACCESS_KEY|$ACCESS_KEY|g" /tmp/config.yaml + sed -i "s|PLACEHOLDER_SECRET_KEY|$SECRET_KEY|g" /tmp/config.yaml + sed -i "s|PLACEHOLDER_BUCKET_NAME|$BUCKET_NAME|g" /tmp/config.yaml + + # Note: With FEATURE_PROXY_STORAGE enabled, we keep internal hostname since all traffic goes through Quay proxy + + echo "Updated Quay configuration to use RHOCSStorage with proxy mode" + + # Create a NEW secret with the actual credentials (not modifying the Git-managed one) + echo "Creating quay-config-with-s3 secret with real credentials..." + oc create secret generic quay-config-with-s3 \ + --from-file=config.yaml=/tmp/config.yaml \ + -n {{ .Values.quay.namespace }} \ + --dry-run=client -o yaml | oc apply -f - + + echo "Quay S3 credentials setup completed successfully" + echo "Created quay-config-with-s3 secret with real S3 credentials" + volumes: + - name: tmp + emptyDir: {} + restartPolicy: OnFailure + backoffLimit: 5 diff --git a/templates/quay-s3-setup-serviceaccount.yaml b/templates/quay-s3-setup-serviceaccount.yaml new file mode 100644 index 0000000..ec81fb6 --- /dev/null +++ b/templates/quay-s3-setup-serviceaccount.yaml @@ -0,0 +1,81 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: quay-s3-setup + namespace: {{ .Values.quay.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "6" # Layer 1: Create ServiceAccount/RBAC + labels: + app: quay-s3-setup +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: quay-s3-setup + namespace: {{ .Values.quay.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "6" # Layer 1: Create ServiceAccount/RBAC +rules: +# Read template secret and create new secret with real credentials in quay namespace +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create", "patch", "update"] + resourceNames: ["quay-init-config-bundle-secret", "quay-config-with-s3"] +# Need to create quay-config-with-s3 secret without resourceName restriction +- apiGroups: [""] + resources: ["secrets"] + verbs: ["create"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: quay-s3-setup-quay-resources + namespace: {{ .Values.quay.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "6" # Layer 1: Create ServiceAccount/RBAC +rules: +# ObjectBucketClaim access - OBC is in quay namespace +- apiGroups: ["objectbucket.io"] + resources: ["objectbucketclaims"] + verbs: ["get", "list", "watch"] +# Secrets access for S3 credentials in quay namespace +- apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch"] +# ConfigMaps access for S3 endpoint info in quay namespace +- apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: quay-s3-setup + namespace: {{ .Values.quay.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "6" # Layer 1: Create ServiceAccount/RBAC +subjects: +- kind: ServiceAccount + name: quay-s3-setup + namespace: {{ .Values.quay.namespace }} +roleRef: + kind: Role + name: quay-s3-setup + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: quay-s3-setup-quay-resources + namespace: {{ .Values.quay.namespace }} + annotations: + argocd.argoproj.io/sync-wave: "6" # Layer 1: Create ServiceAccount/RBAC +subjects: +- kind: ServiceAccount + name: quay-s3-setup + namespace: {{ .Values.quay.namespace }} +roleRef: + kind: Role + name: quay-s3-setup-quay-resources + apiGroup: rbac.authorization.k8s.io diff --git a/values.yaml b/values.yaml index ed97d53..0363056 100644 --- a/values.yaml +++ b/values.yaml @@ -1 +1,41 @@ ---- +quay: + namespace: quay-enterprise + configBundleSecret: + deploy: true + name: quay-init-config-bundle-secret + setup: + admin: + name: quayadmin + email: quayadmin@example.com + user: + name: developer1 + email: developer1@myorg.com + storage: + postgres: + size: 50Gi # Default and minimum size is 50 Gi + clairpostgres: + size: 50Gi # Default and minimum size is 50 Gi + +job: + # Use a version tag; avoid :latest for production (CKV_K8S_14) + image: registry.redhat.io/openshift4/ose-cli:4.15 + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + +quay_config: + org: + name: devel + email: devel@myorg.com + repo: example + +# Object Storage configuration - using NooBaa MCG +objectStorage: + objectBucketClaim: + name: quay-bucket + bucketName: quay-datastore + storageClass: openshift-storage.noobaa.io