{"id":36753792,"url":"https://github.com/openshift-hyperfleet/adapter-pull-secret","last_synced_at":"2026-01-20T17:17:43.296Z","repository":{"id":328153276,"uuid":"1112462728","full_name":"openshift-hyperfleet/adapter-pull-secret","owner":"openshift-hyperfleet","description":null,"archived":false,"fork":false,"pushed_at":"2026-01-12T02:38:27.000Z","size":76,"stargazers_count":0,"open_issues_count":1,"forks_count":6,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-12T08:05:30.641Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/openshift-hyperfleet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-08T16:54:30.000Z","updated_at":"2026-01-12T02:38:32.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/openshift-hyperfleet/adapter-pull-secret","commit_stats":null,"previous_names":["openshift-hyperfleet/adapter-pull-secret"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/openshift-hyperfleet/adapter-pull-secret","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openshift-hyperfleet%2Fadapter-pull-secret","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openshift-hyperfleet%2Fadapter-pull-secret/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openshift-hyperfleet%2Fadapter-pull-secret/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openshift-hyperfleet%2Fadapter-pull-secret/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/openshift-hyperfleet","download_url":"https://codeload.github.com/openshift-hyperfleet/adapter-pull-secret/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openshift-hyperfleet%2Fadapter-pull-secret/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28607625,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T16:10:39.856Z","status":"ssl_error","status_checked_at":"2026-01-20T16:10:39.493Z","response_time":117,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-12T12:47:39.686Z","updated_at":"2026-01-20T17:17:43.281Z","avatar_url":"https://github.com/openshift-hyperfleet.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# HyperFleet MVP - Pull Secret Job\n\nThis MVP demonstrates the Pull Secret Job that stores OpenShift cluster pull secrets in Google Cloud Platform (GCP) Secret Manager.\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Prerequisites](#prerequisites)\n- [Building the Project](#building-the-project)\n- [Building Container Images](#building-container-images)\n- [Running the Job](#running-the-job)\n- [Environment Variables](#environment-variables)\n- [Code Architecture](#code-architecture)\n- [GCP Secret Manager Integration](#gcp-secret-manager-integration)\n- [Verification](#verification)\n- [Troubleshooting](#troubleshooting)\n\n---\n\n## Overview\n\nThe Pull Secret Job is part of the HyperFleet Pull Secret Adapter (HYPERFLEET-162). It securely stores image pull secrets for OpenShift clusters in the Red Hat GCP Secret Manager using Workload Identity authentication.\n\n**Key Features:**\n- ✅ Stores pull secrets in Red Hat GCP Secret Manager\n- ✅ Authenticates using Workload Identity (in Kubernetes) or ADC (local development)\n- ✅ Structured JSON logging with operation metrics\n- ✅ Retry logic with exponential backoff and jitter\n- ✅ Idempotent operations (safe to re-run)\n- ✅ Pull secret validation (Dockercfg JSON format)\n- ✅ Labels for tracking (managed-by, cluster-id, adapter, etc.)\n\n---\n\n## Prerequisites\n\n### 1. Google Cloud SDK\n\nInstall the Google Cloud SDK:\n```bash\n# Verify installation\ngcloud --version\n```\n\n### 2. Authentication\n\n**IMPORTANT:** You must authenticate with Google Cloud before running the job locally.\n\n```bash\n# Authenticate with your Google account\ngcloud auth application-default login\n\n# (Optional) Set quota project\ngcloud auth application-default set-quota-project YOUR_PROJECT_ID\n```\n\nThis creates Application Default Credentials (ADC) that the Secret Manager client uses for authentication.\n\n### 3. GCP Project Setup\n\nEnsure your GCP project has:\n- Secret Manager API enabled\n- Appropriate IAM permissions for your account\n\n**Check API status:**\n```bash\ngcloud services list --enabled --project=YOUR_PROJECT_ID | grep secretmanager\n```\n\n**Enable Secret Manager API (requires `serviceusage.serviceUsageAdmin` role):**\n```bash\ngcloud services enable secretmanager.googleapis.com --project=YOUR_PROJECT_ID\n```\n\n**Required IAM permissions:**\n- `secretmanager.secrets.create` - Create secret resource\n- `secretmanager.secrets.get` - Check if secret exists\n- `secretmanager.versions.add` - Add new secret version\n- `secretmanager.versions.access` - Verify secret data\n\nYou need at least the `roles/secretmanager.admin` role on the project.\n\n### 4. Go Environment\n\n- Go 1.23.9 or later\n- Dependencies will be downloaded automatically via `go mod`\n\n---\n\n## Building the Project\n\n### Download dependencies\n\n```bash\ncd /path/to/mvp\ngo mod download\n```\n\n### Build the binary\n\n```bash\n# Using make (recommended)\nmake build\n\n# Or using go directly\nmkdir -p bin \u0026\u0026 go build -o bin/pull-secret ./cmd/pull-secret\n```\n\nThis creates the `pull-secret` executable in the `bin/` directory.\n\n---\n\n## Building Container Images\n\n### Prerequisites for Container Builds\n\n- **Podman** or Docker installed\n- **Quay.io account** (or other container registry)\n\n### 1. Login to Container Registry\n\n```bash\n# Login to quay.io\npodman login quay.io\n# Username: your-username\n# Password: your-password-or-token\n```\n\n### 2. Build Container Image\n\n#### Using Makefile (Recommended)\n\n```bash\n# Build with automatic git version tag\nmake image\n\n# Build with custom registry\nmake image IMAGE_REGISTRY=quay.io/your-username\n\n# Build with specific tag\nmake image IMAGE_TAG=v1.0.0\n\n# Build for personal development (uses QUAY_USER)\nmake image-dev QUAY_USER=your-username\n```\n\n#### Using Podman Directly\n\n```bash\n# Build with specific tag\npodman build -t quay.io/your-username/pull-secret:v1.0.0 -f Dockerfile .\n\n# Build with latest tag\npodman build -t quay.io/your-username/pull-secret:latest -f Dockerfile .\n\n# Build with multiple tags\npodman build \\\n  -t quay.io/your-username/pull-secret:latest \\\n  -t quay.io/your-username/pull-secret:v1.0.0 \\\n  -f Dockerfile .\n```\n\n### 3. Push Container Image\n\n#### Using Makefile (Recommended)\n\n```bash\n# Build and push with git version\nmake image-push\n\n# Build and push with specific tag\nmake image-push IMAGE_TAG=v1.0.0\n\n# Build and push to personal Quay (development workflow)\nmake image-dev QUAY_USER=your-username\n```\n\n#### Using Podman Directly\n\n```bash\n# Push specific tag\npodman push quay.io/your-username/pull-secret:v1.0.0\n\n# Push latest\npodman push quay.io/your-username/pull-secret:latest\n\n# Push all local tags\npodman push --all-tags quay.io/your-username/pull-secret\n```\n\n### 4. Complete Build and Push Workflow\n\n#### Example 1: Development Build (Personal Quay)\n\n```bash\n# Build and push to your personal Quay.io account\n# Tag will be automatically generated as dev-\u003cgit-commit\u003e\nmake image-dev QUAY_USER=your-username\n\n# Example output:\n# Building dev image quay.io/your-username/pull-secret:dev-f1bf914...\n# Pushing dev image quay.io/your-username/pull-secret:dev-f1bf914...\n```\n\n#### Example 2: Release Build (Official Registry)\n\n```bash\n# Build and push versioned release to official registry\nmake image-push IMAGE_TAG=v1.0.0\n\n# Example output:\n# Building image quay.io/openshift-hyperfleet/pull-secret:v1.0.0...\n# Pushing image quay.io/openshift-hyperfleet/pull-secret:v1.0.0...\n```\n\n#### Example 3: Manual Multi-Tag Push\n\n```bash\n# Build with multiple tags\npodman build \\\n  -t quay.io/your-username/pull-secret:latest \\\n  -t quay.io/your-username/pull-secret:v1.0.0 \\\n  -f Dockerfile .\n\n# Push both tags\npodman push quay.io/your-username/pull-secret:latest\npodman push quay.io/your-username/pull-secret:v1.0.0\n```\n\n### 5. Verify and Test Container Image\n\n```bash\n# List local images\npodman images | grep pull-secret\n\n# Inspect image\npodman inspect quay.io/your-username/pull-secret:latest\n\n# Pull from registry (verify it's available)\npodman pull quay.io/your-username/pull-secret:latest\n\n# Run the container\npodman run --rm \\\n  -e GCP_PROJECT_ID=\"your-project-id\" \\\n  -e CLUSTER_ID=\"cls-test-123\" \\\n  -e SECRET_NAME=\"hyperfleet-cls-test-123-pull-secret\" \\\n  -e PULL_SECRET_DATA='{\"auths\":{\"registry.redhat.io\":{\"auth\":\"dGVzdDp0ZXN0\",\"email\":\"test@example.com\"}}}' \\\n  quay.io/your-username/pull-secret:latest\n```\n\n### 6. Makefile Variables Reference\n\nYou can customize the build using these environment variables:\n\n| Variable | Default | Description |\n|----------|---------|-------------|\n| `VERSION` | git describe | Version tag from git |\n| `COMMIT` | git rev-parse | Short commit hash |\n| `CONTAINER_TOOL` | podman/docker | Container build tool (auto-detected) |\n| `IMAGE_REGISTRY` | `quay.io/openshift-hyperfleet` | Container registry |\n| `IMAGE_NAME` | `pull-secret` | Image name |\n| `IMAGE_TAG` | `$(VERSION)` | Image tag (defaults to git version) |\n| `QUAY_USER` | (empty) | Personal Quay username for dev builds |\n| `DEV_TAG` | `dev-$(COMMIT)` | Dev image tag |\n\n**Examples:**\n\n```bash\n# Use defaults (quay.io/openshift-hyperfleet/pull-secret:\u003cgit-version\u003e)\nmake image\n\n# Build with custom tag\nmake image IMAGE_TAG=v1.0.0\n\n# Build with custom registry\nmake image IMAGE_REGISTRY=quay.io/myorg\n\n# Personal development build\nmake image-dev QUAY_USER=ldornele\n# Results in: quay.io/ldornele/pull-secret:dev-f1bf914\n```\n\n### 7. Dockerfile Details\n\nThe Dockerfile uses a **multi-stage build** for optimization:\n\n- **Stage 1 (Builder):** Uses `registry.access.redhat.com/ubi9/go-toolset:1.23`\n  - Downloads dependencies\n  - Builds static binary with `CGO_ENABLED=0`\n\n- **Stage 2 (Runtime):** Uses `registry.access.redhat.com/ubi9/ubi-minimal:latest`\n  - Minimal attack surface\n  - Runs as non-root user (UID 1000)\n  - Only contains binary + CA certificates\n\n**Image size:** ~150 MB (compared to ~800+ MB with full Go image)\n\n---\n\n## Running the Job\n\n### Basic Usage\n\n```bash\nGCP_PROJECT_ID=\"your-project-id\" \\\n  CLUSTER_ID=\"cls-test-123\" \\\n  SECRET_NAME=\"hyperfleet-cls-test-123-pull-secret\" \\\n  PULL_SECRET_DATA='{\"auths\":{\"registry.redhat.io\":{\"auth\":\"dGVzdDp0ZXN0\",\"email\":\"test@example.com\"}}}' \\\n  ./bin/pull-secret run-job pull-secret\n```\n\n### Real-World Example\n\n```bash\nGCP_PROJECT_ID=\"redhat-prod-12345\" \\\n  CLUSTER_ID=\"cls-abc123\" \\\n  SECRET_NAME=\"hyperfleet-cls-abc123-pull-secret\" \\\n  PULL_SECRET_DATA='{\"auths\":{\"registry.redhat.io\":{\"auth\":\"base64-encoded-credentials\",\"email\":\"user@redhat.com\"},\"quay.io\":{\"auth\":\"base64-encoded-credentials\",\"email\":\"user@redhat.com\"}}}' \\\n  ./bin/pull-secret run-job pull-secret\n```\n\n### Expected Output\n\n```json\n{\"cluster_id\":\"cls-test-123\",\"gcp_project\":\"your-project-id\",\"level\":\"info\",\"message\":\"Starting pull secret storage operation\",\"operation\":\"start\",\"timestamp\":\"2025-12-08T13:07:31Z\"}\n{\"cluster_id\":\"cls-test-123\",\"gcp_project\":\"your-project-id\",\"level\":\"info\",\"message\":\"Successfully initialized Secret Manager client\",\"operation\":\"client-initialized\",\"timestamp\":\"2025-12-08T13:07:31Z\"}\n{\"cluster_id\":\"cls-test-123\",\"gcp_project\":\"your-project-id\",\"level\":\"info\",\"message\":\"Creating new secret: hyperfleet-cls-test-123-pull-secret\",\"operation\":\"create-secret\",\"timestamp\":\"2025-12-08T13:07:32Z\"}\n{\"cluster_id\":\"cls-test-123\",\"duration_ms\":2441,\"gcp_project\":\"your-project-id\",\"level\":\"info\",\"message\":\"Successfully created secret\",\"operation\":\"create-secret\",\"timestamp\":\"2025-12-08T13:07:34Z\"}\n{\"cluster_id\":\"cls-test-123\",\"gcp_project\":\"your-project-id\",\"level\":\"info\",\"message\":\"Adding secret version with pull secret data\",\"operation\":\"add-secret-version\",\"timestamp\":\"2025-12-08T13:07:34Z\"}\n{\"cluster_id\":\"cls-test-123\",\"duration_ms\":2077,\"gcp_project\":\"your-project-id\",\"level\":\"info\",\"message\":\"Successfully created secret version\",\"operation\":\"add-secret-version\",\"timestamp\":\"2025-12-08T13:07:36Z\",\"version\":\"projects/123456/secrets/hyperfleet-cls-test-123-pull-secret/versions/1\"}\n{\"cluster_id\":\"cls-test-123\",\"duration_ms\":379,\"gcp_project\":\"your-project-id\",\"level\":\"info\",\"message\":\"Verified secret (83 bytes)\",\"operation\":\"verify-secret\",\"timestamp\":\"2025-12-08T13:07:36Z\"}\n{\"cluster_id\":\"cls-test-123\",\"gcp_project\":\"your-project-id\",\"level\":\"info\",\"message\":\"Successfully created/updated pull secret\",\"operation\":\"completed\",\"timestamp\":\"2025-12-08T13:07:36Z\"}\n```\n\n---\n\n## Environment Variables\n\n| Variable | Required | Description | Example |\n|----------|----------|-------------|---------|\n| `GCP_PROJECT_ID` | Yes | GCP Project ID where secret will be stored | `redhat-prod-12345` |\n| `CLUSTER_ID` | Yes | Unique identifier for the OpenShift cluster | `cls-abc123` |\n| `SECRET_NAME` | No* | Name of the secret in GCP Secret Manager | `hyperfleet-cls-abc123-pull-secret` |\n| `PULL_SECRET_DATA` | No** | Pull secret in Dockercfg JSON format | `{\"auths\":{...}}` |\n\n**Notes:**\n- *If `SECRET_NAME` is not provided, it will be auto-generated as: `hyperfleet-{CLUSTER_ID}-pull-secret`\n- **If `PULL_SECRET_DATA` is not provided, a fake pull secret will be used (for testing only)\n\n### Pull Secret Format\n\nThe pull secret must be in **Dockercfg JSON format**:\n\n```json\n{\n  \"auths\": {\n    \"registry.redhat.io\": {\n      \"auth\": \"base64-encoded-username:password\",\n      \"email\": \"user@example.com\"\n    },\n    \"quay.io\": {\n      \"auth\": \"base64-encoded-username:password\",\n      \"email\": \"user@example.com\"\n    }\n  }\n}\n```\n\n---\n\n## Code Architecture\n\n### Project Structure\n\n```\nmvp/\n├── cmd/\n│   └── pull-secret/\n│       ├── main.go              # Entry point\n│       └── jobs/\n│           └── pull_secret.go   # Pull secret job implementation\n├── pkg/\n│   └── job/                     # Job framework\n├── go.mod                       # Go module dependencies\n├── go.sum                       # Dependency checksums\n└── README.md                    # This file\n```\n\n### Main Components\n\n#### 1. **PullSecretJob** (`jobs/pull_secret.go`)\n\nThe main job struct that implements the job framework interface:\n\n```go\ntype PullSecretJob struct {}\n\nfunc (pullsecretJob *PullSecretJob) GetTasks() ([]job.Task, error)\nfunc (pullsecretJob *PullSecretJob) GetMetadata() job.Metadata\nfunc (pullsecretJob *PullSecretJob) AddFlags(flags *pflag.FlagSet)\nfunc (pullsecretJob *PullSecretJob) GetWorkerCount() int\n```\n\n- **GetTasks()**: Reads environment variables and creates PullSecretTask instances\n- **GetMetadata()**: Returns job metadata (name, description)\n- **GetWorkerCount()**: Returns number of parallel workers (1 for this job)\n\n#### 2. **PullSecretTask**\n\nThe task struct that contains the actual secret data and performs the work:\n\n```go\ntype PullSecretTask struct {\n    PullSecret   string  // Pull secret JSON data\n    GCPProjectID string  // GCP project ID\n    ClusterID    string  // Cluster identifier\n    SecretName   string  // Secret name in GCP\n}\n\nfunc (e PullSecretTask) Process(ctx context.Context) error\n```\n\nThe `Process()` method executes the following workflow:\n\n1. **Validate configuration** - Ensures all required env vars are present\n2. **Validate pull secret format** - Verifies JSON structure\n3. **Initialize GCP Secret Manager client** - Creates authenticated client\n4. **Create or update secret** - Stores the pull secret in GCP\n5. **Verify secret accessibility** - Confirms secret can be read back\n\n#### 3. **Key Functions**\n\n**Configuration \u0026 Validation:**\n```go\nfunc (e PullSecretTask) validateConfig() error\nfunc validatePullSecret(pullSecretJSON string) error\n```\n\n**GCP Secret Manager Operations:**\n```go\nfunc (e PullSecretTask) secretExists(ctx context.Context, client *secretmanager.Client) (bool, error)\nfunc (e PullSecretTask) createSecret(ctx context.Context, client *secretmanager.Client) error\nfunc (e PullSecretTask) addSecretVersion(ctx context.Context, client *secretmanager.Client) (string, error)\nfunc (e PullSecretTask) verifySecret(ctx context.Context, client *secretmanager.Client) error\n```\n\n**Error Handling \u0026 Retry:**\n```go\nfunc retryWithBackoff(ctx context.Context, fn func() error, maxRetries int) error\nfunc isRetryable(err error) bool\n```\n\n**Logging:**\n```go\nfunc logStructured(level, clusterID, gcpProject, operation string, durationMs int64, message, version string)\n```\n\n---\n\n## GCP Secret Manager Integration\n\n### SDK Methods Used\n\nThe job uses the official GCP Secret Manager Go SDK:\n\n```go\nimport (\n    secretmanager \"cloud.google.com/go/secretmanager/apiv1\"\n    \"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb\"\n)\n```\n\n#### 1. **GetSecret** - Check if secret exists\n\n```go\nreq := \u0026secretmanagerpb.GetSecretRequest{\n    Name: \"projects/{project}/secrets/{secret}\",\n}\nsecret, err := client.GetSecret(ctx, req)\n```\n\nReturns `codes.NotFound` if secret doesn't exist.\n\n#### 2. **CreateSecret** - Create secret resource with labels\n\n```go\nreq := \u0026secretmanagerpb.CreateSecretRequest{\n    Parent:   \"projects/{project}\",\n    SecretId: secretName,\n    Secret: \u0026secretmanagerpb.Secret{\n        Replication: \u0026secretmanagerpb.Replication{\n            Replication: \u0026secretmanagerpb.Replication_Automatic_{\n                Automatic: \u0026secretmanagerpb.Replication_Automatic{},\n            },\n        },\n        Labels: map[string]string{\n            \"managed-by\":         \"hyperfleet\",\n            \"adapter\":            \"pullsecret\",\n            \"cluster-id\":         clusterID,\n            \"resource-type\":      \"pull-secret\",\n            \"hyperfleet-version\": \"v1\",\n        },\n    },\n}\nsecret, err := client.CreateSecret(ctx, req)\n```\n\n**Labels applied:**\n- `managed-by: hyperfleet` - Identifies HyperFleet-managed secrets\n- `adapter: pullsecret` - Identifies the adapter type\n- `cluster-id: {cluster-id}` - Links to specific cluster\n- `resource-type: pull-secret` - Resource classification\n- `hyperfleet-version: v1` - Schema version\n\n#### 3. **AddSecretVersion** - Store pull secret data\n\n```go\nreq := \u0026secretmanagerpb.AddSecretVersionRequest{\n    Parent: \"projects/{project}/secrets/{secret}\",\n    Payload: \u0026secretmanagerpb.SecretPayload{\n        Data: []byte(pullSecretJSON),\n    },\n}\nversion, err := client.AddSecretVersion(ctx, req)\n```\n\nCreates a new immutable version containing the pull secret data.\n\n#### 4. **AccessSecretVersion** - Verify secret accessibility\n\n```go\nreq := \u0026secretmanagerpb.AccessSecretVersionRequest{\n    Name: \"projects/{project}/secrets/{secret}/versions/latest\",\n}\nresult, err := client.AccessSecretVersion(ctx, req)\ndata := result.Payload.Data  // The actual secret data\n```\n\nVerifies the secret can be read back and returns the payload.\n\n### Authentication Flow\n\n**Local Development:**\n```\nUser runs: gcloud auth application-default login\n    ↓\nADC credentials stored at: ~/.config/gcloud/application_default_credentials.json\n    ↓\nsecretmanager.NewClient(ctx) automatically uses ADC\n    ↓\nAPI calls authenticated as user account\n```\n\n**Production (Kubernetes with Workload Identity):**\n```\nJob Pod with K8s Service Account: pullsecret-adapter-job\n    ↓\nWorkload Identity binding to GCP Service Account\n    ↓\nsecretmanager.NewClient(ctx) automatically uses Workload Identity\n    ↓\nAPI calls authenticated as GCP Service Account\n    ↓\nAppears in Cloud Audit Logs with GCP SA email\n```\n\n### Error Handling \u0026 Retry Logic\n\n**Retry Strategy:**\n- **Max retries**: 3 attempts\n- **Backoff**: Exponential with jitter (1s, 2s, 4s)\n- **Jitter**: ±20% to avoid thundering herd\n\n**Retryable Errors:**\n- `codes.Unavailable` - Service temporarily unavailable\n- `codes.DeadlineExceeded` - Request timeout\n- `codes.Internal` - Internal server error\n- `codes.ResourceExhausted` - Rate limit exceeded (429)\n\n**Non-Retryable Errors (fail immediately):**\n- `codes.PermissionDenied` - Missing IAM permissions\n- `codes.InvalidArgument` - Invalid request parameters\n- `codes.NotFound` - Resource not found\n- `codes.AlreadyExists` - Resource already exists\n\n### Structured Logging\n\nAll operations are logged in **structured JSON format**:\n\n```json\n{\n  \"timestamp\": \"2025-12-08T13:07:31Z\",\n  \"level\": \"info\",\n  \"cluster_id\": \"cls-test-123\",\n  \"gcp_project\": \"redhat-prod-12345\",\n  \"operation\": \"create-secret\",\n  \"duration_ms\": 2441,\n  \"message\": \"Successfully created secret\",\n  \"version\": \"projects/123/secrets/hyperfleet-cls-test-123-pull-secret/versions/1\"\n}\n```\n\n**Log Fields:**\n- `timestamp` - ISO 8601 UTC timestamp\n- `level` - Log level (info, error)\n- `cluster_id` - Cluster identifier\n- `gcp_project` - GCP project ID\n- `operation` - Operation name (start, create-secret, add-secret-version, etc.)\n- `duration_ms` - Operation duration in milliseconds (optional)\n- `message` - Human-readable message\n- `version` - Secret version (optional)\n\n**Security Note:** Pull secret data is **NEVER** logged to prevent credential exposure.\n\n---\n\n## Verification\n\n### Verify Secret in GCP Console\n\n1. Go to: https://console.cloud.google.com/security/secret-manager?project=YOUR_PROJECT_ID\n2. Find secret: `hyperfleet-cls-test-123-pull-secret`\n3. Click on the secret to view metadata and labels\n4. Click **VERSIONS** tab to see version 1\n5. Click **⋮** (three dots) → **View secret value** to see the pull secret data\n\n### Verify Secret via gcloud CLI\n\n```bash\n# List secrets with HyperFleet labels\ngcloud secrets list \\\n  --project=YOUR_PROJECT_ID \\\n  --filter=\"labels.managed-by=hyperfleet\"\n\n# Describe the secret (metadata only)\ngcloud secrets describe hyperfleet-cls-test-123-pull-secret \\\n  --project=YOUR_PROJECT_ID\n\n# View labels\ngcloud secrets describe hyperfleet-cls-test-123-pull-secret \\\n  --project=YOUR_PROJECT_ID \\\n  --format=\"table(labels)\"\n\n# List versions\ngcloud secrets versions list hyperfleet-cls-test-123-pull-secret \\\n  --project=YOUR_PROJECT_ID\n\n# Access secret data (requires secretmanager.versions.access permission)\ngcloud secrets versions access latest \\\n  --secret=hyperfleet-cls-test-123-pull-secret \\\n  --project=YOUR_PROJECT_ID\n```\n\n### Idempotency Test\n\nRun the job multiple times with the same parameters:\n\n```bash\n# First run - creates secret and version 1\n./bin/pull-secret run-job pull-secret\n\n# Second run - secret exists, creates version 2\n./bin/pull-secret run-job pull-secret\n\n# Third run - secret exists, creates version 3\n./bin/pull-secret run-job pull-secret\n```\n\nEach run should succeed and create a new version.\n\n---\n\n## Troubleshooting\n\n### Error: \"could not find default credentials\"\n\n**Problem:** ADC not configured\n\n**Solution:**\n```bash\ngcloud auth application-default login\n```\n\n### Error: \"Permission denied on resource project\"\n\n**Problem:** Missing IAM permissions or API not enabled\n\n**Solutions:**\n\n1. Check if Secret Manager API is enabled:\n```bash\ngcloud services list --enabled --project=YOUR_PROJECT_ID | grep secretmanager\n```\n\n2. Check your IAM roles:\n```bash\ngcloud projects get-iam-policy YOUR_PROJECT_ID \\\n  --flatten=\"bindings[].members\" \\\n  --filter=\"bindings.members:user:YOUR_EMAIL\" \\\n  --format=\"table(bindings.role)\"\n```\n\n3. Request `roles/secretmanager.admin` from project administrator\n\n### Error: \"SERVICE_DISABLED\"\n\n**Problem:** Secret Manager API not enabled\n\n**Solution:**\n```bash\ngcloud services enable secretmanager.googleapis.com --project=YOUR_PROJECT_ID\n```\n\nRequires `roles/serviceusage.serviceUsageAdmin` role.\n\n### Error: \"missing required environment variable\"\n\n**Problem:** Required env vars not set\n\n**Solution:** Ensure all required variables are exported:\n```bash\nexport GCP_PROJECT_ID=\"your-project-id\"\nexport CLUSTER_ID=\"cls-test-123\"\n# SECRET_NAME is optional (auto-generated)\n# PULL_SECRET_DATA is optional (uses fake data for testing)\n```\n\n### Error: \"invalid pull secret format\"\n\n**Problem:** PULL_SECRET_DATA is not valid Dockercfg JSON\n\n**Solution:** Ensure JSON has required structure:\n```json\n{\"auths\":{\"registry.redhat.io\":{\"auth\":\"...\",\"email\":\"...\"}}}\n```\n\nMust have:\n- Top-level `auths` key\n- At least one registry entry\n- Each registry has `auth` field (base64-encoded credentials)\n\n### Build Errors\n\nIf you encounter build errors, ensure dependencies are up to date:\n\n```bash\ngo mod tidy\ngo mod download\ngo build ./cmd/pull-secret\n```\n\n---\n\n## Performance Benchmarks\n\nBased on actual test runs:\n\n| Operation | Duration | Notes |\n|-----------|----------|-------|\n| Client initialization | \u003c 1s | Workload Identity token exchange |\n| Secret creation | ~2.5s | First-time secret creation |\n| Add secret version | ~2s | Adding new version with data |\n| Verify secret | ~400ms | Reading back secret data |\n| **Total (first run)** | **~5s** | Complete workflow |\n| **Total (subsequent)** | **~3s** | Secret already exists |\n\n**Resource Usage:**\n- Memory: \u003c 50 MB\n- CPU: \u003c 100m (0.1 cores)\n\n---\n\n## Production Deployment\n\nIn production, this job runs as a Kubernetes Job in the management cluster:\n\n1. **Adapter** creates Kubernetes Job with proper environment variables\n2. **Job Pod** runs with `pullsecret-adapter-job` service account\n3. **Workload Identity** automatically authenticates to GCP\n4. **Job** executes and stores pull secret in RH's GCP project\n5. **Logs** are collected and forwarded to observability platform\n6. **Job** completes and is cleaned up after retention period\n\n**Security Context (Production):**\n```yaml\nsecurityContext:\n  runAsNonRoot: true\n  runAsUser: 1000\n  allowPrivilegeEscalation: false\n  readOnlyRootFilesystem: true\n  capabilities:\n    drop:\n    - ALL\n```\n\n---\n\n## References\n\n- **Epic**: [HYPERFLEET-162](https://issues.redhat.com/browse/HYPERFLEET-162) - Pull Secret Adapter\n- **GCP Secret Manager API**: https://cloud.google.com/secret-manager/docs\n- **Go Client Library**: https://pkg.go.dev/cloud.google.com/go/secretmanager/apiv1\n- **Workload Identity**: https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity\n- **Architecture Documentation**: `/architecture/hyperfleet/components/adapter/PullSecret/GCP/`\n\n---\n\n## License\n\nCopyright © 2025 Red Hat, Inc.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenshift-hyperfleet%2Fadapter-pull-secret","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopenshift-hyperfleet%2Fadapter-pull-secret","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenshift-hyperfleet%2Fadapter-pull-secret/lists"}