diff --git a/cmd/main.go b/cmd/main.go index d5b69740e..0dec97172 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -36,6 +36,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" gitopsv1alpha1 "github.com/hybrid-cloud-patterns/patterns-operator/api/v1alpha1" @@ -85,6 +86,9 @@ func main() { HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "f2850479.hybrid-cloud-patterns.io", + Metrics: server.Options{ + BindAddress: "0", + }, //LeaderElectionNamespace: "default", // Use this if we ever want to enforce a single instance per cluster }) if err != nil { diff --git a/console/integration-tests/tests/pattern-catalog-page.cy.ts b/console/integration-tests/tests/pattern-catalog-page.cy.ts index 74aaf2e8c..8625c092c 100644 --- a/console/integration-tests/tests/pattern-catalog-page.cy.ts +++ b/console/integration-tests/tests/pattern-catalog-page.cy.ts @@ -25,9 +25,11 @@ describe('Pattern Catalog Page', () => { it('pattern cards show tier labels', () => { visitCatalog(); - cy.get('.patterns-operator__card').first().within(() => { - cy.get('.pf-v6-c-label').should('exist'); - }); + cy.get('.patterns-operator__card') + .first() + .within(() => { + cy.get('.pf-v6-c-label').should('exist'); + }); }); it('at least one pattern card displays a description', () => { @@ -41,21 +43,21 @@ describe('Pattern Catalog Page', () => { it('pattern cards have external Docs and Repo links', () => { visitCatalog(); - cy.get('.patterns-operator__card-links').first().within(() => { - cy.contains('a', 'Docs') - .should('have.attr', 'target', '_blank') - .and('have.attr', 'href'); - cy.contains('a', 'Repo') - .should('have.attr', 'target', '_blank') - .and('have.attr', 'href'); - }); + cy.get('.patterns-operator__card-links') + .first() + .within(() => { + cy.contains('a', 'Docs').should('have.attr', 'target', '_blank').and('have.attr', 'href'); + cy.contains('a', 'Repo').should('have.attr', 'target', '_blank').and('have.attr', 'href'); + }); }); it('pattern cards have action buttons', () => { visitCatalog(); - cy.get('.patterns-operator__card-actions').first().within(() => { - cy.get('button').should('have.length.greaterThan', 0); - }); + cy.get('.patterns-operator__card-actions') + .first() + .within(() => { + cy.get('button').should('have.length.greaterThan', 0); + }); }); it('tier filter dropdown shows all tier options', () => { @@ -71,24 +73,28 @@ describe('Pattern Catalog Page', () => { it('selecting all tiers shows at least as many cards as maintained only', () => { visitCatalog(); - cy.get('.patterns-operator__card').its('length').then((maintainedCount) => { - // Open filter and add Tested - cy.contains('button', 'Maintained').click(); - cy.contains('Tested').click(); - // Dropdown may close after selection; re-open to add Sandbox - cy.contains('button', /Maintained/).click(); - cy.contains('Sandbox').click(); - // Close dropdown - cy.get('body').click(0, 0); - // With more tiers selected, card count should be >= maintained only - cy.get('.patterns-operator__card').should('have.length.gte', maintainedCount); - }); + cy.get('.patterns-operator__card') + .its('length') + .then((maintainedCount) => { + // Open filter and add Tested + cy.contains('button', 'Maintained').click(); + cy.contains('Tested').click(); + // Dropdown may close after selection; re-open to add Sandbox + cy.contains('button', /Maintained/).click(); + cy.contains('Sandbox').click(); + // Close dropdown + cy.get('body').click(0, 0); + // With more tiers selected, card count should be >= maintained only + cy.get('.patterns-operator__card').should('have.length.gte', maintainedCount); + }); }); it('clicking Install navigates to the install page', () => { visitCatalog(); cy.get('body').then(($body) => { - const installBtn = $body.find('.patterns-operator__card-actions button:not(:disabled):contains("Install")'); + const installBtn = $body.find( + '.patterns-operator__card-actions button:not(:disabled):contains("Install")', + ); if (installBtn.length === 0) { cy.log('No Install button available (a pattern may already be installed)'); return; diff --git a/console/locales/en/plugin__patterns-operator-console-plugin.json b/console/locales/en/plugin__patterns-operator-console-plugin.json index 1437ac297..c4e55bd70 100644 --- a/console/locales/en/plugin__patterns-operator-console-plugin.json +++ b/console/locales/en/plugin__patterns-operator-console-plugin.json @@ -1,6 +1,8 @@ { + "A file upload is required.": "A file upload is required.", "Additional requirements": "Additional requirements", "Allow override": "Allow override", + "An INI file upload is required.": "An INI file upload is required.", "Applications": "Applications", "Auto-generate this value": "Auto-generate this value", "Back to catalog": "Back to catalog", @@ -9,17 +11,17 @@ "Catalog source": "Catalog source", "Checking vault status": "Checking vault status", "Cluster": "Cluster", - "Configure Secrets": "Configure Secrets", - "Configure Secrets for {{patternName}}": "Configure Secrets for {{patternName}}", + "Configure secrets for this pattern. File and INI fields are required before install.": "Configure secrets for this pattern. File and INI fields are required before install.", "Configure secrets that will be injected into Vault for this pattern.": "Configure secrets that will be injected into Vault for this pattern.", "Confirm Uninstall": "Confirm Uninstall", - "Continue to Install": "Continue to Install", "Current Step": "Current Step", "Deleting": "Deleting", "Deletion Progress": "Deletion Progress", "Docs": "Docs", + "Drag and drop a file or upload one": "Drag and drop a file or upload one", "Enter manual value": "Enter manual value", "Enter or update the secrets that will be injected into Vault for this pattern. Fields left empty will retain their existing values in Vault.": "Enter or update the secrets that will be injected into Vault for this pattern. Fields left empty will retain their existing values in Vault.", + "Enter or update the secrets that will be injected into Vault for this pattern. File and INI fields must be uploaded each time. Other fields may be left empty to keep existing Vault values.": "Enter or update the secrets that will be injected into Vault for this pattern. File and INI fields must be uploaded each time. Other fields may be left empty to keep existing Vault values.", "Enter value for {{fieldName}}": "Enter value for {{fieldName}}", "Error": "Error", "Extracted value preview": "Extracted value preview", @@ -30,11 +32,11 @@ "Failed to load secret template": "Failed to load secret template", "Failed to parse INI file": "Failed to parse INI file", "Failed to read file": "Failed to read file", - "File content will be automatically base64 encoded": "File content will be automatically base64 encoded", - "File will be stored at: {{path}}": "File will be stored at: {{path}}", "Health": "Health", "Hub": "Hub", "I want to use my own fork": "I want to use my own fork", + "If checked this value will be automatically generated using vault policies": "If checked this value will be automatically generated using vault policies", + "If the secret already exists in the vault it will be changed if override is set to true": "If the secret already exists in the vault it will be changed if override is set to true", "Inject Secrets": "Inject Secrets", "Install": "Install", "Install Pattern": "Install Pattern", @@ -68,12 +70,12 @@ "Reconciliation Status": "Reconciliation Status", "Reconciling": "Reconciling", "Repo": "Repo", + "Secret configuration": "Secret configuration", "Secret Configuration": "Secret Configuration", "Secrets Configuration": "Secrets Configuration", "Secrets submitted successfully": "Secrets submitted successfully", "Select a section": "Select a section", "Select INI section": "Select INI section", - "Skip Secrets": "Skip Secrets", "Spoke": "Spoke", "Static value": "Static value", "Sync": "Sync", @@ -83,23 +85,17 @@ "Tested Requirements:": "Tested Requirements:", "The pattern and all its associated resources have been fully deleted.": "The pattern and all its associated resources have been fully deleted.", "The vault injection job has been created.": "The vault injection job has been created.", - "This field has a pre-configured value": "This field has a pre-configured value", "This field is required": "This field is required", "This is the sizing that has been tested. The pattern is expected to work on any similarly-sized architecture.": "This is the sizing that has been tested. The pattern is expected to work on any similarly-sized architecture.", "This pattern does not have a secret template defined.": "This pattern does not have a secret template defined.", - "This value will be automatically generated using vault policies": "This value will be automatically generated using vault policies", "This will delete the pattern and all its deployed resources.": "This will delete the pattern and all its deployed resources.", "Tier": "Tier", "Uninstall": "Uninstall", "Uninstall Pattern": "Uninstall Pattern", - "Upload a file that will be base64 encoded": "Upload a file that will be base64 encoded", - "Upload a text file": "Upload a text file", "Upload an INI/configuration file to extract values": "Upload an INI/configuration file to extract values", "Vault policy: {{policy}}": "Vault policy: {{policy}}", "Vault Secret Injection": "Vault Secret Injection", "Waiting for applications": "Waiting for applications", "Waiting for ArgoCD applications to be created...": "Waiting for ArgoCD applications to be created...", - "You can manually override the auto-generated value": "You can manually override the auto-generated value", - "You can modify this pre-filled value": "You can modify this pre-filled value", "Your pattern has been created. The operator is now reconciling it.": "Your pattern has been created. The operator is now reconciling it." } \ No newline at end of file diff --git a/console/package.json b/console/package.json index c50ece526..fa9cf2aae 100644 --- a/console/package.json +++ b/console/package.json @@ -15,7 +15,7 @@ "start": "concurrently --kill-others -c auto -n \"console,wds,ui-catalog\" -p \"{name}:{pid}\" \"yarn run start:console\" \"yarn run start:wds\" \"yarn run start:ui-catalog\"", "start:wds": "PATTERN_UI_CATALOG_BASE_URL=http://localhost:8080 yarn run webpack -- serve --progress", "start:console": "CONSOLE_VERSION=4.20.0 scripts/console.sh", - "start:ui-catalog": "UI_CATALOG_IMG=quay.io/validatedpatterns/pattern-ui-catalog:stable-v1 scripts/catalog.sh", + "start:ui-catalog": "scripts/catalog.sh", "i18n": "./i18n-scripts/build-i18n.sh && node ./i18n-scripts/set-english-defaults.js", "lint": "yarn eslint src integration-tests --fix && stylelint 'src/**/*.css' --allow-empty-input --fix", "test-cypress": "cd integration-tests && cypress open", @@ -95,7 +95,8 @@ "mocha": "^10.5.1" }, "dependencies": { - "js-yaml": "^4.1.1" + "js-yaml": "^4.1.1", + "yaml": "^2.8.3" }, "resolutions": { "braces": "^3.0.3" diff --git a/console/scripts/catalog.sh b/console/scripts/catalog.sh index cecc53074..5825eb07d 100755 --- a/console/scripts/catalog.sh +++ b/console/scripts/catalog.sh @@ -3,6 +3,7 @@ set -euo pipefail CONSOLE_IMAGE_PLATFORM=${CONSOLE_IMAGE_PLATFORM:="linux/amd64"} +UI_CATALOG_IMG=${UI_CATALOG_IMG:="quay.io/validatedpatterns/pattern-ui-catalog:stable-v1"} echo "Starting local UI catalog image..." echo "Console Image: $UI_CATALOG_IMG" diff --git a/console/src/api.ts b/console/src/api.ts index 519c7882d..77c9ca927 100644 --- a/console/src/api.ts +++ b/console/src/api.ts @@ -1,6 +1,12 @@ import { consoleFetch } from '@openshift-console/dynamic-plugin-sdk'; import { load } from 'js-yaml'; -import { Catalog, Pattern, SecretTemplate } from './types'; +import { + Catalog, + Pattern, + SecretTemplate, + VAULT_UPLOADS_MOUNT_PREFIX, + type VaultInjectionFileArtifact, +} from './types'; declare const __PATTERN_UI_CATALOG_BASE_URL__: string; declare const __PATTERN_OPERATOR_NS__: string; @@ -8,9 +14,10 @@ declare const __PATTERN_OPERATOR_NS__: string; const DEFAULT_PATTERN_OPERATOR_NS = 'patterns-operator'; export const PATTERN_OPERATOR_NS = __PATTERN_OPERATOR_NS__ || DEFAULT_PATTERN_OPERATOR_NS; -const DEFAULT_PATTERN_UI_CATALOG_BASE_URL = '/api/proxy/plugin/patterns-operator-console-plugin/pattern-ui-catalog'; -const PATTERN_UI_CATALOG_BASE_URL = __PATTERN_UI_CATALOG_BASE_URL__ || DEFAULT_PATTERN_UI_CATALOG_BASE_URL; - +const DEFAULT_PATTERN_UI_CATALOG_BASE_URL = + '/api/proxy/plugin/patterns-operator-console-plugin/pattern-ui-catalog'; +const PATTERN_UI_CATALOG_BASE_URL = + __PATTERN_UI_CATALOG_BASE_URL__ || DEFAULT_PATTERN_UI_CATALOG_BASE_URL; async function fetchYAML(url: string): Promise { const response = await consoleFetch(url, { cache: 'no-store' }); @@ -28,9 +35,8 @@ export async function fetchPattern(name: string): Promise { export async function fetchCatalogImage(): Promise { try { - var response = await consoleFetch( + const response = await consoleFetch( `/api/kubernetes/apis/apps/v1/namespaces/${PATTERN_OPERATOR_NS}/deployments/patterns-operator-pattern-ui-catalog`, - ); const data = await response.json(); const containers = data.spec?.template?.spec?.containers || []; @@ -39,12 +45,14 @@ export async function fetchCatalogImage(): Promise { ); return catalogContainer?.image || 'unknown'; } catch (error) { - return 'unknown' + return 'unknown'; } - } -export async function fetchAllPatterns(): Promise<{ patterns: Pattern[]; catalogDescription?: string }> { +export async function fetchAllPatterns(): Promise<{ + patterns: Pattern[]; + catalogDescription?: string; +}> { const catalog = await fetchCatalog(); const patterns = await Promise.all( catalog.patterns.map(async (key) => { @@ -65,7 +73,8 @@ export interface VaultJobStatus { export interface VaultInjectionRequest { patternName: string; valuesSecretYaml: string; - templateYaml?: string; + /** One Kubernetes Secret per entry, mounted under {@link VAULT_UPLOADS_MOUNT_PREFIX}/{slug}. */ + fileArtifacts?: VaultInjectionFileArtifact[]; vaultNamespace?: string; vaultPod?: string; vaultHub?: string; @@ -78,62 +87,102 @@ export interface VaultInjectionResponse { secretName?: string; } -export async function triggerVaultInjection(request: VaultInjectionRequest): Promise { +export async function triggerVaultInjection( + request: VaultInjectionRequest, +): Promise { try { console.log('🚀 [API] Starting vault injection for pattern:', request.patternName); + const fileArtifacts = request.fileArtifacts ?? []; console.log('📊 [API] Request details:', { patternName: request.patternName, valuesSecretYamlLength: request.valuesSecretYaml?.length || 0, - hasTemplate: !!request.templateYaml, + fileArtifactCount: fileArtifacts.length, vaultNamespace: request.vaultNamespace || 'vault', vaultPod: request.vaultPod || 'vault-0', - vaultHub: request.vaultHub || 'hub' + vaultHub: request.vaultHub || 'hub', }); const timestamp = Date.now(); const secretName = `vault-secrets-${request.patternName}-${timestamp}`; const jobName = `vault-inject-${request.patternName}-${timestamp}`; + const runLabelKey = 'patterns.gitops.hybrid-cloud-patterns.io/vault-injection-run'; + const runLabelValue = String(timestamp); + + const commonSecretLabels = { + 'patterns.gitops.hybrid-cloud-patterns.io/pattern': request.patternName, + 'patterns.gitops.hybrid-cloud-patterns.io/component': 'secret-injector', + [runLabelKey]: runLabelValue, + }; console.log('🔐 [API] Creating Kubernetes secret:', secretName); console.log('⚙️ [API] Creating Kubernetes job:', jobName); - // First, create a Secret with the values-secret.yaml content - const secretData: any = { - 'values-secret.yaml': btoa(request.valuesSecretYaml), // base64 encode for Kubernetes - }; + for (let i = 0; i < fileArtifacts.length; i++) { + const artifact = fileArtifacts[i]; + const fileSecretName = `${secretName}-f${i}`; + const fileSecret = { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: fileSecretName, + namespace: PATTERN_OPERATOR_NS, + labels: { ...commonSecretLabels }, + }, + type: 'Opaque', + data: { content: artifact.dataBase64 }, + }; - if (request.templateYaml) { - secretData['values-secret.yaml.template'] = btoa(request.templateYaml); + const fileResp = await consoleFetch( + `/api/kubernetes/api/v1/namespaces/${PATTERN_OPERATOR_NS}/secrets`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(fileSecret), + }, + ); + + if (!fileResp.ok) { + const errorText = await fileResp.text(); + console.error( + '🔴 [API] Failed to create file secret:', + fileSecretName, + fileResp.status, + errorText, + ); + throw new Error(`Failed to create file secret: ${fileResp.status} ${errorText}`); + } } + const secretData: Record = { + 'values-secret.yaml': btoa(request.valuesSecretYaml), + }; + const secret = { apiVersion: 'v1', kind: 'Secret', metadata: { name: secretName, namespace: PATTERN_OPERATOR_NS, - labels: { - 'patterns.gitops.hybrid-cloud-patterns.io/pattern': request.patternName, - 'patterns.gitops.hybrid-cloud-patterns.io/component': 'secret-injector', - }, + labels: { ...commonSecretLabels }, }, type: 'Opaque', data: secretData, }; - // Create the secret - console.log('🔐 [API] Creating secret with payload size:', JSON.stringify(secret).length, 'bytes'); - console.log('🔐 [API] Secret metadata:', { - name: secret.metadata.name, - namespace: secret.metadata.namespace, - labels: secret.metadata.labels - }); + console.log( + '🔐 [API] Creating values-secret with payload size:', + JSON.stringify(secret).length, + 'bytes', + ); - const secretResponse = await consoleFetch(`/api/kubernetes/api/v1/namespaces/${PATTERN_OPERATOR_NS}/secrets`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(secret), - }); + const secretResponse = await consoleFetch( + `/api/kubernetes/api/v1/namespaces/${PATTERN_OPERATOR_NS}/secrets`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(secret), + }, + ); if (!secretResponse.ok) { const errorText = await secretResponse.text(); @@ -146,10 +195,49 @@ export async function triggerVaultInjection(request: VaultInjectionRequest): Pro console.log('✅ [API] Secret creation result:', { name: secretResult.metadata?.name, uid: secretResult.metadata?.uid, - creationTimestamp: secretResult.metadata?.creationTimestamp + creationTimestamp: secretResult.metadata?.creationTimestamp, + }); + + const volumes: Record[] = [ + { + name: 'vault-secrets', + secret: { secretName }, + }, + ]; + if (fileArtifacts.length > 0) { + volumes.push({ + name: 'vault-uploads', + projected: { + sources: fileArtifacts.map((art, i) => ({ + secret: { + name: `${secretName}-f${i}`, + items: [{ key: 'content', path: art.slug }], + }, + })), + }, + }); + } + volumes.push({ name: 'shared', emptyDir: {} }); + + const injectorVolumeMounts: Record[] = [ + { + name: 'vault-secrets', + mountPath: '/vault-secrets', + readOnly: true, + }, + ]; + if (fileArtifacts.length > 0) { + injectorVolumeMounts.push({ + name: 'vault-uploads', + mountPath: VAULT_UPLOADS_MOUNT_PREFIX, + readOnly: true, + }); + } + injectorVolumeMounts.push({ + name: 'shared', + mountPath: '/shared', }); - // Now create a Job that uses this secret const job = { apiVersion: 'batch/v1', kind: 'Job', @@ -159,6 +247,7 @@ export async function triggerVaultInjection(request: VaultInjectionRequest): Pro labels: { 'patterns.gitops.hybrid-cloud-patterns.io/pattern': request.patternName, 'patterns.gitops.hybrid-cloud-patterns.io/component': 'secret-injector', + [runLabelKey]: runLabelValue, }, }, spec: { @@ -168,6 +257,7 @@ export async function triggerVaultInjection(request: VaultInjectionRequest): Pro labels: { 'patterns.gitops.hybrid-cloud-patterns.io/pattern': request.patternName, 'patterns.gitops.hybrid-cloud-patterns.io/component': 'secret-injector', + [runLabelKey]: runLabelValue, }, }, spec: { @@ -183,15 +273,6 @@ export async function triggerVaultInjection(request: VaultInjectionRequest): Pro ` echo "Starting vault secret injection for pattern: ${request.patternName}" - # Copy mounted secret files to expected location - mkdir -p /tmp/pattern - cp /vault-secrets/values-secret.yaml /tmp/pattern/values-secret.yaml - if [[ -f /vault-secrets/values-secret.yaml.template ]]; then - cp /vault-secrets/values-secret.yaml.template /tmp/pattern/values-secret.yaml.template - fi - - echo "Secret files prepared, running ansible to inject into vault..." - # Create a simplified playbook that calls the vault_load_secrets module directly cat > /tmp/vault_injection_playbook.yaml << 'PLAYBOOK_EOF' --- @@ -201,35 +282,21 @@ export async function triggerVaultInjection(request: VaultInjectionRequest): Pro gather_facts: false vars: ansible_python_interpreter: "{{ ansible_playbook_python }}" - values_secrets: "{{ pattern_dir }}/values-secret.yaml" check_missing_secrets: false namespace: "{{ vault_ns }}" pod: "{{ vault_pod }}" tasks: - - name: Check if values-secret.yaml.template exists - ansible.builtin.stat: - path: "{{ pattern_dir }}/values-secret.yaml.template" - register: template_file_check - - - name: Set values-secret.yaml.template exists fact - ansible.builtin.set_fact: - values_secret_template: "{{ (template_file_check.stat.exists) | ternary(pattern_dir + '/values-secret.yaml.template', omit) }}" - - name: Load secrets into vault using rhvp.cluster_utils module ansible.builtin.include_role: name: rhvp.cluster_utils.load_secrets PLAYBOOK_EOF - # Run the playbook and save the exit code cd /pattern-home ansible-playbook -v -i localhost, /tmp/vault_injection_playbook.yaml \\ - -e pattern_name="${request.patternName}" \\ -e pattern_dir="/tmp/pattern" \\ -e vault_ns="${request.vaultNamespace || 'vault'}" \\ -e vault_pod="${request.vaultPod || 'vault-0'}" \\ - -e vault_hub="${request.vaultHub || 'hub'}" \\ - -e found_file="/tmp/pattern/values-secret.yaml" \\ - -e secret_template="/tmp/pattern/values-secret.yaml.template" + -e vault_hub="${request.vaultHub || 'hub'}" rc=$? echo $rc > /shared/rc @@ -239,21 +306,9 @@ PLAYBOOK_EOF ], env: [ { name: 'PATTERN_NAME', value: request.patternName }, - { name: 'VAULT_NAMESPACE', value: request.vaultNamespace || 'vault' }, - { name: 'VAULT_POD', value: request.vaultPod || 'vault-0' }, - { name: 'VAULT_HUB', value: request.vaultHub || 'hub' }, - ], - volumeMounts: [ - { - name: 'vault-secrets', - mountPath: '/vault-secrets', - readOnly: true, - }, - { - name: 'shared', - mountPath: '/shared', - }, + { name: 'VALUES_SECRET', value: '/vault-secrets/values-secret.yaml' }, ], + volumeMounts: injectorVolumeMounts, resources: { requests: { cpu: '100m', memory: '256Mi' }, limits: { cpu: '500m', memory: '512Mi' }, @@ -265,13 +320,14 @@ PLAYBOOK_EOF command: [ '/bin/bash', '-c', - `echo "Deleting temporary secret $SECRET_NAME in namespace $SECRET_NAMESPACE" - kubectl delete secret "$SECRET_NAME" -n "$SECRET_NAMESPACE" --ignore-not-found + `echo "Deleting temporary secrets for injection run $VAULT_INJECTION_RUN" + kubectl delete secret -n "$SECRET_NAMESPACE" -l "${runLabelKey}=$VAULT_INJECTION_RUN,patterns.gitops.hybrid-cloud-patterns.io/pattern=$PATTERN_NAME" --ignore-not-found echo "Cleanup complete"`, ], env: [ - { name: 'SECRET_NAME', value: secretName }, { name: 'SECRET_NAMESPACE', value: PATTERN_OPERATOR_NS }, + { name: 'VAULT_INJECTION_RUN', value: runLabelValue }, + { name: 'PATTERN_NAME', value: request.patternName }, ], resources: { requests: { cpu: '50m', memory: '64Mi' }, @@ -307,16 +363,7 @@ PLAYBOOK_EOF }, }, ], - volumes: [ - { - name: 'vault-secrets', - secret: { secretName: secretName }, - }, - { - name: 'shared', - emptyDir: {}, - }, - ], + volumes, }, }, }, @@ -329,14 +376,17 @@ PLAYBOOK_EOF namespace: job.metadata.namespace, labels: job.metadata.labels, serviceAccountName: job.spec.template.spec.serviceAccountName, - containerImage: job.spec.template.spec.containers[0].image + containerImage: job.spec.template.spec.containers[0].image, }); - const jobResponse = await consoleFetch(`/api/kubernetes/apis/batch/v1/namespaces/${PATTERN_OPERATOR_NS}/jobs`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(job), - }); + const jobResponse = await consoleFetch( + `/api/kubernetes/apis/batch/v1/namespaces/${PATTERN_OPERATOR_NS}/jobs`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(job), + }, + ); if (!jobResponse.ok) { const errorText = await jobResponse.text(); @@ -350,7 +400,7 @@ PLAYBOOK_EOF name: jobData.metadata?.name, uid: jobData.metadata?.uid, creationTimestamp: jobData.metadata?.creationTimestamp, - backoffLimit: jobData.spec?.backoffLimit + backoffLimit: jobData.spec?.backoffLimit, }); console.log('🎉 [API] Vault injection setup completed successfully'); @@ -365,7 +415,7 @@ PLAYBOOK_EOF console.error('🔴 [API] Error details:', { name: error.name, message: error.message, - stack: error.stack + stack: error.stack, }); return { success: false, @@ -382,18 +432,21 @@ export async function fetchVaultJobStatus(patternName: string): Promise ({ - name: job.metadata?.name, - creationTimestamp: job.metadata?.creationTimestamp, - status: job.status - })) || [] + items: + data.items?.map((job) => ({ + name: job.metadata?.name, + creationTimestamp: job.metadata?.creationTimestamp, + status: job.status, + })) || [], }); if (!data.items || data.items.length === 0) { @@ -412,7 +465,9 @@ export async function fetchVaultJobStatus(patternName: string): Promise ({ type: c.type, status: c.status, reason: c.reason })) || [] + conditions: + jobStatus.conditions?.map((c) => ({ type: c.type, status: c.status, reason: c.reason })) || + [], }); let status: VaultJobStatus['status'] = 'pending'; @@ -448,7 +503,7 @@ export async function fetchVaultJobStatus(patternName: string): Promise { }; } catch (err) { // consoleFetch may throw on 404 instead of returning a response - if (err?.response?.status === 404 || err?.status === 404 || - (err?.message && /404|not found/i.test(err.message))) { + if ( + err?.response?.status === 404 || + err?.status === 404 || + (err?.message && /404|not found/i.test(err.message)) + ) { return { exists: false }; } throw err; @@ -531,7 +589,9 @@ export async function deletePattern(name: string): Promise { export async function fetchSecretTemplate(name: string): Promise { try { - return await fetchYAML(`${PATTERN_UI_CATALOG_BASE_URL}/${name}/values-secret.yaml.template`); + return await fetchYAML( + `${PATTERN_UI_CATALOG_BASE_URL}/${name}/values-secret.yaml.template`, + ); } catch { return null; // Template doesn't exist } diff --git a/console/src/components/InstallPatternPage.tsx b/console/src/components/InstallPatternPage.tsx index eb2d72d50..ed288490e 100644 --- a/console/src/components/InstallPatternPage.tsx +++ b/console/src/components/InstallPatternPage.tsx @@ -14,7 +14,6 @@ import { DescriptionListDescription, DescriptionListGroup, DescriptionListTerm, - ExpandableSection, Form, FormGroup, Label, @@ -23,32 +22,25 @@ import { TextInput, Title, } from '@patternfly/react-core'; -import { - Table, - Thead, - Tbody, - Tr, - Th, - Td, -} from '@patternfly/react-table'; +import { Table, Thead, Tbody, Tr, Th, Td } from '@patternfly/react-table'; import { k8sCreate } from '@openshift-console/dynamic-plugin-sdk'; import { fetchPattern, fetchPatternCR, fetchSecretTemplate, - fetchVaultJobStatus, triggerVaultInjection as apiTriggerVaultInjection, PATTERN_OPERATOR_NS, PatternCRStatus, - VaultJobStatus, - VaultInjectionRequest } from '../api'; -import { SecretTemplate, SecretFormData, SecretDefinition, SecretField } from '../types'; -import { GenerateField } from './SecretForm/GenerateField'; -import { PromptField } from './SecretForm/PromptField'; -import { FileField } from './SecretForm/FileField'; -import { IniField } from './SecretForm/IniField'; -import { StaticField } from './SecretForm/StaticField'; +import { useVaultJobPolling } from '../hooks/useVaultJobPolling'; +import { + buildVaultInjectionPayload, + getMissingFileAndIniFields, + secretTemplateHasFileOrIniFields, +} from '../vaultSecrets'; +import { SecretTemplate, SecretFormData } from '../types'; +import { SecretFormExpandableSections } from './SecretForm/SecretFormExpandableSections'; +import { VaultInjectionStatusAlert } from './SecretForm/VaultInjectionStatusAlert'; import './SecretForm/SecretForm.css'; const PatternModel = { @@ -85,8 +77,9 @@ export default function InstallPatternPage() { const [secretTemplate, setSecretTemplate] = React.useState(null); const [secretFormData, setSecretFormData] = React.useState({}); const [expandedSections, setExpandedSections] = React.useState>({}); - const [vaultJobStatus, setVaultJobStatus] = React.useState(null); - const [checkingVaultStatus, setCheckingVaultStatus] = React.useState(false); + const [secretsValidationAttempted, setSecretsValidationAttempted] = React.useState(false); + const { vaultJobStatus, setVaultJobStatus, checkingVaultStatus, checkVaultJobStatus } = + useVaultJobPolling(patternName); const [patternStatus, setPatternStatus] = React.useState(null); React.useEffect(() => { @@ -97,7 +90,7 @@ export default function InstallPatternPage() { console.log('🟢 [InstallPatternPage] Pattern data loaded successfully:', { patternName: patternData.name, repoUrl: patternData.repo_url, - hasSecretTemplate: !!template + hasSecretTemplate: !!template, }); setPatternName(patternData.name); @@ -114,7 +107,9 @@ export default function InstallPatternPage() { const initialExpanded: Record = {}; template.secrets.forEach((secret, index) => { - console.log(`🔧 [InstallPatternPage] Processing secret: ${secret.name} with ${secret.fields.length} fields`); + console.log( + `🔧 [InstallPatternPage] Processing secret: ${secret.name} with ${secret.fields.length} fields`, + ); initialData[secret.name] = {}; secret.fields.forEach((field) => { initialData[secret.name][field.name] = ''; @@ -126,7 +121,7 @@ export default function InstallPatternPage() { console.log('🔧 [InstallPatternPage] Secret form initialized:', { secretCount: template.secrets.length, initialData: Object.keys(initialData), - expandedSections: Object.keys(initialExpanded).filter(key => initialExpanded[key]) + expandedSections: Object.keys(initialExpanded).filter((key) => initialExpanded[key]), }); setSecretFormData(initialData); @@ -157,55 +152,23 @@ export default function InstallPatternPage() { console.log('✅ [InstallPatternPage] All required data present for vault injection:', { patternName, secretDataKeys: Object.keys(secretFormData), - templateSecrets: secretTemplate.secrets.map(s => s.name) + templateSecrets: secretTemplate.secrets.map((s) => s.name), }); try { - // Convert secretFormData to YAML format with proper structure for vault_load_secrets - const yaml = await import('js-yaml'); - console.log('🔄 [InstallPatternPage] Converting secretFormData to YAML:', secretFormData); - - // Build the v2.0 secrets list structure expected by parse_secrets_info - const secretsList = secretTemplate.secrets.map((secretDef) => { - const formValues = secretFormData[secretDef.name] || {}; - const secret: any = { name: secretDef.name }; - if (secretDef.vaultMount) secret.vaultMount = secretDef.vaultMount; - if (secretDef.vaultPrefixes) secret.vaultPrefixes = secretDef.vaultPrefixes; - secret.fields = secretDef.fields.map((fieldDef) => { - const field: any = { name: fieldDef.name }; - if (fieldDef.onMissingValue) field.onMissingValue = fieldDef.onMissingValue; - if (fieldDef.vaultPolicy) field.vaultPolicy = fieldDef.vaultPolicy; - if (fieldDef.base64) field.base64 = fieldDef.base64; - if (fieldDef.override) field.override = fieldDef.override; - const val = formValues[fieldDef.name]; - if (typeof val === 'string' && val !== '') { - field.value = val; - // User provided an explicit value, so don't auto-generate - if (fieldDef.onMissingValue === 'generate') { - delete field.onMissingValue; - delete field.vaultPolicy; - } - } - return field; - }); - return secret; - }); - - const vaultSecretStructure: SecretTemplate = { - version: '2.0', - secrets: secretsList, - vaultPolicies: secretTemplate?.vaultPolicies || null - }; - - const valuesSecretYaml = yaml.dump(vaultSecretStructure); - const templateYaml = JSON.stringify(secretTemplate, null, 2); - console.log('✅ [InstallPatternPage] Generated values YAML with vault structure:', valuesSecretYaml); - console.log('✅ [InstallPatternPage] Generated template YAML:', templateYaml); + const { valuesSecretYaml, fileArtifacts } = buildVaultInjectionPayload( + secretTemplate, + secretFormData, + ); + console.log( + '✅ [InstallPatternPage] Generated values YAML with vault structure:', + valuesSecretYaml, + ); - const request: VaultInjectionRequest = { + const request = { patternName, valuesSecretYaml, - templateYaml, + fileArtifacts, }; console.log('🚀 [InstallPatternPage] Triggering vault injection with request:', request); @@ -213,7 +176,9 @@ export default function InstallPatternPage() { console.log('📥 [InstallPatternPage] Vault injection result:', result); if (result.success) { - console.log('✅ [InstallPatternPage] Vault injection triggered successfully, starting job status polling'); + console.log( + '✅ [InstallPatternPage] Vault injection triggered successfully, starting job status polling', + ); // Start polling for job status setTimeout(() => { checkVaultJobStatus(); @@ -235,40 +200,13 @@ export default function InstallPatternPage() { } }, [patternName, secretFormData, secretTemplate]); - const checkVaultJobStatus = React.useCallback(async () => { - if (!patternName) { - console.log('🟡 [InstallPatternPage] No pattern name for vault job status check'); - return; - } - - try { - console.log('🔍 [InstallPatternPage] Checking vault job status for pattern:', patternName); - setCheckingVaultStatus(true); - const status = await fetchVaultJobStatus(patternName); - console.log('📋 [InstallPatternPage] Vault job status received:', status); - setVaultJobStatus(status); - - // Continue polling if job is still running or pending - if (status.status === 'running' || status.status === 'pending') { - console.log('⏳ [InstallPatternPage] Job still in progress, will poll again in 5 seconds'); - setTimeout(() => { - checkVaultJobStatus(); - }, 5000); // Poll every 5 seconds - } else { - console.log('✅ [InstallPatternPage] Job finished with status:', status.status); - } - } catch (err) { - console.error('🔴 [InstallPatternPage] Error checking vault job status:', err); - } finally { - setCheckingVaultStatus(false); - } - }, [patternName]); - // Check vault job status on component mount if secrets were configured React.useEffect(() => { const hasSecretData = secretFormData && Object.keys(secretFormData).length > 0; if (success && hasSecretData && secretTemplate && patternName) { - console.log('⏰ [InstallPatternPage] Pattern created successfully with secrets, starting vault job status check'); + console.log( + '⏰ [InstallPatternPage] Pattern created successfully with secrets, starting vault job status check', + ); const timer = setTimeout(() => { checkVaultJobStatus(); }, 2000); // Wait 2 seconds after pattern creation @@ -282,7 +220,9 @@ export default function InstallPatternPage() { // Initial fetch after a short delay to let the reconciler start const initialTimer = setTimeout(() => { - fetchPatternCR(patternName).then(setPatternStatus).catch(() => {}); + fetchPatternCR(patternName) + .then(setPatternStatus) + .catch(() => undefined); }, 3000); const interval = setInterval(async () => { @@ -319,9 +259,26 @@ export default function InstallPatternPage() { const handleSubmit = async () => { console.log('🚀 [InstallPatternPage] Starting pattern installation process'); - setSubmitting(true); + + if (secretTemplate) { + const missingUploads = getMissingFileAndIniFields(secretTemplate, secretFormData); + if (missingUploads.length > 0) { + setSecretsValidationAttempted(true); + setExpandedSections((prev) => { + const next = { ...prev }; + missingUploads.forEach(({ secretName }) => { + next[secretName] = true; + }); + return next; + }); + return; + } + } + setSecretsValidationAttempted(false); setSubmitError(null); + setSubmitting(true); + try { const hasSecrets = secretFormData && Object.keys(secretFormData).length > 0 && secretTemplate; console.log('📊 [InstallPatternPage] Installation details:', { @@ -330,7 +287,7 @@ export default function InstallPatternPage() { targetRepo, targetRevision, hasSecrets, - secretCount: hasSecrets ? Object.keys(secretFormData).length : 0 + secretCount: hasSecrets ? Object.keys(secretFormData).length : 0, }); const patternData: { apiVersion: string; @@ -357,7 +314,10 @@ export default function InstallPatternPage() { }, }; - console.log('🔧 [InstallPatternPage] Creating Pattern CR with data:', JSON.stringify(patternData, null, 2)); + console.log( + '🔧 [InstallPatternPage] Creating Pattern CR with data:', + JSON.stringify(patternData, null, 2), + ); await k8sCreate({ model: PatternModel, @@ -384,12 +344,10 @@ export default function InstallPatternPage() { }; // Secret form handling functions - const handleFieldChange = ( - secretName: string, - fieldName: string, - value: string | File | null, - ) => { - console.log(`🔄 [InstallPatternPage] Secret field changed: ${secretName}.${fieldName}`, { value: value instanceof File ? `[File: ${value.name}]` : value }); + const handleFieldChange = (secretName: string, fieldName: string, value: string | null) => { + console.log(`🔄 [InstallPatternPage] Secret field changed: ${secretName}.${fieldName}`, { + value: value, + }); setSecretFormData((prev) => ({ ...prev, [secretName]: { @@ -407,41 +365,6 @@ export default function InstallPatternPage() { })); }; - const getFieldType = (field: SecretField): 'generate' | 'prompt' | 'file' | 'ini' | 'static' => { - if (field.onMissingValue === 'generate') return 'generate'; - if (field.path) return 'file'; - if (field.ini_file) return 'ini'; - if (field.value !== undefined && field.value !== null) return 'static'; - return 'prompt'; // Default to prompt for required fields - }; - - const renderField = (secret: SecretDefinition, field: SecretField) => { - const fieldType = getFieldType(field); - const value = secretFormData[secret.name]?.[field.name] || ''; - - const commonProps = { - field, - value, - onChange: (newValue: string | File | null) => - handleFieldChange(secret.name, field.name, newValue), - }; - - switch (fieldType) { - case 'generate': - return ; - case 'prompt': - return ; - case 'file': - return ; - case 'ini': - return ; - case 'static': - return ; - default: - return ; - } - }; - if (loading) { return ( @@ -474,7 +397,8 @@ export default function InstallPatternPage() {

{(() => { - const hasSecrets = secretTemplate && secretFormData && Object.keys(secretFormData).length > 0; + const hasSecrets = + secretTemplate && secretFormData && Object.keys(secretFormData).length > 0; const reconcileComplete = patternStatus?.lastStep === 'reconcile complete'; const vaultDone = !hasSecrets || vaultJobStatus?.status === 'succeeded'; if (reconcileComplete && vaultDone) { @@ -502,7 +426,9 @@ export default function InstallPatternPage() { {t('Current Step')}

- {!patternStatus.lastError && } + {!patternStatus.lastError && ( + + )} {patternStatus.lastStep}
@@ -520,7 +446,9 @@ export default function InstallPatternPage() { {patternStatus.applications && patternStatus.applications.length > 0 && (
- {t('Applications')} + + {t('Applications')} + @@ -536,12 +464,30 @@ export default function InstallPatternPage() { @@ -552,44 +498,37 @@ export default function InstallPatternPage() { )} - {(!patternStatus.applications || patternStatus.applications.length === 0) && !patternStatus.lastError && ( -
- - {t('Waiting for ArgoCD applications to be created...')} -
- )} + {(!patternStatus.applications || patternStatus.applications.length === 0) && + !patternStatus.lastError && ( +
+ + {t('Waiting for ArgoCD applications to be created...')} +
+ )} )} )} {/* Vault injection status */} - {success && secretFormData && Object.keys(secretFormData).length > 0 && secretTemplate && vaultJobStatus && ( - -
- {(vaultJobStatus.status === 'running' || vaultJobStatus.status === 'pending' || checkingVaultStatus) && ( - - )} - {vaultJobStatus.message} -
- {vaultJobStatus.jobName && ( -

- {t('Job')}: {vaultJobStatus.jobName} -

- )} -
- )} + {success && + secretFormData && + Object.keys(secretFormData).length > 0 && + secretTemplate && + vaultJobStatus && ( + + )} {submitError && ( {submitError} @@ -638,38 +577,29 @@ export default function InstallPatternPage() { {/* Secrets Configuration Section */} {secretTemplate && ( - - {t('Configure secrets that will be injected into Vault for this pattern.')} + + {secretTemplateHasFileOrIniFields(secretTemplate) + ? t( + 'Configure secrets for this pattern. File and INI fields are required before install.', + ) + : t('Configure secrets that will be injected into Vault for this pattern.')} -
- {secretTemplate.secrets.map((secret) => ( - toggleSection(secret.name)} - className="patterns-operator__secret-section" - > - - {secret.name} - - {secret.fields.map((field) => ( - - {renderField(secret, field)} - - ))} - - - - ))} -
+
)} diff --git a/console/src/components/ManageSecretsPage.tsx b/console/src/components/ManageSecretsPage.tsx index 18174c812..ed22cf551 100644 --- a/console/src/components/ManageSecretsPage.tsx +++ b/console/src/components/ManageSecretsPage.tsx @@ -6,12 +6,7 @@ import { ActionGroup, Alert, Button, - Card, - CardBody, - CardTitle, - ExpandableSection, Form, - FormGroup, PageSection, Spinner, Title, @@ -19,18 +14,17 @@ import { import { fetchPattern, fetchSecretTemplate, - fetchVaultJobStatus, triggerVaultInjection as apiTriggerVaultInjection, - PATTERN_OPERATOR_NS, - VaultJobStatus, - VaultInjectionRequest, } from '../api'; -import { SecretTemplate, SecretFormData, SecretDefinition, SecretField } from '../types'; -import { GenerateField } from './SecretForm/GenerateField'; -import { PromptField } from './SecretForm/PromptField'; -import { FileField } from './SecretForm/FileField'; -import { IniField } from './SecretForm/IniField'; -import { StaticField } from './SecretForm/StaticField'; +import { useVaultJobPolling } from '../hooks/useVaultJobPolling'; +import { + buildVaultInjectionPayload, + getMissingFileAndIniFields, + secretTemplateHasFileOrIniFields, +} from '../vaultSecrets'; +import { SecretTemplate, SecretFormData } from '../types'; +import { SecretFormExpandableSections } from './SecretForm/SecretFormExpandableSections'; +import { VaultInjectionStatusAlert } from './SecretForm/VaultInjectionStatusAlert'; import './SecretForm/SecretForm.css'; export default function ManageSecretsPage() { @@ -50,11 +44,11 @@ export default function ManageSecretsPage() { const [secretTemplate, setSecretTemplate] = React.useState(null); const [secretFormData, setSecretFormData] = React.useState({}); const [expandedSections, setExpandedSections] = React.useState>({}); - const [vaultJobStatus, setVaultJobStatus] = React.useState(null); - const [checkingVaultStatus, setCheckingVaultStatus] = React.useState(false); + const [secretsValidationAttempted, setSecretsValidationAttempted] = React.useState(false); + const { vaultJobStatus, setVaultJobStatus, checkingVaultStatus, checkVaultJobStatus } = + useVaultJobPolling(patternName); React.useEffect(() => { - Promise.all([fetchPattern(name), fetchSecretTemplate(name)]) .then(([patternData, template]) => { setPatternName(patternData.name); @@ -86,72 +80,38 @@ export default function ManageSecretsPage() { }); }, [name]); - const checkVaultJobStatus = React.useCallback(async () => { - if (!patternName) return; - - try { - setCheckingVaultStatus(true); - const status = await fetchVaultJobStatus(patternName); - setVaultJobStatus(status); - - if (status.status === 'running' || status.status === 'pending') { - setTimeout(() => { - checkVaultJobStatus(); - }, 5000); - } - } catch (err) { - console.error('Error checking vault job status:', err); - } finally { - setCheckingVaultStatus(false); - } - }, [patternName]); - const handleSubmit = async () => { - setSubmitting(true); - setSubmitError(null); + if (!secretTemplate) return; + setSuccess(false); setVaultJobStatus(null); - try { - const yaml = await import('js-yaml'); - - const secretsList = secretTemplate.secrets.map((secretDef) => { - const formValues = secretFormData[secretDef.name] || {}; - const secret: any = { name: secretDef.name }; - if (secretDef.vaultMount) secret.vaultMount = secretDef.vaultMount; - if (secretDef.vaultPrefixes) secret.vaultPrefixes = secretDef.vaultPrefixes; - secret.fields = secretDef.fields.map((fieldDef) => { - const field: any = { name: fieldDef.name }; - if (fieldDef.onMissingValue) field.onMissingValue = fieldDef.onMissingValue; - if (fieldDef.vaultPolicy) field.vaultPolicy = fieldDef.vaultPolicy; - if (fieldDef.base64) field.base64 = fieldDef.base64; - if (fieldDef.override) field.override = fieldDef.override; - const val = formValues[fieldDef.name]; - if (typeof val === 'string' && val !== '') { - field.value = val; - if (fieldDef.onMissingValue === 'generate') { - delete field.onMissingValue; - delete field.vaultPolicy; - } - } - return field; + const missingUploads = getMissingFileAndIniFields(secretTemplate, secretFormData); + if (missingUploads.length > 0) { + setSecretsValidationAttempted(true); + setExpandedSections((prev) => { + const next = { ...prev }; + missingUploads.forEach(({ secretName }) => { + next[secretName] = true; }); - return secret; + return next; }); + return; + } + setSecretsValidationAttempted(false); + setSubmitError(null); - const vaultSecretStructure: SecretTemplate = { - version: '2.0', - secrets: secretsList, - vaultPolicies: secretTemplate?.vaultPolicies || null - }; - - const valuesSecretYaml = yaml.dump(vaultSecretStructure); - const templateYaml = JSON.stringify(secretTemplate, null, 2); + setSubmitting(true); + try { + const { valuesSecretYaml, fileArtifacts } = buildVaultInjectionPayload( + secretTemplate, + secretFormData, + ); - const request: VaultInjectionRequest = { + const request = { patternName, valuesSecretYaml, - templateYaml, + fileArtifacts, }; const result = await apiTriggerVaultInjection(request); @@ -171,11 +131,7 @@ export default function ManageSecretsPage() { } }; - const handleFieldChange = ( - secretName: string, - fieldName: string, - value: string | File | null, - ) => { + const handleFieldChange = (secretName: string, fieldName: string, value: string | null) => { setSecretFormData((prev) => ({ ...prev, [secretName]: { @@ -192,41 +148,6 @@ export default function ManageSecretsPage() { })); }; - const getFieldType = (field: SecretField): 'generate' | 'prompt' | 'file' | 'ini' | 'static' => { - if (field.onMissingValue === 'generate') return 'generate'; - if (field.path) return 'file'; - if (field.ini_file) return 'ini'; - if (field.value !== undefined && field.value !== null) return 'static'; - return 'prompt'; - }; - - const renderField = (secret: SecretDefinition, field: SecretField) => { - const fieldType = getFieldType(field); - const value = secretFormData[secret.name]?.[field.name] || ''; - - const commonProps = { - field, - value, - onChange: (newValue: string | File | null) => - handleFieldChange(secret.name, field.name, newValue), - }; - - switch (fieldType) { - case 'generate': - return ; - case 'prompt': - return ; - case 'file': - return ; - case 'ini': - return ; - case 'static': - return ; - default: - return ; - } - }; - if (loading) { return ( @@ -264,9 +185,7 @@ export default function ManageSecretsPage() { {t('Manage Secrets')} - - {t('Manage Secrets for {{displayName}}', { displayName })} - + {t('Manage Secrets for {{displayName}}', { displayName })} {success && ( @@ -275,30 +194,11 @@ export default function ManageSecretsPage() {
)} {success && vaultJobStatus && ( - -
- {(vaultJobStatus.status === 'running' || vaultJobStatus.status === 'pending' || checkingVaultStatus) && ( - - )} - {vaultJobStatus.message} -
- {vaultJobStatus.jobName && ( -

- {t('Job')}: {vaultJobStatus.jobName} -

- )} -
+ vaultJobStatus={vaultJobStatus} + checkingVaultStatus={checkingVaultStatus} + /> )} {submitError && ( @@ -313,45 +213,25 @@ export default function ManageSecretsPage() { }} > - {t('Enter or update the secrets that will be injected into Vault for this pattern. Fields left empty will retain their existing values in Vault.')} + {secretTemplateHasFileOrIniFields(secretTemplate) + ? t( + 'Enter or update the secrets that will be injected into Vault for this pattern. File and INI fields must be uploaded each time. Other fields may be left empty to keep existing Vault values.', + ) + : t( + 'Enter or update the secrets that will be injected into Vault for this pattern. Fields left empty will retain their existing values in Vault.', + )} -
- {secretTemplate.secrets.map((secret) => ( - toggleSection(secret.name)} - className="patterns-operator__secret-section" - > - - {secret.name} - - {secret.fields.map((field) => ( - - {renderField(secret, field)} - - ))} - - - - ))} -
+ - )} {!isInstalled && (
{app.name} {app.namespace} - -