Skip to content

Commit 9d09335

Browse files
ferruhcihansvcAPLBotCasLubbers
authored
fix: load workload values (#860)
* fix: load workload values * fix: load comment lines * fix: get all resources * fix: get all resources * fix: loading platform and team resources by kind * fix: loading raw yaml and gracefully handle errors * fix: loading raw yaml and gracefully handle errors --------- Co-authored-by: svcAPLBot <[email protected]> Co-authored-by: Cas Lubbers <[email protected]>
1 parent 03bc0b3 commit 9d09335

File tree

3 files changed

+123
-58
lines changed

3 files changed

+123
-58
lines changed

src/fileStore/file-store.ts

Lines changed: 89 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
// The in-memory key-value store: file path -> parsed content
2-
import path from 'path'
3-
import { globSync } from 'glob'
2+
import Debug from 'debug'
43
import { ensureDir } from 'fs-extra'
54
import { writeFile } from 'fs/promises'
5+
import { globSync } from 'glob'
6+
import { merge } from 'lodash'
7+
import path from 'path'
68
import { stringify as stringifyYaml } from 'yaml'
79
import { z } from 'zod'
8-
import { merge } from 'lodash'
9-
import { loadYaml } from '../utils'
10-
import { getFileMapForKind, getFileMaps, getResourceFilePath } from './file-map'
1110
import { APL_KINDS, AplKind, AplObject, AplPlatformObject, AplRecord, AplTeamObject } from '../otomi-models'
12-
import Debug from 'debug'
11+
import { loadRawYaml, loadYaml } from '../utils'
12+
import { getFileMapForKind, getFileMaps, getResourceFilePath } from './file-map'
1313

1414
const debug = Debug('otomi:file-store')
1515

@@ -44,6 +44,10 @@ function shouldSkipValidation(filePath: string): boolean {
4444
return filePath.includes('/sealedsecrets/') || filePath.includes('/workloadValues/')
4545
}
4646

47+
function isRawContent(filePath: string): boolean {
48+
return filePath.includes('/workloadValues/')
49+
}
50+
4751
export class FileStore {
4852
private store: Map<string, AplObject> = new Map()
4953

@@ -68,29 +72,33 @@ export class FileStore {
6872

6973
await Promise.all(
7074
filesToLoad.map(async (filePath) => {
71-
const rawContent = await loadYaml(filePath)
72-
const relativePath = path.relative(envDir, filePath).replace(/\.dec$/, '')
73-
74-
// Skip validation for specific file paths
75-
if (shouldSkipValidation(filePath)) {
76-
allFiles.set(relativePath, rawContent as AplObject)
77-
return
75+
try {
76+
const rawContent = isRawContent(filePath) ? await loadRawYaml(filePath) : await loadYaml(filePath)
77+
const relativePath = path.relative(envDir, filePath).replace(/\.dec$/, '')
78+
79+
// Skip validation for specific file paths
80+
if (shouldSkipValidation(filePath)) {
81+
allFiles.set(relativePath, rawContent as AplObject)
82+
return
83+
}
84+
85+
// Validate all other kinds
86+
const result = AplObjectSchema.safeParse(rawContent)
87+
88+
if (!result.success) {
89+
debug(`Validation failed for ${relativePath}:`, result.error.message)
90+
return
91+
}
92+
93+
if (!result.data) {
94+
debug(`No content found for ${relativePath}`)
95+
return
96+
}
97+
98+
allFiles.set(relativePath, result.data as AplObject)
99+
} catch (error) {
100+
debug(`Failed to load file ${filePath}:`, error)
78101
}
79-
80-
// Validate all other kinds
81-
const result = AplObjectSchema.safeParse(rawContent)
82-
83-
if (!result.success) {
84-
debug(`Validation failed for ${relativePath}:`, result.error.message)
85-
return
86-
}
87-
88-
if (!result.data) {
89-
debug(`No content found for ${relativePath}`)
90-
return
91-
}
92-
93-
allFiles.set(relativePath, result.data as AplObject)
94102
}),
95103
)
96104

@@ -171,23 +179,72 @@ export class FileStore {
171179
return filePath
172180
}
173181

174-
// Generic method for all resources (platform and team)
175-
getByKind(kind: AplKind, teamId?: string): Map<string, AplObject> {
182+
// Get platform resources (no team scope - e.g., AplUser, settings)
183+
getPlatformResourcesByKind(kind: AplKind): Map<string, AplObject> {
176184
const fileMap = getFileMapForKind(kind)
177185
if (!fileMap) {
178186
throw new Error(`Unknown kind: ${kind}`)
179187
}
180188

181-
// Generate path prefix from template (e.g., 'env/teams/team1/workloads/')
182-
const prefix = fileMap.pathTemplate.replace('{teamId}', teamId || '').replace('{name}.yaml', '')
189+
// Platform resources don't have {teamId} in their path
190+
// e.g., 'env/settings/{name}.yaml' → 'env/settings/'
191+
const prefix = fileMap.pathTemplate.replace('{name}.yaml', '')
192+
const result = new Map<string, AplObject>()
193+
194+
for (const filePath of this.store.keys()) {
195+
if (filePath.startsWith(prefix) && filePath.endsWith('.yaml')) {
196+
const content = this.store.get(filePath)
197+
if (content) result.set(filePath, content)
198+
}
199+
}
183200

201+
return result
202+
}
203+
204+
// Get ALL team resources across all teams (e.g., all workloads, all services)
205+
getAllTeamResourcesByKind(kind: AplKind): Map<string, AplObject> {
206+
const fileMap = getFileMapForKind(kind)
207+
if (!fileMap) {
208+
throw new Error(`Unknown kind: ${kind}`)
209+
}
210+
211+
// Extract resource path segment from template
212+
// e.g., 'env/teams/{teamId}/workloads/{name}.yaml' → '/workloads'
213+
const parts = fileMap.pathTemplate.split('{teamId}')
214+
const resourcePath = parts[1].replace('/{name}.yaml', '')
215+
const result = new Map<string, AplObject>()
216+
217+
// Match any path containing this resource segment
218+
// e.g., matches 'env/teams/*/workloads/*.yaml'
219+
for (const filePath of this.store.keys()) {
220+
if (filePath.includes(resourcePath) && filePath.endsWith('.yaml')) {
221+
const content = this.store.get(filePath)
222+
if (content) result.set(filePath, content)
223+
}
224+
}
225+
226+
return result
227+
}
228+
229+
getTeamResourcesByKindAndTeamId(kind: AplKind, teamId: string): Map<string, AplObject> {
230+
const fileMap = getFileMapForKind(kind)
231+
if (!fileMap) {
232+
throw new Error(`Unknown kind: ${kind}`)
233+
}
234+
235+
// Generate exact prefix for this team
236+
// e.g., 'env/teams/team1/workloads/'
237+
const prefix = fileMap.pathTemplate.replace('{teamId}', teamId).replace('{name}.yaml', '')
184238
const result = new Map<string, AplObject>()
239+
240+
// Exact prefix match
185241
for (const filePath of this.store.keys()) {
186242
if (filePath.startsWith(prefix) && filePath.endsWith('.yaml')) {
187243
const content = this.store.get(filePath)
188244
if (content) result.set(filePath, content)
189245
}
190246
}
247+
191248
return result
192249
}
193250

src/otomi-stack.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ export default class OtomiStack {
383383
const fileMap = settingsFileMaps.get(key)
384384
if (!fileMap) return // Skip unknown keys
385385

386-
const files = this.fileStore.getByKind(fileMap.kind)
386+
const files = this.fileStore.getPlatformResourcesByKind(fileMap.kind)
387387
for (const [, content] of files) {
388388
settings[key] = content?.spec || content
389389
}
@@ -399,7 +399,7 @@ export default class OtomiStack {
399399

400400
// No keys specified: fetch all settings
401401
for (const [name, fileMap] of settingsFileMaps.entries()) {
402-
const files = this.fileStore.getByKind(fileMap.kind)
402+
const files = this.fileStore.getPlatformResourcesByKind(fileMap.kind)
403403
for (const [, content] of files) {
404404
settings[name] = content?.spec || content
405405
}
@@ -643,7 +643,7 @@ export default class OtomiStack {
643643
const teamIds = this.fileStore.getTeamIds()
644644

645645
for (const teamId of teamIds) {
646-
const settingsFiles = this.fileStore.getByKind('AplTeamSettingSet', teamId)
646+
const settingsFiles = this.fileStore.getTeamResourcesByKindAndTeamId('AplTeamSettingSet', teamId)
647647
for (const [, content] of settingsFiles) {
648648
// v1 format: return spec directly
649649
const team = getV1ObjectFromApl(content as AplTeamSettingsResponse) as Team
@@ -667,7 +667,7 @@ export default class OtomiStack {
667667

668668
for (const teamId of teamIds) {
669669
if (teamId === 'admin') continue
670-
const settingsFiles = this.fileStore.getByKind('AplTeamSettingSet', teamId)
670+
const settingsFiles = this.fileStore.getTeamResourcesByKindAndTeamId('AplTeamSettingSet', teamId)
671671
for (const [, content] of settingsFiles) {
672672
if (content) {
673673
// Return full v2 object with password removed
@@ -859,7 +859,7 @@ export default class OtomiStack {
859859
}
860860

861861
getTeamAplNetpols(teamId: string): AplNetpolResponse[] {
862-
const files = this.fileStore.getByKind('AplTeamNetworkControl', teamId)
862+
const files = this.fileStore.getTeamResourcesByKindAndTeamId('AplTeamNetworkControl', teamId)
863863
return Array.from(files.values()) as AplNetpolResponse[]
864864
}
865865

@@ -868,7 +868,7 @@ export default class OtomiStack {
868868
}
869869

870870
getAllAplNetpols(): AplNetpolResponse[] {
871-
const files = this.fileStore.getByKind('AplTeamNetworkControl')
871+
const files = this.fileStore.getAllTeamResourcesByKind('AplTeamNetworkControl')
872872
return Array.from(files.values()) as AplNetpolResponse[]
873873
}
874874

@@ -936,7 +936,7 @@ export default class OtomiStack {
936936
}
937937

938938
getAllUsers(sessionUser: SessionUser): Array<User> {
939-
const files = this.fileStore.getByKind('AplUser')
939+
const files = this.fileStore.getPlatformResourcesByKind('AplUser')
940940
const aplObjects = Array.from(files.values()) as AplObject[]
941941
const users = aplObjects.map((aplObject) => {
942942
return { ...aplObject.spec, id: aplObject.metadata.name } as User
@@ -970,7 +970,7 @@ export default class OtomiStack {
970970
const user: User = { ...data, id: userId, initialPassword }
971971

972972
// Get existing users' emails
973-
const files = this.fileStore.getByKind('AplUser')
973+
const files = this.fileStore.getPlatformResourcesByKind('AplUser')
974974
let existingUsersEmail = Array.from(files.values()).map((aplObject: AplObject) => aplObject.spec.email)
975975

976976
if (!env.isDev) {
@@ -1120,7 +1120,7 @@ export default class OtomiStack {
11201120
}
11211121

11221122
getTeamAplCodeRepos(teamId: string): AplCodeRepoResponse[] {
1123-
const files = this.fileStore.getByKind('AplTeamCodeRepo', teamId)
1123+
const files = this.fileStore.getTeamResourcesByKindAndTeamId('AplTeamCodeRepo', teamId)
11241124
return Array.from(files.values()) as AplCodeRepoResponse[]
11251125
}
11261126

@@ -1129,7 +1129,7 @@ export default class OtomiStack {
11291129
}
11301130

11311131
getAllAplCodeRepos(): AplCodeRepoResponse[] {
1132-
const files = this.fileStore.getByKind('AplTeamCodeRepo')
1132+
const files = this.fileStore.getAllTeamResourcesByKind('AplTeamCodeRepo')
11331133
return Array.from(files.values()) as AplCodeRepoResponse[]
11341134
}
11351135

@@ -1298,7 +1298,7 @@ export default class OtomiStack {
12981298
}
12991299

13001300
getTeamAplBuilds(teamId: string): AplBuildResponse[] {
1301-
const files = this.fileStore.getByKind('AplTeamBuild', teamId)
1301+
const files = this.fileStore.getTeamResourcesByKindAndTeamId('AplTeamBuild', teamId)
13021302
return Array.from(files.values()) as AplBuildResponse[]
13031303
}
13041304

@@ -1307,7 +1307,7 @@ export default class OtomiStack {
13071307
}
13081308

13091309
getAllAplBuilds(): AplBuildResponse[] {
1310-
const files = this.fileStore.getByKind('AplTeamBuild')
1310+
const files = this.fileStore.getAllTeamResourcesByKind('AplTeamBuild')
13111311
return Array.from(files.values()) as AplBuildResponse[]
13121312
}
13131313

@@ -1387,7 +1387,7 @@ export default class OtomiStack {
13871387
}
13881388

13891389
getTeamAplPolicies(teamId: string): AplPolicyResponse[] {
1390-
const files = this.fileStore.getByKind('AplTeamPolicy', teamId)
1390+
const files = this.fileStore.getTeamResourcesByKindAndTeamId('AplTeamPolicy', teamId)
13911391
return Array.from(files.values()) as AplPolicyResponse[]
13921392
}
13931393

@@ -1401,7 +1401,7 @@ export default class OtomiStack {
14011401
}
14021402

14031403
getAllAplPolicies(): AplPolicyResponse[] {
1404-
const files = this.fileStore.getByKind('AplTeamPolicy')
1404+
const files = this.fileStore.getAllTeamResourcesByKind('AplTeamPolicy')
14051405
return Array.from(files.values()) as AplPolicyResponse[]
14061406
}
14071407

@@ -1610,7 +1610,7 @@ export default class OtomiStack {
16101610
}
16111611

16121612
getTeamAplWorkloads(teamId: string): AplWorkloadResponse[] {
1613-
const files = this.fileStore.getByKind('AplTeamWorkload', teamId)
1613+
const files = this.fileStore.getTeamResourcesByKindAndTeamId('AplTeamWorkload', teamId)
16141614
return Array.from(files.values()) as AplWorkloadResponse[]
16151615
}
16161616

@@ -1632,7 +1632,7 @@ export default class OtomiStack {
16321632
}
16331633

16341634
getAllAplWorkloads(): AplWorkloadResponse[] {
1635-
const files = this.fileStore.getByKind('AplTeamWorkload')
1635+
const files = this.fileStore.getAllTeamResourcesByKind('AplTeamWorkload')
16361636
return Array.from(files.values()) as AplWorkloadResponse[]
16371637
}
16381638

@@ -1738,7 +1738,7 @@ export default class OtomiStack {
17381738
}
17391739

17401740
getAllAplServices(): AplServiceResponse[] {
1741-
const files = this.fileStore.getByKind('AplTeamService')
1741+
const files = this.fileStore.getAllTeamResourcesByKind('AplTeamService')
17421742
return Array.from(files.values()) as AplServiceResponse[]
17431743
}
17441744

@@ -1747,7 +1747,7 @@ export default class OtomiStack {
17471747
}
17481748

17491749
getTeamAplServices(teamId: string): AplServiceResponse[] {
1750-
const files = this.fileStore.getByKind('AplTeamService', teamId)
1750+
const files = this.fileStore.getTeamResourcesByKindAndTeamId('AplTeamService', teamId)
17511751
return Array.from(files.values()) as AplServiceResponse[]
17521752
}
17531753

@@ -2155,7 +2155,7 @@ export default class OtomiStack {
21552155
}
21562156

21572157
getAllAplSealedSecrets(): AplSecretResponse[] {
2158-
const files = this.fileStore.getByKind('AplTeamSecret')
2158+
const files = this.fileStore.getAllTeamResourcesByKind('AplTeamSecret')
21592159
return Array.from(files.values()) as AplSecretResponse[]
21602160
}
21612161

@@ -2167,7 +2167,7 @@ export default class OtomiStack {
21672167
}
21682168

21692169
getAplSealedSecrets(teamId: string): AplSecretResponse[] {
2170-
const files = this.fileStore.getByKind('AplTeamSecret', teamId)
2170+
const files = this.fileStore.getTeamResourcesByKindAndTeamId('AplTeamSecret', teamId)
21712171
return Array.from(files.values()) as AplSecretResponse[]
21722172
}
21732173

@@ -2229,7 +2229,7 @@ export default class OtomiStack {
22292229
}
22302230

22312231
getAplKnowledgeBases(teamId: string): AplKnowledgeBaseResponse[] {
2232-
const files = this.fileStore.getByKind('AkamaiKnowledgeBase', teamId)
2232+
const files = this.fileStore.getTeamResourcesByKindAndTeamId('AkamaiKnowledgeBase', teamId)
22332233
return Array.from(files.values()) as AplKnowledgeBaseResponse[]
22342234
}
22352235

@@ -2294,12 +2294,12 @@ export default class OtomiStack {
22942294
}
22952295

22962296
getAplAgents(teamId: string): AplAgentResponse[] {
2297-
const files = this.fileStore.getByKind('AkamaiAgent', teamId)
2297+
const files = this.fileStore.getTeamResourcesByKindAndTeamId('AkamaiAgent', teamId)
22982298
return Array.from(files.values()) as AplAgentResponse[]
22992299
}
23002300

23012301
getAllAplAgents(): AplAgentResponse[] {
2302-
const files = this.fileStore.getByKind('AkamaiAgent')
2302+
const files = this.fileStore.getAllTeamResourcesByKind('AkamaiAgent')
23032303
return Array.from(files.values()) as AplAgentResponse[]
23042304
}
23052305

src/utils.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,20 @@ export const flattenObject = (
6565
}, {})
6666
}
6767

68-
export const loadYaml = async (path: string, opts?: { noError: boolean }): Promise<Record<string, any> | undefined> => {
68+
export const loadYaml = async (path: string): Promise<Record<string, any> | undefined> => {
6969
if (!(await pathExists(path))) {
70-
if (opts?.noError) return undefined
7170
throw new Error(`${path} does not exist`)
7271
}
73-
return parse(await readFile(path, 'utf-8')) as Record<string, any>
72+
const rawFile = await readFile(path, 'utf-8')
73+
return parse(rawFile) as Record<string, any>
74+
}
75+
76+
export async function loadRawYaml(path: string) {
77+
if (!(await pathExists(path))) {
78+
throw new Error(`${path} does not exist`)
79+
}
80+
const rawFile = await readFile(path, 'utf-8')
81+
return rawFile as any
7482
}
7583

7684
const valuesSchemaEndpointUrl = `${BASEURL}/apl/schema`

0 commit comments

Comments
 (0)