diff --git a/src/api-v2.authz.test.ts b/src/api-v2.authz.test.ts index e6db0bdab..05cc28879 100644 --- a/src/api-v2.authz.test.ts +++ b/src/api-v2.authz.test.ts @@ -491,10 +491,18 @@ describe('API V2 authz tests', () => { }) describe('V2 Sealed Secret Endpoints', () => { - const secretData = createTeamResource('AplTeamSecret', { - type: 'kubernetes.io/opaque', - encryptedData: { key: 'value' }, - }) + const secretData = { + kind: 'SealedSecret', + metadata: { + name: 'test-secret', + }, + spec: { + encryptedData: { key: 'value' }, + template: { + type: 'kubernetes.io/opaque', + }, + }, + } describe('Platform Admin', () => { test('platform admin can get all sealed secrets', async () => { diff --git a/src/api/v2/teams/{teamId}/sealedsecrets.ts b/src/api/v2/teams/{teamId}/sealedsecrets.ts index 3e1fbc1f3..408cb71b1 100644 --- a/src/api/v2/teams/{teamId}/sealedsecrets.ts +++ b/src/api/v2/teams/{teamId}/sealedsecrets.ts @@ -1,12 +1,12 @@ import Debug from 'debug' import { Response } from 'express' -import { AplSecretRequest, OpenApiRequestExt } from 'src/otomi-models' +import { OpenApiRequestExt, SealedSecretManifestRequest } from 'src/otomi-models' const debug = Debug('otomi:api:v2:teams:sealedsecrets') /** * GET /v2/teams/{teamId}/sealedsecrets - * Get all sealed secrets for a team (APL format) + * Get all sealed secrets for a team (SealedSecret manifest format) */ export const getAplSealedSecrets = (req: OpenApiRequestExt, res: Response): void => { const { teamId } = req.params @@ -17,11 +17,11 @@ export const getAplSealedSecrets = (req: OpenApiRequestExt, res: Response): void /** * POST /v2/teams/{teamId}/sealedsecrets - * Create a new sealed secret (APL format) + * Create a new sealed secret (SealedSecret manifest format) */ export const createAplSealedSecret = async (req: OpenApiRequestExt, res: Response): Promise => { const { teamId } = req.params debug(`createSealedSecret(${teamId}, ...)`) - const v = await req.otomi.createAplSealedSecret(decodeURIComponent(teamId), req.body as AplSecretRequest) + const v = await req.otomi.createAplSealedSecret(decodeURIComponent(teamId), req.body as SealedSecretManifestRequest) res.json(v) } diff --git a/src/api/v2/teams/{teamId}/sealedsecrets/{sealedSecretName}.ts b/src/api/v2/teams/{teamId}/sealedsecrets/{sealedSecretName}.ts index 22a47b60d..1c2c6c1ab 100644 --- a/src/api/v2/teams/{teamId}/sealedsecrets/{sealedSecretName}.ts +++ b/src/api/v2/teams/{teamId}/sealedsecrets/{sealedSecretName}.ts @@ -1,12 +1,12 @@ import Debug from 'debug' import { Response } from 'express' -import { AplSecretRequest, DeepPartial, OpenApiRequestExt } from 'src/otomi-models' +import { DeepPartial, OpenApiRequestExt, SealedSecretManifestRequest } from 'src/otomi-models' const debug = Debug('otomi:api:v2:teams:sealedsecrets') /** * GET /v2/teams/{teamId}/sealedsecrets/{sealedSecretName} - * Get a specific sealed secret (APL format) + * Get a specific sealed secret (SealedSecret manifest format) */ export const getAplSealedSecret = async (req: OpenApiRequestExt, res: Response): Promise => { const { teamId, sealedSecretName } = req.params @@ -17,7 +17,7 @@ export const getAplSealedSecret = async (req: OpenApiRequestExt, res: Response): /** * PUT /v2/teams/{teamId}/sealedsecrets/{sealedSecretName} - * Edit a sealed secret (APL format) + * Edit a sealed secret (SealedSecret manifest format) */ export const editAplSealedSecret = async (req: OpenApiRequestExt, res: Response): Promise => { const { teamId, sealedSecretName } = req.params @@ -25,14 +25,14 @@ export const editAplSealedSecret = async (req: OpenApiRequestExt, res: Response) const data = await req.otomi.editAplSealedSecret( decodeURIComponent(teamId), decodeURIComponent(sealedSecretName), - req.body as AplSecretRequest, + req.body as SealedSecretManifestRequest, ) res.json(data) } /** * PATCH /v2/teams/{teamId}/sealedsecrets/{sealedSecretName} - * Partially update a sealed secret (APL format) + * Partially update a sealed secret (SealedSecret manifest format) */ export const patchAplSealedSecret = async (req: OpenApiRequestExt, res: Response): Promise => { const { teamId, sealedSecretName } = req.params @@ -40,7 +40,7 @@ export const patchAplSealedSecret = async (req: OpenApiRequestExt, res: Response const data = await req.otomi.editAplSealedSecret( decodeURIComponent(teamId), decodeURIComponent(sealedSecretName), - req.body as DeepPartial, + req.body as DeepPartial, true, ) res.json(data) diff --git a/src/app.ts b/src/app.ts index a1f86fd31..0e6adea4f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -20,7 +20,7 @@ import { } from 'src/middleware' import { apiRateLimiter, authRateLimiter } from 'src/middleware/rate-limit' import { setMockIdx } from 'src/mocks' -import { AplResponseObject, OpenAPIDoc, Schema } from 'src/otomi-models' +import { AplResponseObject, OpenAPIDoc, Schema, SealedSecretManifestResponse } from 'src/otomi-models' import { default as OtomiStack } from 'src/otomi-stack' import { extract, getPaths, getValuesSchema } from 'src/utils' import { @@ -89,7 +89,7 @@ const resourceStatus = async (errorSet) => { } const { cluster } = otomiStack.getSettings(['cluster']) const domainSuffix = cluster?.domainSuffix - const resources: Record = { + const resources: Record = { workloads: otomiStack.getAllAplWorkloads(), builds: otomiStack.getAllAplBuilds(), services: otomiStack.getAllAplServices(), diff --git a/src/fileStore/file-map.ts b/src/fileStore/file-map.ts index f9a880536..5c497155e 100644 --- a/src/fileStore/file-map.ts +++ b/src/fileStore/file-map.ts @@ -163,8 +163,8 @@ export function getFileMaps(envDir: string): Map { name: 'services', }) - maps.set('AplTeamSecret', { - kind: 'AplTeamSecret', + maps.set('SealedSecret', { + kind: 'SealedSecret', envDir, pathGlob: `${envDir}/env/teams/*/sealedsecrets/*.yaml`, pathTemplate: 'env/teams/{teamId}/sealedsecrets/{name}.yaml', diff --git a/src/k8s_operations.ts b/src/k8s_operations.ts index 9782692c9..67f2bc8f4 100644 --- a/src/k8s_operations.ts +++ b/src/k8s_operations.ts @@ -11,7 +11,7 @@ import Debug from 'debug' import * as fs from 'fs' import * as yaml from 'js-yaml' import { promisify } from 'util' -import { AplBuildResponse, AplSecretResponse, AplServiceResponse, AplWorkloadResponse } from './otomi-models' +import { AplBuildResponse, AplServiceResponse, AplWorkloadResponse, SealedSecretManifestResponse } from './otomi-models' const debug = Debug('otomi:api:k8sOperations') @@ -433,10 +433,10 @@ export async function getSealedSecretSyncedStatus(name: string, namespace: strin } } -export async function getSealedSecretStatus(sealedsecret: AplSecretResponse): Promise { +export async function getSealedSecretStatus(sealedsecret: SealedSecretManifestResponse): Promise { const { name, labels } = sealedsecret.metadata const teamName = labels['apl.io/teamId'] - const namespace = sealedsecret.spec.namespace ?? `team-${teamName}` + const namespace = sealedsecret.spec.template?.metadata?.namespace ?? `team-${teamName}` const value = await getSecretValues(name, namespace) const syncedStatus = await getSealedSecretSyncedStatus(name, namespace) diff --git a/src/openapi/api.yaml b/src/openapi/api.yaml index d646dd13d..c18a6525a 100644 --- a/src/openapi/api.yaml +++ b/src/openapi/api.yaml @@ -730,7 +730,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/AplSecretResponse' + $ref: '#/components/schemas/SealedSecretManifestResponse' '400': $ref: '#/components/responses/BadRequest' @@ -750,7 +750,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/AplSecretResponse' + $ref: '#/components/schemas/SealedSecretManifestResponse' '400': description: Bad Request content: @@ -766,7 +766,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AplSecretRequest' + $ref: '#/components/schemas/SealedSecretManifestRequest' description: SealedSecret object required: true responses: @@ -779,7 +779,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AplSecretResponse' + $ref: '#/components/schemas/SealedSecretManifestResponse' '/v2/teams/{teamId}/sealedsecrets/{sealedSecretName}': parameters: @@ -800,7 +800,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AplSecretResponse' + $ref: '#/components/schemas/SealedSecretManifestResponse' put: operationId: editAplSealedSecret x-eov-operation-handler: v2/teams/{teamId}/sealedsecrets/{sealedSecretName} @@ -810,7 +810,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AplSecretRequest' + $ref: '#/components/schemas/SealedSecretManifestRequest' description: SealedSecret object that contains updated values required: true responses: @@ -823,7 +823,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AplSecretResponse' + $ref: '#/components/schemas/SealedSecretManifestResponse' delete: operationId: deleteAplSealedSecret x-eov-operation-handler: v2/teams/{teamId}/sealedsecrets/{sealedSecretName} @@ -2833,6 +2833,16 @@ components: properties: name: $ref: 'definitions.yaml#/idName' + namespace: + $ref: 'definitions.yaml#/idName' + annotations: + type: object + additionalProperties: + type: string + labels: + type: object + additionalProperties: + type: string required: - name required: @@ -2845,6 +2855,12 @@ components: properties: name: $ref: 'definitions.yaml#/idName' + namespace: + $ref: 'definitions.yaml#/idName' + annotations: + type: object + additionalProperties: + type: string labels: type: object properties: @@ -3014,25 +3030,28 @@ components: - $ref: '#/components/schemas/AplPolicy' - $ref: '#/components/schemas/aplTeamMetadata' - $ref: '#/components/schemas/aplStatusResponse' - AplSecret: + SealedSecretManifest: type: object properties: + apiVersion: + type: string + enum: ['bitnami.com/v1alpha1'] kind: type: string - enum: [AplTeamSecret] + enum: [SealedSecret] spec: - $ref: 'sealedsecret.yaml#/AplSealedSecretSpec' + $ref: '#/components/schemas/SealedSecretManifestSpec' required: - kind - spec - AplSecretRequest: + SealedSecretManifestRequest: allOf: - - $ref: '#/components/schemas/AplSecret' + - $ref: '#/components/schemas/SealedSecretManifest' - $ref: '#/components/schemas/aplMetadata' - AplSecretResponse: + SealedSecretManifestResponse: type: object allOf: - - $ref: '#/components/schemas/AplSecret' + - $ref: '#/components/schemas/SealedSecretManifest' - $ref: '#/components/schemas/aplTeamMetadata' - $ref: '#/components/schemas/aplStatusResponse' AplService: @@ -3139,6 +3158,8 @@ components: $ref: 'policies.yaml#/Policies' SealedSecret: $ref: 'sealedsecret.yaml#/SealedSecret' + SealedSecretManifestSpec: + $ref: 'sealedsecret.yaml#/SealedSecretManifestSpec' SealedSecretsKeys: $ref: 'sealedsecretskeys.yaml#/SealedSecretsKeys' K8sSecret: diff --git a/src/openapi/sealedsecret.yaml b/src/openapi/sealedsecret.yaml index 84bc92cb5..d1b1a553b 100644 --- a/src/openapi/sealedsecret.yaml +++ b/src/openapi/sealedsecret.yaml @@ -21,85 +21,73 @@ SealedSecret: $ref: 'definitions.yaml#/idName' namespace: $ref: 'definitions.yaml#/idName' - immutable: - description: 'Immutable, if set to true, ensures that data stored in the Secret cannot be updated (only object metadata can be modified).' - type: boolean - type: - description: Used to facilitate programmatic handling of secret data. - type: string - default: kubernetes.io/opaque - enum: - - kubernetes.io/opaque - - kubernetes.io/dockercfg - - kubernetes.io/dockerconfigjson - - kubernetes.io/basic-auth - - kubernetes.io/ssh-auth - - kubernetes.io/tls encryptedData: type: object - properties: - additionalProperties: - type: string - metadata: - description: 'Standard objects metadata.' - properties: - annotations: - type: object - properties: - additionalProperties: - type: string - finalizers: - type: array - items: - type: string - labels: - type: object - properties: - additionalProperties: - type: string + additionalProperties: + type: string + template: + $ref: '#/SealedSecretTemplate' required: - name - type - encryptedData type: object -AplSealedSecretSpec: +# Secret type enum - reusable +SecretType: + type: string + default: kubernetes.io/opaque + enum: + - kubernetes.io/opaque + - kubernetes.io/dockercfg + - kubernetes.io/dockerconfigjson + - kubernetes.io/basic-auth + - kubernetes.io/ssh-auth + - kubernetes.io/tls + +# Template metadata schema for K8s SealedSecret +SealedSecretTemplateMetadata: type: object properties: + name: + $ref: 'definitions.yaml#/idName' namespace: $ref: 'definitions.yaml#/idName' + annotations: + type: object + additionalProperties: + type: string + labels: + type: object + additionalProperties: + type: string + finalizers: + type: array + items: + type: string + +# Template schema for K8s SealedSecret +SealedSecretTemplate: + type: object + properties: + type: + $ref: '#/SecretType' immutable: description: 'Immutable, if set to true, ensures that data stored in the Secret cannot be updated (only object metadata can be modified).' type: boolean - type: - description: Used to facilitate programmatic handling of secret data. - type: string - default: kubernetes.io/opaque - enum: - - kubernetes.io/opaque - - kubernetes.io/dockercfg - - kubernetes.io/dockerconfigjson - - kubernetes.io/basic-auth - - kubernetes.io/ssh-auth - - kubernetes.io/tls + default: false + metadata: + $ref: '#/SealedSecretTemplateMetadata' + +# The raw Kubernetes SealedSecret spec (nested inside APL spec) +SealedSecretManifestSpec: + type: object + properties: encryptedData: type: object additionalProperties: type: string - metadata: - description: 'Standard objects metadata.' - properties: - annotations: - type: object - additionalProperties: - type: string - labels: - type: object - additionalProperties: - type: string - finalizers: - type: array - items: - type: string + template: + $ref: '#/SealedSecretTemplate' required: - - type + - encryptedData diff --git a/src/otomi-models.ts b/src/otomi-models.ts index 3ac2098f1..76dad9bc4 100644 --- a/src/otomi-models.ts +++ b/src/otomi-models.ts @@ -17,8 +17,10 @@ export type Netpol = components['schemas']['Netpol'] export type AplNetpolRequest = components['schemas']['AplNetpolRequest'] export type AplNetpolResponse = components['schemas']['AplNetpolResponse'] export type SealedSecret = components['schemas']['SealedSecret'] -export type AplSecretRequest = components['schemas']['AplSecretRequest'] -export type AplSecretResponse = components['schemas']['AplSecretResponse'] +export type SealedSecretManifest = components['schemas']['SealedSecretManifest'] +export type SealedSecretManifestSpec = components['schemas']['SealedSecretManifestSpec'] +export type SealedSecretManifestRequest = components['schemas']['SealedSecretManifestRequest'] +export type SealedSecretManifestResponse = components['schemas']['SealedSecretManifestResponse'] export type SealedSecretsKeys = components['schemas']['SealedSecretsKeys'] export type K8sSecret = components['schemas']['K8sSecret'] export type Service = components['schemas']['Service'] @@ -78,7 +80,7 @@ export type AplRequestObject = | AplAgentRequest | AplNetpolRequest | AplPolicyRequest - | AplSecretRequest + | SealedSecretManifestRequest | AplServiceRequest | AplWorkloadRequest | AplTeamSettingsRequest @@ -89,7 +91,7 @@ export type AplResponseObject = | AplAgentResponse | AplNetpolResponse | AplPolicyResponse - | AplSecretResponse + | SealedSecretManifestResponse | AplServiceResponse | AplWorkloadResponse | AplTeamSettingsResponse @@ -115,7 +117,7 @@ export const APL_KINDS = [ 'AplTeamPolicy', 'AplTeamSettingSet', 'AplTeamNetworkControl', - 'AplTeamSecret', + 'SealedSecret', 'AplTeamService', 'AplTeamWorkload', 'AplTeamWorkloadValues', @@ -311,7 +313,7 @@ export interface TeamConfig { agents: AplAgentResponse[] netpols: AplNetpolResponse[] policies: AplPolicyResponse[] - sealedsecrets: AplSecretResponse[] + sealedsecrets: SealedSecretManifestResponse[] services: AplServiceResponse[] settings: AplTeamSettingsResponse workloads: AplWorkloadResponse[] diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index f8babbbd9..a14e57ac7 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -39,8 +39,6 @@ import { AplPolicyRequest, AplPolicyResponse, AplRecord, - AplSecretRequest, - AplSecretResponse, AplServiceRequest, AplServiceResponse, AplTeamObject, @@ -62,6 +60,8 @@ import { Policies, Policy, SealedSecret, + SealedSecretManifestRequest, + SealedSecretManifestResponse, Service, ServiceSpec, Session, @@ -129,12 +129,7 @@ import { testPublicRepoConnect, } from './utils/codeRepoUtils' import { getAplObjectFromV1, getV1MergeObject, getV1ObjectFromApl } from './utils/manifests' -import { - getSealedSecretsPEM, - sealedSecretManifest, - SealedSecretManifestType, - toSealedSecretResponse, -} from './utils/sealedSecretUtils' +import { ensureSealedSecretMetadata, getSealedSecretsPEM, sealedSecretManifest } from './utils/sealedSecretUtils' import { getKeycloakUsers, isValidUsername } from './utils/userUtils' import { defineClusterId, ObjectStorageClient } from './utils/wizardUtils' import { @@ -834,11 +829,10 @@ export default class OtomiStack { return aplRecord } - async saveTeamSealedSecret(teamId: string, data: AplSecretRequest): Promise { + async saveTeamSealedSecret(teamId: string, data: SealedSecretManifestRequest): Promise { debug(`Saving sealed secrets of team: ${teamId}`) const { metadata } = data - const aplObject = toTeamObject(teamId, data) as AplSecretResponse - const sealedSecretChartValues = sealedSecretManifest(aplObject) + const sealedSecretChartValues = sealedSecretManifest(teamId, data) const aplRecord = this.fileStore.set( getTeamSealedSecretsValuesFilePath(teamId, metadata.name), sealedSecretChartValues, @@ -2103,98 +2097,171 @@ export default class OtomiStack { } async createSealedSecret(teamId: string, data: SealedSecret): Promise { - const newSecret = await this.createAplSealedSecret( - teamId, - getAplObjectFromV1('AplTeamSecret', data) as AplSecretRequest, - ) - return getV1ObjectFromApl(newSecret) as SealedSecret + // Convert V1 format to SealedSecretManifestRequest + const request: SealedSecretManifestRequest = { + kind: 'SealedSecret', + metadata: { name: data.name }, + spec: { + encryptedData: data.encryptedData || {}, + template: { + type: data.template?.type, + immutable: data.template?.immutable, + metadata: data.template?.metadata, + }, + }, + } + const manifest = await this.createAplSealedSecret(teamId, request) + // Convert back to V1 format + return { + name: manifest.metadata.name, + namespace: manifest.spec.template?.metadata?.namespace, + encryptedData: manifest.spec.encryptedData, + template: manifest.spec.template, + } as SealedSecret } - async createAplSealedSecret(teamId: string, data: AplSecretRequest): Promise { + async createAplSealedSecret( + teamId: string, + data: SealedSecretManifestRequest, + ): Promise { if (data.metadata.name.length < 2) throw new ValidationError('Secret name must be at least 2 characters long') - if (this.fileStore.getTeamResource(data.kind, teamId, data.metadata.name)) { + if (this.fileStore.getTeamResource('SealedSecret', teamId, data.metadata.name)) { throw new AlreadyExists('SealedSecret name already exists') } const aplRecord = await this.saveTeamSealedSecret(teamId, data) await this.doDeployment(aplRecord, false) - return aplRecord.content as AplSecretResponse + return aplRecord.content as unknown as SealedSecretManifestResponse } async editSealedSecret(teamId: string, name: string, data: SealedSecret): Promise { - const mergeObj = getV1MergeObject(data) as DeepPartial - const mergedSecret = await this.editAplSealedSecret(teamId, name, mergeObj) - return getV1ObjectFromApl(mergedSecret) as SealedSecret + // Convert V1 format to SealedSecretManifestRequest + const request: DeepPartial = { + kind: 'SealedSecret', + metadata: { name }, + spec: { + encryptedData: data.encryptedData, + template: { + type: data.template?.type, + immutable: data.template?.immutable, + metadata: data.template?.metadata, + }, + }, + } + const manifest = await this.editAplSealedSecret(teamId, name, request) + // Convert back to V1 format + return { + name: manifest.metadata.name, + namespace: manifest.spec.template?.metadata?.namespace, + encryptedData: manifest.spec.encryptedData, + template: manifest.spec.template, + } as SealedSecret } async editAplSealedSecret( teamId: string, name: string, - data: DeepPartial, + data: DeepPartial, patch = false, - ): Promise { + ): Promise { const existing = await this.getAplSealedSecret(teamId, name) - const namespace = data.spec?.namespace ?? existing.spec.namespace ?? `team-${teamId}` - const updatedSpec = patch - ? merge(cloneDeep(existing.spec), { - encryptedData: data.spec?.encryptedData, - namespace, - }) - : ({ - ...existing.spec, - encryptedData: data.spec?.encryptedData, - namespace, - immutable: data.spec?.immutable ?? existing.spec.immutable, - metadata: data.spec?.metadata ?? existing.spec.metadata, - } as SealedSecret) - - const aplRecord = await this.saveTeamSealedSecret(teamId, { - kind: existing.kind, - metadata: existing.metadata, - spec: updatedSpec, - }) + let updatedRequest: SealedSecretManifestRequest + if (patch) { + // Merge mode: merge encryptedData + updatedRequest = { + kind: 'SealedSecret', + metadata: { name }, + spec: { + encryptedData: merge( + cloneDeep(existing.spec.encryptedData || {}), + (data.spec?.encryptedData || {}) as Record, + ), + template: (data.spec?.template ?? existing.spec.template) as SealedSecretManifestRequest['spec']['template'], + }, + } + } else { + // Replace mode: use provided encryptedData or existing + updatedRequest = { + kind: 'SealedSecret', + metadata: { name }, + spec: { + encryptedData: ((data.spec?.encryptedData ?? existing.spec.encryptedData) || {}) as Record, + template: { + type: data.spec?.template?.type ?? existing.spec.template?.type, + immutable: data.spec?.template?.immutable ?? existing.spec.template?.immutable, + metadata: (data.spec?.template?.metadata ?? existing.spec.template?.metadata) as + | { annotations?: Record; labels?: Record; finalizers?: string[] } + | undefined, + }, + }, + } + } + + const aplRecord = await this.saveTeamSealedSecret(teamId, updatedRequest) await this.doDeployment(aplRecord, false) - return aplRecord.content as AplSecretResponse + return aplRecord.content as unknown as SealedSecretManifestResponse } async deleteSealedSecret(teamId: string, name: string): Promise { - const filePath = this.fileStore.deleteTeamResource('AplTeamSecret', teamId, name) + const filePath = this.fileStore.deleteTeamResource('SealedSecret', teamId, name) await this.git.removeFile(filePath) await this.doDeleteDeployment([filePath]) } async getSealedSecret(teamId: string, name: string): Promise { - const aplSecret = await this.getAplSealedSecret(teamId, name) - return getV1ObjectFromApl(aplSecret) as SealedSecret + const manifest = await this.getAplSealedSecret(teamId, name) + // Convert to V1 format + return { + name: manifest.metadata.name, + namespace: manifest.spec.template?.metadata?.namespace, + encryptedData: manifest.spec.encryptedData, + template: manifest.spec.template, + } as SealedSecret } - async getAplSealedSecret(teamId: string, name: string): Promise { - const sealedSecret = this.fileStore.getTeamResource('AplTeamSecret', teamId, name) + async getAplSealedSecret(teamId: string, name: string): Promise { + const sealedSecret = this.fileStore.getTeamResource('SealedSecret', teamId, name) if (!sealedSecret) { throw new NotExistError(`SealedSecret ${name} not found in team ${teamId}`) } - return toSealedSecretResponse(sealedSecret as SealedSecretManifestType) + return ensureSealedSecretMetadata(sealedSecret as SealedSecretManifestResponse, teamId) } getAllSealedSecrets(): SealedSecret[] { - return this.getAllAplSealedSecrets().map(getV1ObjectFromApl) as SealedSecret[] + return this.getAllAplSealedSecrets().map((manifest) => ({ + name: manifest.metadata.name, + namespace: manifest.spec.template?.metadata?.namespace, + encryptedData: manifest.spec.encryptedData, + template: manifest.spec.template, + teamId: manifest.metadata.labels?.['apl.io/teamId'], + })) as SealedSecret[] } - getAllAplSealedSecrets(): AplSecretResponse[] { - const files = this.fileStore.getAllTeamResourcesByKind('AplTeamSecret') - return Array.from(files.values()).map((secret) => toSealedSecretResponse(secret as SealedSecretManifestType)) + getAllAplSealedSecrets(): SealedSecretManifestResponse[] { + const files = this.fileStore.getAllTeamResourcesByKind('SealedSecret') + return Array.from(files.values()).map((secret) => { + const manifest = secret as SealedSecretManifestResponse + // Derive teamId from namespace (format: team-{teamId}) + const teamId = manifest.metadata.namespace?.replace(/^team-/, '') || manifest.metadata.labels?.['apl.io/teamId'] + return teamId ? ensureSealedSecretMetadata(manifest, teamId) : manifest + }) } getSealedSecrets(teamId: string): SealedSecret[] { - return this.getAplSealedSecrets(teamId).map((secret) => ({ - ...getV1ObjectFromApl(secret), + return this.getAplSealedSecrets(teamId).map((manifest) => ({ + name: manifest.metadata.name, + namespace: manifest.spec.template?.metadata?.namespace, + encryptedData: manifest.spec.encryptedData, + template: manifest.spec.template, teamId, })) as SealedSecret[] } - getAplSealedSecrets(teamId: string): AplSecretResponse[] { - const files = this.fileStore.getTeamResourcesByKindAndTeamId('AplTeamSecret', teamId) - return Array.from(files.values()).map((secret) => toSealedSecretResponse(secret as SealedSecretManifestType)) + getAplSealedSecrets(teamId: string): SealedSecretManifestResponse[] { + const files = this.fileStore.getTeamResourcesByKindAndTeamId('SealedSecret', teamId) + return Array.from(files.values()).map((secret) => + ensureSealedSecretMetadata(secret as SealedSecretManifestResponse, teamId), + ) } async getSecretsFromK8s(teamId: string): Promise> { diff --git a/src/utils/sealedSecretUtils.ts b/src/utils/sealedSecretUtils.ts index 47d541cbb..fef34194f 100644 --- a/src/utils/sealedSecretUtils.ts +++ b/src/utils/sealedSecretUtils.ts @@ -1,59 +1,31 @@ import { X509Certificate } from 'crypto' import { isEmpty } from 'lodash' -import { AplSecretResponse } from 'src/otomi-models' +import { SealedSecretManifestRequest, SealedSecretManifestResponse } from 'src/otomi-models' import { ValidationError } from '../error' import { getSealedSecretsCertificate } from '../k8s_operations' -export interface SealedSecretManifestType { - apiVersion: string - kind: string - metadata: { - name: string - namespace: string - annotations?: Record - finalizers?: string[] - labels?: Record - } - spec: { - encryptedData: Record - template: { - type: - | 'kubernetes.io/opaque' - | 'kubernetes.io/dockercfg' - | 'kubernetes.io/dockerconfigjson' - | 'kubernetes.io/basic-auth' - | 'kubernetes.io/ssh-auth' - | 'kubernetes.io/tls' - immutable: boolean - metadata: { - name: string - namespace: string - annotations?: Record - finalizers?: string[] - labels?: Record - } - } - } -} +export function sealedSecretManifest(teamId: string, data: SealedSecretManifestRequest): SealedSecretManifestResponse { + const { annotations, labels, finalizers } = data.spec?.template?.metadata || {} + const namespace = `team-${teamId}` -export function sealedSecretManifest(data: AplSecretResponse): SealedSecretManifestType { - const { annotations, labels, finalizers } = data.spec.metadata || {} - const namespace = data.spec.namespace! return { apiVersion: 'bitnami.com/v1alpha1', kind: 'SealedSecret', metadata: { - ...data.metadata, + name: data.metadata.name, annotations: { 'sealedsecrets.bitnami.com/namespace-wide': 'true', }, + labels: { + 'apl.io/teamId': teamId, + }, namespace, }, spec: { encryptedData: data.spec.encryptedData || {}, template: { - type: data.spec.type || 'kubernetes.io/opaque', - immutable: data.spec.immutable || false, + type: data.spec.template?.type || 'kubernetes.io/opaque', + immutable: data.spec.template?.immutable || false, metadata: { name: data.metadata.name, namespace, @@ -63,32 +35,34 @@ export function sealedSecretManifest(data: AplSecretResponse): SealedSecretManif }, }, }, + status: {}, } } -export function toSealedSecretResponse(data: SealedSecretManifestType): AplSecretResponse { - { - return { - kind: 'AplTeamSecret', - metadata: { - name: data.metadata.name, - labels: { - 'apl.io/teamId': data.metadata.labels?.['apl.io/teamId'] || '', - }, +export function ensureSealedSecretMetadata( + manifest: SealedSecretManifestResponse, + teamId: string, +): SealedSecretManifestResponse { + const hasCorrectLabel = manifest.metadata.labels?.['apl.io/teamId'] === teamId + const hasCorrectAnnotation = manifest.metadata.annotations?.['sealedsecrets.bitnami.com/namespace-wide'] === 'true' + + if (hasCorrectLabel && hasCorrectAnnotation) { + return manifest + } + + return { + ...manifest, + metadata: { + ...manifest.metadata, + annotations: { + ...manifest.metadata.annotations, + 'sealedsecrets.bitnami.com/namespace-wide': 'true', }, - spec: { - namespace: data.spec.template.metadata.namespace, - type: data.spec.template.type, - immutable: data.spec.template.immutable, - encryptedData: data.spec.encryptedData, - metadata: { - annotations: data.spec.template.metadata.annotations, - labels: data.spec.template.metadata.labels, - finalizers: data.spec.template.metadata.finalizers, - }, + labels: { + ...manifest.metadata.labels, + 'apl.io/teamId': teamId, }, - status: {}, - } + }, } }