Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
04a523a
feat: update dependencies and enhance secret management
ferruhcihan Feb 20, 2026
cb4517c
feat: sync values schema from apl-core and update workflows
ferruhcihan Feb 20, 2026
01534a2
feat: enhance schema fetching with branch fallback in workflow
ferruhcihan Feb 20, 2026
807e4c0
feat: update user management
ferruhcihan Feb 22, 2026
d11c495
Merge remote-tracking branch 'origin/main' into APL-523
svcAPLBot Feb 24, 2026
38734b2
Merge remote-tracking branch 'origin/main' into APL-523
svcAPLBot Feb 24, 2026
b0c9b00
Merge branch 'main' into APL-523
ferruhcihan Feb 25, 2026
de87ebb
Merge branch 'main' into APL-523
ferruhcihan Feb 25, 2026
969d406
fix: otomi-stack
ferruhcihan Feb 25, 2026
4b03f28
test: platform secrets
ferruhcihan Feb 25, 2026
95511d3
Merge remote-tracking branch 'origin/main' into APL-523
svcAPLBot Feb 26, 2026
74f9b95
Revert "test: platform secrets"
ferruhcihan Mar 2, 2026
94a965b
Merge remote-tracking branch 'origin/main' into APL-523
svcAPLBot Mar 3, 2026
b18a1d1
feat: update user management for sealed secrets
ferruhcihan Mar 3, 2026
7a0f9a5
fix: get users
ferruhcihan Mar 3, 2026
76fdad6
Merge branch 'main' into APL-523
ferruhcihan Mar 5, 2026
4590c22
feat: add isK8sReachable function to improve local dev env
ferruhcihan Mar 5, 2026
e866048
fix: get internal repositories
ferruhcihan Mar 5, 2026
d86d192
Merge remote-tracking branch 'origin/main' into APL-523
svcAPLBot Mar 10, 2026
be63b02
Merge branch 'main' into APL-523
ferruhcihan Mar 12, 2026
2e2ac28
feat: sealed secret handling with users and settings management
ferruhcihan Mar 13, 2026
2612727
Merge remote-tracking branch 'origin/main' into APL-523
svcAPLBot Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ name: Build test push release
on:
push:
branches:
- '**'
- "**"
tags-ignore:
- '*'
- "*"
env:
COMMIT_MSG: ${{ github.event.head_commit.message }}
CACHE_REGISTRY: ghcr.io
Expand All @@ -35,11 +35,21 @@ jobs:
echo "TAG=$tag" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v6
- name: Fetch values-schema.yaml from apl-core
run: |
# Try matching branch first, fall back to main
curl -sL -f -H "Authorization: token ${{ env.BOT_TOKEN }}" \
"https://raw.githubusercontent.com/linode/apl-core/${{ env.TAG }}/values-schema.yaml" \
-o src/values-schema.yaml 2>/dev/null \
|| curl -sL -f -H "Authorization: token ${{ env.BOT_TOKEN }}" \
"https://raw.githubusercontent.com/linode/apl-core/main/values-schema.yaml" \
-o src/values-schema.yaml
echo "Schema fetched for branch: ${{ env.TAG }} (with main fallback)"
- name: CI tests, image build and push tag for main or branch
uses: whoan/docker-build-with-cache-action@v8
with:
username: ${{ env.BOT_USERNAME }}
password: '${{ env.BOT_TOKEN }}'
password: "${{ env.BOT_TOKEN }}"
registry: ${{ env.CACHE_REGISTRY }}
image_name: ${{ env.CACHE_REPO }}
image_tag: ${{ env.TAG }}
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/postman.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: 'postman'
name: "postman"
on:
pull_request:
branches:
- '*'
- "*"
workflow_dispatch:
jobs:
postman:
Expand Down Expand Up @@ -57,6 +57,8 @@ jobs:
npm install
npm run compile
NODE_PATH="/usr/local/lib/node_modules" npm run server > $GITHUB_WORKSPACE/core.log 2>&1 &
- name: Sync values schema from apl-core
run: cp apl-core/values-schema.yaml src/values-schema.yaml
- name: Start api
run: |
npm install
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ kms.json*

/vendors/client/
/src/generated-*
/src/values-schema.yaml
secrets.*.yaml.dec

#intelij
Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@casl/ability": "6.8.0",
"@kubernetes/client-node": "1.4.0",
"@linode/api-v4": "0.157.0",
"@linode/kubeseal-encrypt": "^1.0.1",
"@types/json-schema": "7.0.15",
"@types/jsonwebtoken": "9.0.10",
"axios": "1.13.6",
Expand Down Expand Up @@ -53,8 +54,8 @@
"@eslint/compat": "2.0.3",
"@redocly/openapi-cli": "1.0.0-beta.95",
"@semantic-release/changelog": "6.0.3",
"@types/async-retry": "^1.4.8",
"@semantic-release/git": "10.0.1",
"@types/async-retry": "^1.4.8",
"@types/debug": "^4.1.12",
"@types/expect": "24.3.2",
"@types/express": "^5.0.6",
Expand Down Expand Up @@ -135,6 +136,7 @@
"clean": "rm -rf dist >/dev/null",
"cz": "git-cz",
"cz:retry": "git-cz --retry",
"predev": "npm run schema:sync",
"dev": "run-p watch dev:node",
"dev:node": "tsx watch --env-file=.env --inspect=4321 src/app.ts",
"lint": "run-p types lint:ts",
Expand All @@ -144,6 +146,7 @@
"postinstall": "npm run build:models",
"pre-release:client": "npm version prerelease --preid rc --no-commit-hooks --no-git-tag-version && bin/release-client.sh",
"release": "standard-version",
"schema:sync": "APL_CORE_PATH=${APL_CORE_PATH:-../apl-core} && cp \"$APL_CORE_PATH/values-schema.yaml\" src/values-schema.yaml && echo \"Schema synced from $APL_CORE_PATH\"",
"release:bump:minor": "standard-version --skip.changelog true --release-as minor",
"release:client": "bin/release-client.sh",
"start": "node dist/src/app.js",
Expand Down
29 changes: 26 additions & 3 deletions src/api.authz.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { initApp, loadSpec } from 'src/app'
import getToken from 'src/fixtures/jwt'
import OtomiStack from 'src/otomi-stack'
import request from 'supertest'
import TestAgent from 'supertest/lib/agent'
import { HttpError } from './error'
import { FileStore } from './fileStore/file-store'
import { Git } from './git'
import { getSessionStack } from './middleware'
import { App, CodeRepo, Netpol, SealedSecret } from './otomi-models'
import * as getValuesSchemaModule from './utils'
import TestAgent from 'supertest/lib/agent'
import { FileStore } from './fileStore/file-store'

const platformAdminToken = getToken(['platform-admin'])
const teamAdminToken = getToken(['team-admin', 'team-team1'])
Expand All @@ -20,7 +20,29 @@ const userToken = getToken([])
const teamId = 'team1'
const otherTeamId = 'team2'

jest.mock('./k8s_operations')
jest.mock('./k8s_operations', () => {
const original = jest.requireActual('./k8s_operations')
return {
...original,
apply: jest.fn(),
checkPodExists: jest.fn(),
getCloudttyActiveTime: jest.fn(),
getKubernetesVersion: jest.fn().mockResolvedValue('x.x.x'),
getSecretValues: jest.fn().mockResolvedValue({ adminPassword: 'test-admin-password' }),
getTeamSecretsFromK8s: jest.fn().mockResolvedValue([]),
getUserSecretFromK8s: jest.fn().mockResolvedValue(undefined),
listUserSecretsFromK8s: jest.fn().mockResolvedValue([]),
k8sdelete: jest.fn(),
watchPodUntilRunning: jest.fn(),
getSealedSecretsCertificate: jest.fn().mockResolvedValue(''),
getSealedSecretSyncedStatus: jest.fn().mockResolvedValue('NotFound'),
getSealedSecretStatus: jest.fn().mockResolvedValue('NotFound'),
getSealedSecretsKeys: jest.fn().mockResolvedValue({}),
getWorkloadStatus: jest.fn().mockResolvedValue('NotFound'),
getBuildStatus: jest.fn().mockResolvedValue('NotFound'),
getServiceStatus: jest.fn().mockResolvedValue('NotFound'),
}
})
jest.mock('./utils/sealedSecretUtils')
beforeAll(async () => {
jest.spyOn(console, 'log').mockImplementation(() => {})
Expand Down Expand Up @@ -92,6 +114,7 @@ describe('API authz tests', () => {

beforeEach(() => {
jest.spyOn(otomiStack, 'createTeam').mockResolvedValue({ name: 'team', resourceQuota: [] })
jest.spyOn(getValuesSchemaModule, 'getValuesSchema').mockResolvedValue({})
})

afterEach(() => {
Expand Down
4 changes: 2 additions & 2 deletions src/api/v1/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const debug = Debug('otomi:api:v1:apps')
* Get all apps across all teams
* Returns list of all apps with their ids and enabled status
*/
export const getApps = (req: OpenApiRequestExt, res: Response): void => {
export const getApps = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
debug('getAllApps')
res.json(req.otomi.getApps())
res.json(await req.otomi.getApps())
}
4 changes: 2 additions & 2 deletions src/api/v1/apps/{teamId}.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ const debug = Debug('otomi:api:v1:apps')
* Get apps for a team
* Returns list of team apps with their ids and enabled status
*/
export const getTeamApps = (req: OpenApiRequestExt, res: Response): void => {
export const getTeamApps = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId } = req.params
debug('getTeamApps', teamId)
res.json(req.otomi.getTeamApps(teamId))
res.json(await req.otomi.getTeamApps(teamId))
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/api/v1/apps/{teamId}/{appId}.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { App, OpenApiRequestExt } from 'src/otomi-models'
* GET /v1/apps/{teamId}/{appId}
* Get a specific team app
*/
export const getTeamApp = (req: OpenApiRequestExt, res: Response): void => {
export const getTeamApp = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId, appId } = req.params
res.json(req.otomi.getTeamApp(teamId, appId))
res.json(await req.otomi.getTeamApp(teamId, appId))
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/api/v1/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ const debug = Debug('otomi:api:v1:dashboard')
* GET /v1/dashboard
* Get dashboard information
*/
export const getDashboard = (req: OpenApiRequestExt, res: Response): void => {
export const getDashboard = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId } = req.query
debug(`getDashboard(${teamId})`)
const v = req.otomi.getDashboard(teamId as string)
const v = await req.otomi.getDashboard(teamId as string)
res.json(v)
}
4 changes: 2 additions & 2 deletions src/api/v1/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const debug = Debug('otomi:api:v1:services')
* GET /v1/services
* Get all services running on the cluster
*/
export const getAllServices = (req: OpenApiRequestExt, res: Response): void => {
export const getAllServices = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
debug('getAllServices')
const v = req.otomi.getAllServices()
const v = await req.otomi.getAllServices()
res.json(v)
}
11 changes: 3 additions & 8 deletions src/api/v1/settings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Debug from 'debug'
import { Response } from 'express'
import { omit } from 'lodash'
import { OpenApiRequestExt } from 'src/otomi-models'

const debug = Debug('otomi:api:v1:settings')
Expand All @@ -9,15 +8,11 @@ const debug = Debug('otomi:api:v1:settings')
* GET /v1/settings
* Get settings (optionally filtered by IDs)
*/
export const getSettings = (req: OpenApiRequestExt, res: Response): void => {
export const getSettings = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { ids } = req.query
// Handle comma-separated string or array
const idsArray = ids ? (typeof ids === 'string' ? ids.split(',') : (ids as string[])) : undefined
debug(`getSettings(${idsArray})`)
const v = req.otomi.getSettings(idsArray)
if (v?.otomi) {
res.json(omit(v, ['otomi.adminPassword', 'otomi.git.password']))
} else {
res.json(v)
}
const v = await req.otomi.getSettings(idsArray)
res.json(v)
}
7 changes: 1 addition & 6 deletions src/api/v1/settings/{settingId}.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Debug from 'debug'
import { Response } from 'express'
import { OpenApiRequestExt, Settings } from 'src/otomi-models'
import { omit } from 'lodash'

const debug = Debug('otomi:api:v1:settings')

Expand All @@ -14,9 +13,5 @@ export const editSettings = async (req: OpenApiRequestExt, res: Response): Promi
const ids = Object.keys(req.body as Settings)
debug(`editSettings(${ids.join(',')})`)
const v = await req.otomi.editSettings(req.body as Settings, settingId)
if (v?.otomi) {
res.json(omit(v, ['otomi.adminPassword', 'otomi.git.password']))
} else {
res.json(v)
}
res.json(v)
}
4 changes: 2 additions & 2 deletions src/api/v1/settingsInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const debug = Debug('otomi:api:v1:settingsinfo')
* GET /v1/settingsInfo
* Get settings info
*/
export const getSettingsInfo = (req: OpenApiRequestExt, res: Response): void => {
export const getSettingsInfo = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
debug('getSettingsInfo')
res.json(req.otomi.getSettingsInfo())
res.json(await req.otomi.getSettingsInfo())
}
4 changes: 2 additions & 2 deletions src/api/v1/teams/{teamId}/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const debug = Debug('otomi:api:v1:teams:services')
* GET /v1/teams/{teamId}/services
* Get services from a given team
*/
export const getTeamServices = (req: OpenApiRequestExt, res: Response): void => {
export const getTeamServices = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId } = req.params
debug(`getTeamServices(${teamId})`)
const v = req.otomi.getTeamServices(teamId)
const v = await req.otomi.getTeamServices(teamId)
res.json(v)
}

Expand Down
4 changes: 2 additions & 2 deletions src/api/v1/teams/{teamId}/services/{serviceName}.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const debug = Debug('otomi:api:v1:teams:services')
* GET /v1/teams/{teamId}/services/{serviceName}
* Get a specific service
*/
export const getService = (req: OpenApiRequestExt, res: Response): void => {
export const getService = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId, serviceName } = req.params
debug(`getService(${serviceName})`)
const data = req.otomi.getService(decodeURIComponent(teamId), decodeURIComponent(serviceName))
const data = await req.otomi.getService(decodeURIComponent(teamId), decodeURIComponent(serviceName))
res.json(data)
}

Expand Down
4 changes: 2 additions & 2 deletions src/api/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ const debug = Debug('otomi:api:v1:users')
* GET /v1/users
* Get all users
*/
export const getAllUsers = (req: OpenApiRequestExt, res: Response): void => {
export const getAllUsers = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
debug('getAllUsers')
const v = req.otomi.getAllUsers(req.user)
const v = await req.otomi.getAllUsers(req.user)
res.json(v)
}

Expand Down
4 changes: 2 additions & 2 deletions src/api/v1/users/{userId}.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ const debug = Debug('otomi:api:v1:users')
* GET /v1/users/{userId}
* Get a specific user
*/
export const getUser = (req: OpenApiRequestExt, res: Response): void => {
export const getUser = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { userId } = req.params
debug(`getUser(${userId})`)
const data = req.otomi.getUser(decodeURIComponent(userId), req.user)
const data = await req.otomi.getUser(decodeURIComponent(userId), req.user)
res.json(data)
}

Expand Down
13 changes: 3 additions & 10 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { apiRateLimiter, authRateLimiter } from 'src/middleware/rate-limit'
import { setMockIdx } from 'src/mocks'
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 { getValuesSchema } from 'src/utils'
import {
CATALOG_CACHE_REFRESH_INTERVAL_MS,
CHECK_LATEST_COMMIT_INTERVAL,
Expand All @@ -48,7 +48,6 @@ debug('NODE_ENV: ', process.env.NODE_ENV)

type OtomiSpec = {
spec: OpenAPIDoc
secretPaths: string[]
valuesSchema: Record<string, any>
}

Expand Down Expand Up @@ -84,7 +83,7 @@ const resourceStatus = async (errorSet) => {
debug('Values are not loaded yet')
return
}
const { cluster } = otomiStack.getSettings(['cluster'])
const { cluster } = await otomiStack.getSettings(['cluster'])
const domainSuffix = cluster?.domainSuffix
const resources: Record<string, (AplResponseObject | SealedSecretManifestResponse)[]> = {
workloads: otomiStack.getAllAplWorkloads(),
Expand Down Expand Up @@ -126,17 +125,11 @@ export const loadSpec = async (): Promise<void> => {
debug(`Loading api spec from: ${openApiPath}`)
const spec = (await $parser.parse(openApiPath)) as OpenAPIDoc
const valuesSchema = await getValuesSchema()
const secrets = extract(valuesSchema, (o, i) => i === 'x-secret')
const secretPaths = getPaths(secrets)
otomiSpec = { spec, secretPaths, valuesSchema }
otomiSpec = { spec, valuesSchema }
}
export const getSpec = (): OtomiSpec => {
return otomiSpec
}
export function getSecretPaths(): string[] {
const { secretPaths } = getSpec()
return secretPaths
}
export const getAppSchema = (appId: string): Schema => {
let id: string = appId
if (appId.startsWith('ingress-nginx')) id = 'ingress-nginx-platform'
Expand Down
Loading
Loading