From 2225194e93e807419b3c7b6580f04e4548005db3 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:53:52 +0100 Subject: [PATCH 01/10] feat: remove artificial values property stored in workload values file --- src/otomi-stack.ts | 18 +++++++++++------- src/repo.ts | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index 342cac004..3f208e4ab 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -97,7 +97,7 @@ import { k8sdelete, watchPodUntilRunning, } from './k8s_operations' -import { getFileMaps, loadValues } from './repo' +import { getFileMap, getFileMaps, loadValues } from './repo' import { RepoService } from './services/RepoService' import { TeamConfigService } from './services/TeamConfigService' import { validateBackupFields } from './utils/backupUtils' @@ -147,6 +147,10 @@ function getTeamSealedSecretsValuesFilePath(teamId: string, sealedSecretsName: s return `env/teams/${teamId}/sealedsecrets/${sealedSecretsName}` } +function getTeamWorkloadValuesManagedFilePath(teamId: string, workloadName: string): string { + return `env/teams/${teamId}/workloads/${workloadName}.managed.yaml` +} + export default class OtomiStack { private coreValues: Core editor?: string @@ -795,14 +799,12 @@ export default class OtomiStack { const { metadata } = data const teamId = metadata.labels['apl.io/teamId']! debug(`Saving AplTeamWorkloadValues ${metadata.name} for team ${teamId}`) - const values = { - name: metadata.name, - values: data.spec.values, - } const configKey = this.getConfigKey('AplTeamWorkloadValues') - const repo = this.createTeamConfigInRepo(teamId, configKey, [values]) - const fileMap = getFileMaps('').find((fm) => fm.kind === 'AplTeamWorkloadValues')! + const repo = this.createTeamConfigInRepo(teamId, configKey, [data.spec.values]) + const fileMap = getFileMap('AplTeamWorkloadValues', '') await this.git.saveConfig(repo, fileMap, false) + const filePathValuesManaged = getTeamWorkloadValuesManagedFilePath(teamId, metadata.name) + await this.git.writeFile(filePathValuesManaged, {}) } async saveTeamPolicy(teamId: string, data: AplPolicyResponse): Promise { @@ -841,8 +843,10 @@ export default class OtomiStack { const fileMapWorkload = getFileMaps('').find((fm) => fm.kind === 'AplTeamWorkload')! const repoValues = this.createTeamConfigInRepo(teamId, 'workloads', [data]) const fileMapValues = getFileMaps('').find((fm) => fm.kind === 'AplTeamWorkloadValues')! + const filePathValuesManaged = getTeamWorkloadValuesManagedFilePath(teamId, metadata.name) await this.git.deleteConfig(repoWorkload, fileMapWorkload) await this.git.deleteConfig(repoValues, fileMapValues) + await this.git.removeFile(filePathValuesManaged) } getTeamBackups(teamId: string): Backup[] { diff --git a/src/repo.ts b/src/repo.ts index e26f2a66e..0d527fd09 100755 --- a/src/repo.ts +++ b/src/repo.ts @@ -399,7 +399,7 @@ export function getFileMap(kind: AplKind, envDir: string): FileMap { export function renderManifest(fileMap: FileMap, jsonPath: jsonpath.PathComponent[], data: Record) { //TODO remove this custom workaround for workloadValues if (fileMap.kind === 'AplTeamWorkloadValues') { - return { values: data.values } + return data.values } let spec = data const labels = {} From 34f16213bde76fd144fcd19827823b1c25b05c1c Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:37:00 +0100 Subject: [PATCH 02/10] feat: remove artificial values property stored in workload values file --- src/git.ts | 7 +++++++ src/otomi-stack.ts | 24 +++++++++++++----------- src/repo.ts | 4 ---- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/git.ts b/src/git.ts index 1e7069c8e..519e166ea 100644 --- a/src/git.ts +++ b/src/git.ts @@ -203,6 +203,13 @@ export class Git { await writeFile(absolutePath, content, 'utf8') } + async writeTextFile(file: string, content: string): Promise { + const absolutePath = join(this.path, file) + debug(`Writing to file: ${absolutePath}`) + await ensureDir(dirname(absolutePath)) + await writeFile(absolutePath, content, 'utf8') + } + async fileExists(relativePath: string): Promise { const absolutePath = join(this.path, relativePath) return await pathExists(absolutePath) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index d15f8ae29..8718b9242 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -1,4 +1,4 @@ -import { CoreV1Api, KubeConfig, User as k8sUser, V1ObjectReference } from '@kubernetes/client-node' +import { CoreV1Api, User as k8sUser, KubeConfig, V1ObjectReference } from '@kubernetes/client-node' import Debug from 'debug' import { getRegions, ObjectStorageKeyRegions } from '@linode/api-v4' @@ -94,6 +94,10 @@ import { } from 'src/validators' import { v4 as uuidv4 } from 'uuid' import { parse as parseYaml, stringify as stringifyYaml } from 'yaml' +import { getAIModels } from './ai/aiModelHandler' +import { AkamaiAgentCR } from './ai/AkamaiAgentCR' +import { AkamaiKnowledgeBaseCR } from './ai/AkamaiKnowledgeBaseCR' +import { DatabaseCR } from './ai/DatabaseCR' import { apply, checkPodExists, @@ -104,7 +108,7 @@ import { k8sdelete, watchPodUntilRunning, } from './k8s_operations' -import { getFileMap, getFileMaps, loadValues } from './repo' +import { getFileMaps, loadValues } from './repo' import { RepoService } from './services/RepoService' import { TeamConfigService } from './services/TeamConfigService' import { validateBackupFields } from './utils/backupUtils' @@ -121,10 +125,6 @@ import { getSealedSecretsPEM, sealedSecretManifest, SealedSecretManifestType } f import { getKeycloakUsers, isValidUsername } from './utils/userUtils' import { ObjectStorageClient } from './utils/wizardUtils' import { fetchChartYaml, fetchWorkloadCatalog, NewHelmChartValues, sparseCloneChart } from './utils/workloadUtils' -import { getAIModels } from './ai/aiModelHandler' -import { AkamaiKnowledgeBaseCR } from './ai/AkamaiKnowledgeBaseCR' -import { AkamaiAgentCR } from './ai/AkamaiAgentCR' -import { DatabaseCR } from './ai/DatabaseCR' interface ExcludedApp extends App { managed: boolean @@ -163,6 +163,10 @@ function getTeamWorkloadValuesManagedFilePath(teamId: string, workloadName: stri return `env/teams/${teamId}/workloads/${workloadName}.managed.yaml` } +function getTeamWorkloadValuesFilePath(teamId: string, workloadName: string): string { + return `env/teams/${teamId}/workloads/${workloadName}.yaml` +} + function getTeamKnowledgeBaseValuesFilePath(teamId: string, knowledgeBaseName: string): string { return `env/teams/${teamId}/knowledgebases/${knowledgeBaseName}` } @@ -843,12 +847,10 @@ export default class OtomiStack { const { metadata } = data const teamId = metadata.labels['apl.io/teamId']! debug(`Saving AplTeamWorkloadValues ${metadata.name} for team ${teamId}`) - const configKey = this.getConfigKey('AplTeamWorkloadValues') - const repo = this.createTeamConfigInRepo(teamId, configKey, [data.spec.values]) - const fileMap = getFileMap('AplTeamWorkloadValues', '') - await this.git.saveConfig(repo, fileMap, false) + const filePath = getTeamWorkloadValuesFilePath(teamId, metadata.name) + await this.git.writeTextFile(filePath, data.spec.values || '{}') const filePathValuesManaged = getTeamWorkloadValuesManagedFilePath(teamId, metadata.name) - await this.git.writeFile(filePathValuesManaged, {}) + await this.git.writeTextFile(filePathValuesManaged, '') } async saveTeamPolicy(teamId: string, data: AplPolicyResponse): Promise { diff --git a/src/repo.ts b/src/repo.ts index ac537def6..224bb0a78 100755 --- a/src/repo.ts +++ b/src/repo.ts @@ -419,10 +419,6 @@ export function getFileMap(kind: AplKind, envDir: string): FileMap { } export function renderManifest(fileMap: FileMap, jsonPath: jsonpath.PathComponent[], data: Record) { - //TODO remove this custom workaround for workloadValues - if (fileMap.kind === 'AplTeamWorkloadValues') { - return data.values - } let spec = data const labels = {} if (fileMap.resourceGroup === 'team') { From 5ff6bbdba6f4c1f547fad64a5d1a79bc182ccdba Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Mon, 3 Nov 2025 16:56:15 +0100 Subject: [PATCH 03/10] feat: remove artificial values property stored in workload values file --- src/otomi-stack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index 8718b9242..3825341b5 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -160,11 +160,11 @@ function getTeamSealedSecretsValuesFilePath(teamId: string, sealedSecretsName: s } function getTeamWorkloadValuesManagedFilePath(teamId: string, workloadName: string): string { - return `env/teams/${teamId}/workloads/${workloadName}.managed.yaml` + return `env/teams/${teamId}/workloadValues/${workloadName}.managed.yaml` } function getTeamWorkloadValuesFilePath(teamId: string, workloadName: string): string { - return `env/teams/${teamId}/workloads/${workloadName}.yaml` + return `env/teams/${teamId}/workloadValues/${workloadName}.yaml` } function getTeamKnowledgeBaseValuesFilePath(teamId: string, knowledgeBaseName: string): string { From 742fa830ec2e93de6b42f777abcdc5b0fde0443e Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:09:40 +0100 Subject: [PATCH 04/10] feat: load workloads --- src/otomi-models.ts | 1 + src/otomi-stack.ts | 22 +++++++++++----------- src/repo.ts | 5 ++++- src/services/RepoService.ts | 1 + 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/otomi-models.ts b/src/otomi-models.ts index 8b7924cf4..5f561e7c1 100644 --- a/src/otomi-models.ts +++ b/src/otomi-models.ts @@ -252,6 +252,7 @@ export interface Repo { users: User[] versions: Versions teamConfig: Record + files: Record } export interface TeamConfig { diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index 3825341b5..9827c9e75 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -192,6 +192,10 @@ export default class OtomiStack { this.sessionId = sessionId ?? 'main' } + getFileContent(path: string): string { + return this.repoService.getRepo().files[path] || '' + } + getAppList() { let apps = getAppList() apps = apps.filter((item) => item !== 'ingress-nginx') @@ -280,16 +284,13 @@ export default class OtomiStack { }) } - transformWorkloads(workloads: AplWorkloadResponse[], workloadValues: Record[]): AplWorkloadResponse[] { - const values = {} - workloadValues.forEach((value) => { - const workloadName = value.name - if (workloadName) { - values[workloadName] = value.values - } - }) + transformWorkloads(workloads: AplWorkloadResponse[], files: string[]): AplWorkloadResponse[] { return workloads.map((workload) => { - return merge(workload, { spec: { values: values[workload.metadata.name] } }) + const workloadName = workload.metadata.name + const teamId = workload.metadata.labels?.['apl.io/teamId'] + + const filePath = getTeamWorkloadValuesFilePath(teamId, workloadName) + return merge(workload, { spec: { values: files[filePath] } }) }) } @@ -322,10 +323,9 @@ export default class OtomiStack { apps: this.transformApps(teamConfig.apps), policies: this.transformPolicies(teamName, teamConfig.policies || {}), sealedsecrets: this.transformSecrets(teamName, teamConfig.sealedsecrets || []), - workloads: this.transformWorkloads(teamConfig.workloads || [], teamConfig.workloadValues || []), + workloads: this.transformWorkloads(teamConfig.workloads || [], rawRepo.files || {}), settings: this.transformTeamSettings(teamConfig.settings), })) - const repo = rawRepo as Repo this.repoService = new RepoService(repo) } diff --git a/src/repo.ts b/src/repo.ts index 224bb0a78..0eb636e9a 100755 --- a/src/repo.ts +++ b/src/repo.ts @@ -485,6 +485,7 @@ export async function loadFileToSpec( const jsonPath = getJsonPath(fileMap, filePath) try { const data = (await deps.loadYaml(filePath)) || {} + spec.files[filePath] = data if (fileMap.processAs === 'arrayItem') { const ref: Record[] = get(spec, jsonPath) const name = filePath.match(/\/([^/]+)\.yaml$/)?.[1] @@ -573,7 +574,9 @@ export async function loadToSpec( export async function loadValues(envDir: string, deps = { loadToSpec }): Promise> { //We need everything to load to spec for the API const fileMaps = getFileMaps(envDir) - const spec = {} + const spec = { + files: {}, + } await Promise.all( fileMaps.map(async (fileMap) => { diff --git a/src/services/RepoService.ts b/src/services/RepoService.ts index 1df3f2e04..fe563618b 100644 --- a/src/services/RepoService.ts +++ b/src/services/RepoService.ts @@ -52,6 +52,7 @@ export class RepoService { this.repo.users ??= [] this.repo.versions ??= {} as Versions this.repo.teamConfig ??= {} + this.repo.files ??= {} } public getTeamConfigService(teamName: string): TeamConfigService { From 9596c166176164839e501fb38d1fa2c227d3f413 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:06:31 +0100 Subject: [PATCH 05/10] feat: load workloads --- src/repo.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/repo.ts b/src/repo.ts index 0eb636e9a..5d906322b 100755 --- a/src/repo.ts +++ b/src/repo.ts @@ -485,7 +485,9 @@ export async function loadFileToSpec( const jsonPath = getJsonPath(fileMap, filePath) try { const data = (await deps.loadYaml(filePath)) || {} - spec.files[filePath] = data + const localFilePath = filePath.replace(fileMap.envDir, '').replace(/^\/+/, '') + + spec.files[localFilePath] = data if (fileMap.processAs === 'arrayItem') { const ref: Record[] = get(spec, jsonPath) const name = filePath.match(/\/([^/]+)\.yaml$/)?.[1] From 87cddc10175234582584a8919dd4912695e966eb Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:08:55 +0100 Subject: [PATCH 06/10] feat: load workloads --- src/repo.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/repo.ts b/src/repo.ts index 5d906322b..e84a4686c 100755 --- a/src/repo.ts +++ b/src/repo.ts @@ -485,6 +485,7 @@ export async function loadFileToSpec( const jsonPath = getJsonPath(fileMap, filePath) try { const data = (await deps.loadYaml(filePath)) || {} + // ensure that local path does not include envDir and the leading slash const localFilePath = filePath.replace(fileMap.envDir, '').replace(/^\/+/, '') spec.files[localFilePath] = data From d29b2905870dda48d537fa9c8ce623099c1dd6c2 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 5 Nov 2025 08:40:54 +0100 Subject: [PATCH 07/10] test: fix repo serivce --- src/services/RepoService.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/RepoService.test.ts b/src/services/RepoService.test.ts index 74a216cbb..8f2d8f0ca 100644 --- a/src/services/RepoService.test.ts +++ b/src/services/RepoService.test.ts @@ -39,6 +39,7 @@ describe('RepoService', () => { obj: {}, oidc: { issuer: 'https://issuer.com', clientID: 'client-id', clientSecret: 'client-secret' }, versions: { version: '1.0.0' }, + files: {}, } as Repo service = new RepoService(repo) service.createTeamConfig(teamSettings) From 53bf8abd4ffb91a4b50e3174c16ddd0e61d08b5d Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 5 Nov 2025 08:50:50 +0100 Subject: [PATCH 08/10] test: linter exception --- eslint.config.mjs | 1 + src/repo.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 1ba46fce3..ebdc23197 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -75,6 +75,7 @@ export default defineConfig([ varsIgnorePattern: '^_', }, ], + '@typescript-eslint/no-use-before-define': ['error'], '@typescript-eslint/restrict-template-expressions': 'off', '@typescript-eslint/naming-convention': [ diff --git a/src/repo.ts b/src/repo.ts index e84a4686c..36bcdbbd3 100755 --- a/src/repo.ts +++ b/src/repo.ts @@ -487,7 +487,7 @@ export async function loadFileToSpec( const data = (await deps.loadYaml(filePath)) || {} // ensure that local path does not include envDir and the leading slash const localFilePath = filePath.replace(fileMap.envDir, '').replace(/^\/+/, '') - + // eslint-disable-next-line no-param-reassign spec.files[localFilePath] = data if (fileMap.processAs === 'arrayItem') { const ref: Record[] = get(spec, jsonPath) From 40d1a79f292bcdf1d3850a2ccfb74f9abd191b8f Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:05:09 +0100 Subject: [PATCH 09/10] fix: rework --- src/otomi-stack.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index 953c33026..53fb90b21 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -192,10 +192,6 @@ export default class OtomiStack { this.sessionId = sessionId ?? 'main' } - getFileContent(path: string): string { - return this.repoService.getRepo().files[path] || '' - } - getAppList() { let apps = getAppList() apps = apps.filter((item) => item !== 'ingress-nginx') From 36515dd44666cbd6a2cac228ee98308b3f6ee882 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:07:53 +0100 Subject: [PATCH 10/10] fix: rework --- src/otomi-stack.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index f652bc5c1..184df680a 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -839,14 +839,16 @@ export default class OtomiStack { await this.git.saveConfig(repo, fileMap) } - async saveTeamWorkloadValues(data: AplWorkloadResponse) { + async saveTeamWorkloadValues(data: AplWorkloadResponse, createManagedFile = false) { const { metadata } = data const teamId = metadata.labels['apl.io/teamId']! debug(`Saving AplTeamWorkloadValues ${metadata.name} for team ${teamId}`) const filePath = getTeamWorkloadValuesFilePath(teamId, metadata.name) await this.git.writeTextFile(filePath, data.spec.values || '{}') - const filePathValuesManaged = getTeamWorkloadValuesManagedFilePath(teamId, metadata.name) - await this.git.writeTextFile(filePathValuesManaged, '') + if (createManagedFile) { + const filePathValuesManaged = getTeamWorkloadValuesManagedFilePath(teamId, metadata.name) + await this.git.writeTextFile(filePathValuesManaged, '') + } } async saveTeamPolicy(teamId: string, data: AplPolicyResponse): Promise { @@ -1789,7 +1791,7 @@ export default class OtomiStack { try { const workload = this.repoService.getTeamConfigService(teamId).createWorkload(data) await this.saveTeamWorkload(workload) - await this.saveTeamWorkloadValues(workload) + await this.saveTeamWorkloadValues(workload, true) await this.doTeamDeployment( teamId, (teamService) => {