From bac9fc1d3d3c6e9328bd051ee79bc48bf035ff5a Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:17:49 +0200 Subject: [PATCH 01/21] ci: agent skill to validate gotmpl renders --- .github/copilot-instructions.md | 42 +++++-------------- .../skills/test-gotemplate-renders/SKILL.md | 32 ++++++++++++++ AGENTS.md | 15 +++++++ CLAUDE.md | 1 + 4 files changed, 58 insertions(+), 32 deletions(-) create mode 100644 .github/skills/test-gotemplate-renders/SKILL.md create mode 100644 AGENTS.md create mode 120000 CLAUDE.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 46d8a14e4a..30468a7066 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -31,44 +31,12 @@ All Helmfile specs in `helmfile.d/` execute alphabetically. Use reusable anchors All user-configurable parameters MUST be defined in `values-schema.yaml` (JSON Schema). Run `npm run validate-values` to validate. The schema serves as both validation and documentation. -## CLI Commands & Workflow - -### Essential Commands - -```bash -# Bootstrap a new values repo (creates $ENV_DIR with defaults) -otomi bootstrap - -# Validate user configuration against schema -otomi validate-values - -# Validate rendered Kubernetes manifests -otomi validate-templates [-l name=myapp] - -# Render values for inspection -otomi values - -# Render chart values for a specific app -otomi x helmfile -l name=myapp write-values - -# Deploy all charts (or use -l name=myapp for selective deploy) -otomi apply [-l name=myapp] - -# Generate diff before applying -otomi diff [-l name=myapp] - -# Deploy to cluster (initial setup) -otomi install -``` - ### Development Setup ```bash # Install dependencies (helmfile, helm, kubectl, etc.) npm run install-deps -# Run CLI locally (bypass Docker) -export IN_DOCKER=false export ENV_DIR=$PWD/tests/fixtures export NODE_ENV=test @@ -111,6 +79,16 @@ The `binzx/otomi` script wraps all commands in Docker by default: - Template validation: `otomi validate-templates` (validates all rendered manifests against K8s schemas) - Policy tests: `npm run test:opa` (Rego policy testing) +### Mandatory Template Diff Check + +When changing files under `charts/`, `values/`, or `helmfile.d/`, always run: + +```bash +bin/compare.sh +``` + +Review and summarize rendered diffs before finalizing changes. + ## Key Files & Directories | Path | Purpose | diff --git a/.github/skills/test-gotemplate-renders/SKILL.md b/.github/skills/test-gotemplate-renders/SKILL.md new file mode 100644 index 0000000000..2630697a25 --- /dev/null +++ b/.github/skills/test-gotemplate-renders/SKILL.md @@ -0,0 +1,32 @@ +--- +name: test-gotemplate-renders +description: Enforces rendered Go template comparison before and after templating-related changes using bin/compare.sh. +--- + +# Compare Go Template Renders + +## Quick start + +1. Make your code changes. +2. export ENV_DIR=$PWD/tests/fixtures (if not already set) +3. Run: + +```bash +bin/compare.sh +``` + +3. Review diffs in rendered output and confirm they match intent. + +## Workflows + +### Standard workflow for templating changes + +- [ ] Run `bin/compare.sh` after making changes. +- [ ] Inspect before/after rendered template differences. +- [ ] Verify differences are expected and limited to intended behavior. +- [ ] If unexpected diffs appear, revise changes and rerun `bin/compare.sh`. +- [ ] Include a brief summary of observed render diffs in your report/PR notes. + +### Scope guardrail + +Use it only when changes affect `charts/**`, `values/**`, or `helmfile.d/**` diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..0583ea8afb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# TDD + +## Mandatory Template Diff Check + +When code changes any file under `charts/`, `values/`, or `helmfile.d/`, run: + +if no ENV_DIR is set then + +```bash + +export ENV_DIR=$PWD/tests/fixtures +bin/compare.sh +``` + +Review and summarize the rendered diffs before finalizing changes. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From 5a22779704ce3ea46482a82313205650c2cb008f Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:04:07 +0200 Subject: [PATCH 02/21] ci: add skill to add new helm chart --- .github/skills/add-helm-chart/SKILL.md | 57 ++ .../add-helm-chart/scripts/add-helm-chart.sh | 7 + ci/src/add-helm-chart.test.ts | 61 ++ ci/src/add-helm-chart.ts | 587 ++++++++++++++++++ jest.config.ts | 2 +- 5 files changed, 713 insertions(+), 1 deletion(-) create mode 100644 .github/skills/add-helm-chart/SKILL.md create mode 100755 .github/skills/add-helm-chart/scripts/add-helm-chart.sh create mode 100644 ci/src/add-helm-chart.test.ts create mode 100644 ci/src/add-helm-chart.ts diff --git a/.github/skills/add-helm-chart/SKILL.md b/.github/skills/add-helm-chart/SKILL.md new file mode 100644 index 0000000000..8b4caad305 --- /dev/null +++ b/.github/skills/add-helm-chart/SKILL.md @@ -0,0 +1,57 @@ +--- +name: add-helm-chart +description: Add and wire a new Helm chart into APL Core by updating chart index, vendoring the chart, scaffolding Helmfile/defaults/values files, and running required validation checks. Use when adding a new app chart and the user provides or asks for name and repository; the script resolves the latest official stable semver chart version automatically. +argument-hint: +--- + +# Add Helm Chart + +## Quick start + +1. Ask for required inputs: chart name and chart repository. +2. Ask if the app must be publicly exposed. +3. If public is needed, offer security scaffolding for HTTPRoute + Istio RequestAuthentication + AuthorizationPolicy. +4. Run: + +```bash +npx tsx ci/src/add-helm-chart.ts +``` + +3. Validate: + +```bash +npm run validate-values +bin/compare.sh +``` + +## Workflow + +Checklist: + +- [ ] Collect required parameters: name and repository. +- [ ] Resolve latest official stable semver chart version automatically. +- [ ] Keep chart name, app key, release name, and namespace identical. +- [ ] Ask if the app is public and whether to scaffold HTTPRoute/authentication/authorization resources. +- [ ] Add dependency entry in chart/chart-index/Chart.yaml. +- [ ] Vendor chart into charts//. +- [ ] Create a new Helmfile file with the next highest numeric prefix in helmfile.d. +- [ ] Add Helmfile release stanza in that new file using \*default anchor. +- [ ] Always add namespace entry in core.yaml. +- [ ] If public, ensure sidecar injection is enabled for that namespace. +- [ ] Add empty defaults entry in helmfile.d/snippets/defaults.yaml in alphabetical order. +- [ ] Create values//.gotmpl. +- [ ] Add tests fixture app file in tests/fixtures/env/apps/.yaml. +- [ ] Add corresponding app entries in tests/integration/\*.yaml. +- [ ] Run schema and template-diff checks. +- [ ] Summarize generated diffs and touched files. + +## Notes + +- Default release anchor used by script is \*default. +- Script is idempotent for existing entries and directories. +- New Chart.yaml dependency entry is inserted in alphabetical order by dependency name. +- If chart source is OCI, repository should be oci://... (script pulls from /). +- Chart version is auto-selected as latest stable semver; pre-release versions are ignored. +- Main implementation is in ci/src/add-helm-chart.ts. +- .github/skills/add-helm-chart/scripts/add-helm-chart.sh is a compatibility wrapper. +- Script confirms rendering path by running bin/compare.sh. diff --git a/.github/skills/add-helm-chart/scripts/add-helm-chart.sh b/.github/skills/add-helm-chart/scripts/add-helm-chart.sh new file mode 100755 index 0000000000..70a9bc1064 --- /dev/null +++ b/.github/skills/add-helm-chart/scripts/add-helm-chart.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)" +cd "$repo_root" + +exec npx tsx ci/src/add-helm-chart.ts "$@" diff --git a/ci/src/add-helm-chart.test.ts b/ci/src/add-helm-chart.test.ts new file mode 100644 index 0000000000..deb3f664c6 --- /dev/null +++ b/ci/src/add-helm-chart.test.ts @@ -0,0 +1,61 @@ +import { parseStableSemver, resolveLatestOfficialSemver, type RuntimeDeps } from './add-helm-chart' + +describe('add-helm-chart semver resolution', () => { + const makeDeps = (runCommand: RuntimeDeps['runCommand']): RuntimeDeps => ({ + runCommand, + pid: 4242, + log: jest.fn(), + }) + + it('accepts stable semver and trims v-prefix', () => { + const parsed = parseStableSemver('v1.2.3') + expect(parsed?.version).toBe('1.2.3') + }) + + it('rejects prerelease semver', () => { + const parsed = parseStableSemver('1.2.3-rc.1') + expect(parsed).toBeNull() + }) + + it('resolves highest stable semver from helm repo search', () => { + const runCommand = jest.fn, Parameters>() + runCommand.mockReturnValueOnce('') + runCommand.mockReturnValueOnce( + JSON.stringify([ + { name: 'apl-temp-dex-4242/dex', version: '0.24.1' }, + { name: 'apl-temp-dex-4242/dex', version: '0.25.0-rc.1' }, + { name: 'apl-temp-dex-4242/dex', version: '0.23.0' }, + ]), + ) + runCommand.mockReturnValueOnce('') + + const version = resolveLatestOfficialSemver('dex', 'https://charts.dexidp.io', makeDeps(runCommand)) + + expect(version).toBe('0.24.1') + expect(runCommand).toHaveBeenNthCalledWith(1, 'helm repo add apl-temp-dex-4242 https://charts.dexidp.io', { stdio: 'inherit' }) + expect(runCommand).toHaveBeenNthCalledWith(2, 'helm search repo apl-temp-dex-4242/dex --versions -o json', { encoding: 'utf8' }) + expect(runCommand).toHaveBeenNthCalledWith(3, 'helm repo remove apl-temp-dex-4242', { stdio: 'inherit' }) + }) + + it('removes temporary repo alias even when search fails', () => { + const runCommand = jest.fn, Parameters>() + runCommand.mockReturnValueOnce('') + runCommand.mockImplementationOnce(() => { + throw new Error('search failed') + }) + runCommand.mockReturnValueOnce('') + + expect(() => resolveLatestOfficialSemver('dex', 'https://charts.dexidp.io', makeDeps(runCommand))).toThrow('search failed') + expect(runCommand).toHaveBeenNthCalledWith(3, 'helm repo remove apl-temp-dex-4242', { stdio: 'inherit' }) + }) + + it('resolves stable semver from OCI chart metadata', () => { + const runCommand = jest.fn, Parameters>() + runCommand.mockReturnValue('name: dex\nversion: 0.24.2\n') + + const version = resolveLatestOfficialSemver('dex', 'oci://ghcr.io/dexidp/charts', makeDeps(runCommand)) + + expect(version).toBe('0.24.2') + expect(runCommand).toHaveBeenCalledWith('helm show chart oci://ghcr.io/dexidp/charts/dex', { encoding: 'utf8' }) + }) +}) diff --git a/ci/src/add-helm-chart.ts b/ci/src/add-helm-chart.ts new file mode 100644 index 0000000000..117c97268e --- /dev/null +++ b/ci/src/add-helm-chart.ts @@ -0,0 +1,587 @@ +import type { ExecSyncOptions } from 'node:child_process' +import { execSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import readline from 'node:readline/promises' +import semver from 'semver' + +type Opts = { + name: string + repository: string + public: boolean + configurePublicSecurity: boolean + runCompare: boolean +} + +export type CommandRunner = (command: string, options?: ExecSyncOptions) => string + +export type RuntimeDeps = { + runCommand: CommandRunner + pid: number + log: (message: string) => void +} + +const defaultRunCommand: CommandRunner = (command, options = {}) => { + const result = execSync(command, options) + return typeof result === 'string' ? result : result.toString('utf8') +} + +const defaultDeps: RuntimeDeps = { + runCommand: defaultRunCommand, + pid: process.pid, + log: (message) => console.log(message), +} + +function usage(): never { + console.error( + 'Usage: npx tsx ci/src/add-helm-chart.ts [--public true|false] [--configure-public-security true|false] [--run-compare true|false]', + ) + process.exit(2) +} + +function parseBool(value: string, flag: string): boolean { + if (value === 'true') return true + if (value === 'false') return false + throw new Error(`Invalid value for ${flag}: ${value}. Expected true|false.`) +} + +async function parseArgs(argv: string[]): Promise { + if (argv.length < 2) usage() + + const name = argv[0] + const repository = argv[1] + + let isPublic: boolean | undefined + let configurePublicSecurity: boolean | undefined + let runCompare = true + + for (let i = 2; i < argv.length; i += 1) { + const arg = argv[i] + if (arg === '--public') { + const value = argv[i + 1] + if (!value) usage() + isPublic = parseBool(value, '--public') + i += 1 + continue + } + if (arg === '--configure-public-security') { + const value = argv[i + 1] + if (!value) usage() + configurePublicSecurity = parseBool(value, '--configure-public-security') + i += 1 + continue + } + if (arg === '--run-compare') { + const value = argv[i + 1] + if (!value) usage() + runCompare = parseBool(value, '--run-compare') + i += 1 + continue + } + throw new Error(`Unknown argument: ${arg}`) + } + + if (isPublic === undefined && process.stdin.isTTY) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) + const ans = await rl.question('Expose app publicly? (true/false) [false]: ') + rl.close() + isPublic = ans.trim() === '' ? false : parseBool(ans.trim(), '--public') + } + if (isPublic === undefined) isPublic = false + + if (configurePublicSecurity === undefined) { + if (isPublic && process.stdin.isTTY) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) + const ans = await rl.question('Scaffold HTTPRoute + Istio authn/authz resources? (true/false) [true]: ') + rl.close() + configurePublicSecurity = ans.trim() === '' ? true : parseBool(ans.trim(), '--configure-public-security') + } else { + configurePublicSecurity = false + } + } + + return { + name, + repository, + public: isPublic, + configurePublicSecurity, + runCompare, + } +} + +export function parseStableSemver(version: string): semver.SemVer | null { + const cleaned = semver.valid(version) ?? semver.valid(version.replace(/^v/, '')) + if (!cleaned) return null + const parsed = semver.parse(cleaned) + if (!parsed) return null + if (parsed.prerelease.length > 0) return null + return parsed +} + +export function resolveLatestOfficialSemver(name: string, repository: string, deps: RuntimeDeps = defaultDeps): string { + if (repository.startsWith('oci://')) { + const out = deps.runCommand(`helm show chart ${repository}/${name}`, { encoding: 'utf8' }) + const m = out.match(/^version:\s*(.+)$/m) + if (!m) { + throw new Error(`Could not resolve chart version from OCI source for ${name}`) + } + const version = m[1].trim() + const parsed = parseStableSemver(version) + if (!parsed) { + throw new Error(`Latest OCI chart version is not a stable semver: ${version}`) + } + return version + } + + const tempAlias = `apl-temp-${name}-${deps.pid}` + deps.runCommand(`helm repo add ${tempAlias} ${repository}`, { stdio: 'inherit' }) + let out = '' + try { + out = deps.runCommand(`helm search repo ${tempAlias}/${name} --versions -o json`, { encoding: 'utf8' }) + } finally { + deps.runCommand(`helm repo remove ${tempAlias}`, { stdio: 'inherit' }) + } + const items = JSON.parse(out) as Array<{ name?: string; version?: string }> + const candidates = items + .filter((i) => i.name?.endsWith(`/${name}`) || i.name === name) + .map((i) => ({ raw: i.version ?? '', parsed: parseStableSemver(i.version ?? '') })) + .filter((i) => i.parsed !== null) as Array<{ raw: string; parsed: semver.SemVer }> + + if (candidates.length === 0) { + throw new Error(`No stable semver chart versions found for ${name} in ${repository}`) + } + + candidates.sort((a, b) => semver.rcompare(a.parsed.version, b.parsed.version)) + return candidates[0].raw +} + +function requireFile(filePath: string): void { + if (!fs.existsSync(filePath)) { + throw new Error(`Missing required file: ${filePath}`) + } +} + +function readLines(filePath: string): string[] { + return fs.readFileSync(filePath, 'utf8').split(/\r?\n/) +} + +function writeLines(filePath: string, lines: string[]): void { + fs.writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf8') +} + +function findHighestHelmfileNumber(helmfileDir: string): number { + const names = fs.readdirSync(helmfileDir) + let max = 0 + for (const name of names) { + const m = name.match(/^helmfile-(\d+)\..*\.yaml\.gotmpl$/) + if (!m) continue + const n = Number(m[1]) + if (n > max) max = n + } + return max +} + +function hasReleaseNameInHelmfiles(helmfileDir: string, releaseName: string): boolean { + const files = fs.readdirSync(helmfileDir).filter((f) => f.endsWith('.yaml') || f.endsWith('.gotmpl')) + const rx = new RegExp(`^\\s*-\\s+name:\\s+${releaseName}\\s*$`, 'm') + for (const file of files) { + const content = fs.readFileSync(path.join(helmfileDir, file), 'utf8') + if (rx.test(content)) return true + } + return false +} + +function insertSortedBlock( + lines: string[], + startMatcher: (line: string) => boolean, + itemMatcher: (line: string) => RegExpMatchArray | null, + sectionEndMatcher: (line: string) => boolean, + getKey: (m: RegExpMatchArray) => string, + newKey: string, + emitBlock: () => string[], +): string[] { + const out: string[] = [] + let inSection = false + let inserted = false + + for (const line of lines) { + if (!inSection && startMatcher(line)) { + out.push(line) + inSection = true + continue + } + + if (inSection && !inserted) { + const item = itemMatcher(line) + if (item) { + const key = getKey(item) + if (key > newKey) { + out.push(...emitBlock()) + inserted = true + } + } else if (sectionEndMatcher(line)) { + out.push(...emitBlock()) + inserted = true + inSection = false + } + } + + out.push(line) + } + + if (inSection && !inserted) { + out.push(...emitBlock()) + inserted = true + } + + if (!inserted) { + throw new Error('Failed to insert block in target section') + } + + return out +} + +function ensureChartDependency(chartIndexPath: string, name: string, version: string, repository: string): void { + const content = fs.readFileSync(chartIndexPath, 'utf8') + if (content.includes(`- name: ${name}\n`)) { + console.log(`Chart index already contains dependency for ${name}`) + return + } + + const lines = readLines(chartIndexPath) + const next = insertSortedBlock( + lines, + (line) => line.trim() === 'dependencies:', + (line) => line.match(/^ - name: (.+)$/), + () => false, + (m) => m[1], + name, + () => [` - name: ${name}`, ` version: ${version}`, ` repository: ${repository}`], + ) + + writeLines(chartIndexPath, next) + console.log('Added dependency to chart/chart-index/Chart.yaml') +} + +function vendorChart(root: string, name: string, version: string, repository: string, deps: RuntimeDeps = defaultDeps): void { + const chartDir = path.join(root, 'charts', name) + if (fs.existsSync(chartDir)) { + console.log(`charts/${name} already exists; skipping chart pull`) + return + } + + if (repository.startsWith('oci://')) { + deps.runCommand(`helm pull ${repository}/${name} --version ${version} --untar --untardir ${path.join(root, 'charts')}`, { + stdio: 'inherit', + }) + } else { + deps.runCommand(`helm pull ${name} --repo ${repository} --version ${version} --untar --untardir ${path.join(root, 'charts')}`, { + stdio: 'inherit', + }) + } + console.log(`Vendored chart into charts/${name}`) +} + +function createHelmfile(root: string, name: string, isPublic: boolean, configurePublicSecurity: boolean): string { + const next = findHighestHelmfileNumber(path.join(root, 'helmfile.d')) + 1 + const padded = String(next).padStart(2, '0') + const relPath = `helmfile.d/helmfile-${padded}.${name}.yaml.gotmpl` + const fullPath = path.join(root, relPath) + + const parts: string[] = [ + 'bases:', + ' - snippets/defaults.yaml', + '---', + 'bases:', + ' - snippets/env.gotmpl', + '---', + 'bases:', + ' - snippets/derived.gotmpl', + '---', + '{{ readFile "snippets/templates.gotmpl" }}', + '{{- $v := .Values }}', + '{{- $a := $v.apps }}', + '', + 'releases:', + ] + + if (isPublic && configurePublicSecurity) { + parts.push( + ` - name: ${name}-artifacts`, + ` installed: {{ $a | get "${name}.enabled" }}`, + ` namespace: ${name}`, + ' labels:', + ` pkg: ${name}`, + ' app: core', + ' <<: *raw', + ) + } + + parts.push( + ` - name: ${name}`, + ` installed: {{ $a | get "${name}.enabled" }}`, + ` namespace: ${name}`, + ' labels:', + ` pkg: ${name}`, + ' app: core', + ' <<: *default', + '', + ) + + fs.writeFileSync(fullPath, parts.join('\n'), 'utf8') + console.log(`Created new Helmfile release file: ${relPath}`) + return relPath +} + +function ensureCoreNamespace(corePath: string, name: string, isPublic: boolean): void { + const lines = readLines(corePath) + if (lines.some((line) => line === ` - name: ${name}`)) { + console.log(`core.yaml already contains namespace ${name}`) + return + } + + const next = insertSortedBlock( + lines, + (line) => line.trim() === 'namespaces:', + (line) => line.match(/^ - name: (.+)$/), + (line) => line === 'adminApps:', + (m) => m[1], + name, + () => { + const block = [` - name: ${name}`, ` app: ${name}`] + if (!isPublic) block.push(' disableIstioInjection: true') + return block + }, + ) + + writeLines(corePath, next) + console.log(`Added namespace ${name} to core.yaml`) +} + +function ensureAdminApp(corePath: string, name: string): void { + const lines = readLines(corePath) + + let inAdmin = false + for (const line of lines) { + if (line === 'adminApps:') { + inAdmin = true + continue + } + if (line === 'teamApps:') { + inAdmin = false + } + if (inAdmin && line === ` - name: ${name}`) { + console.log(`core.yaml already contains adminApps entry for ${name}`) + return + } + } + + const next = insertSortedBlock( + lines, + (line) => line === 'adminApps:', + (line) => line.match(/^ - name: (.+)$/), + (line) => line === 'teamApps:', + (m) => m[1], + name, + () => [` - name: ${name}`, ' tags: [custom]', ' ownHost: true'], + ) + + writeLines(corePath, next) + console.log(`Added adminApps public entry for ${name} in core.yaml`) +} + +function ensureDefaultsEntry(defaultsPath: string, name: string): void { + const lines = readLines(defaultsPath) + if (lines.some((line) => line === ` ${name}:`)) { + console.log(`defaults already contains apps.${name}`) + return + } + + const next = insertSortedBlock( + lines, + (line) => line === ' apps:', + (line) => line.match(/^ ([A-Za-z0-9._-]+):$/), + (line) => /^ [A-Za-z0-9._-]+:/.test(line), + (m) => m[1], + name, + () => [` ${name}:`, ' enabled: false', ' _rawValues: {}'], + ) + + writeLines(defaultsPath, next) + console.log('Added apps entry in helmfile.d/snippets/defaults.yaml') +} + +function ensureValuesFile(valuesFile: string, name: string): void { + fs.mkdirSync(path.dirname(valuesFile), { recursive: true }) + if (fs.existsSync(valuesFile)) { + console.log(`values file already exists: values/${name}/${name}.gotmpl`) + return + } + fs.writeFileSync(valuesFile, '{}\n', 'utf8') + console.log(`Created values/${name}/${name}.gotmpl`) +} + +function ensureRawValuesFile(rawValuesFile: string, name: string): void { + if (fs.existsSync(rawValuesFile)) { + console.log(`raw values file already exists: values/${name}/${name}-raw.gotmpl`) + return + } + + const content = `{{- $v := .Values }} +{{- $httpRoute := tpl (readFile "../../helmfile.d/snippets/routes.gotmpl") $v | fromYaml }} +resources: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: ${name} + {{ with $httpRoute.annotations }} + annotations: + {{ . | toYaml | nindent 8 }} + {{- end }} + spec: + parentRefs: + {{- $httpRoute.parentRefs | toYaml | nindent 8 }} + hostnames: + - {{ printf "${name}.%s" $v.cluster.domainSuffix }} + rules: + {{- $httpRoute.authRules | toYaml | nindent 8 }} + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - kind: Service + name: ${name} + port: 80 + - apiVersion: security.istio.io/v1 + kind: RequestAuthentication + metadata: + name: ${name}-auth + namespace: ${name} + spec: + selector: + matchLabels: + app.kubernetes.io/name: ${name} + jwtRules: + - issuer: {{ $v._derived.oidcBaseUrl }} + jwksUri: {{ $v._derived.oidcBaseUrlBackchannel }}/protocol/openid-connect/certs + fromHeaders: + - name: Authorization + prefix: "Bearer " + - apiVersion: security.istio.io/v1 + kind: AuthorizationPolicy + metadata: + name: ${name}-authz + namespace: ${name} + spec: + action: ALLOW + selector: + matchLabels: + app.kubernetes.io/name: ${name} + rules: + - from: + - source: + requestPrincipals: ["*"] +` + + fs.writeFileSync(rawValuesFile, content, 'utf8') + console.log(`Created values/${name}/${name}-raw.gotmpl for public route + auth scaffolding`) +} + +function ensureFixtureAppFile(fixturePath: string, name: string): void { + if (fs.existsSync(fixturePath)) { + console.log(`fixture app file already exists: tests/fixtures/env/apps/${name}.yaml`) + return + } + + const content = `kind: AplApp +metadata: + name: ${name} +spec: + _rawValues: {} + enabled: false +` + fs.writeFileSync(fixturePath, content, 'utf8') + console.log(`Created tests fixture app file: tests/fixtures/env/apps/${name}.yaml`) +} + +function ensureIntegrationEntries(integrationDir: string, name: string): void { + for (const file of fs.readdirSync(integrationDir)) { + if (!file.endsWith('.yaml')) continue + const filePath = path.join(integrationDir, file) + const lines = readLines(filePath) + + if (lines.some((line) => line === ` ${name}:`)) { + console.log(`Integration file already contains apps.${name}: ${file}`) + continue + } + + const next = insertSortedBlock( + lines, + (line) => line === 'apps:', + (line) => line.match(/^ ([A-Za-z0-9._-]+):$/), + (line) => line === 'teamConfig:' || line === 'files:', + (m) => m[1], + name, + () => [` ${name}:`, ' enabled: false'], + ) + + writeLines(filePath, next) + console.log(`Updated integration file: tests/integration/${file}`) + } +} + +function runCompareIfNeeded(root: string, runCompare: boolean, deps: RuntimeDeps = defaultDeps): void { + if (!runCompare) return + const env = { ...process.env } + if (!env.ENV_DIR) env.ENV_DIR = path.join(root, 'tests/fixtures') + console.log('Running bin/compare.sh to confirm rendering with new files...') + deps.runCommand(path.join(root, 'bin/compare.sh'), { stdio: 'inherit', env }) +} + +export async function main(deps: RuntimeDeps = defaultDeps): Promise { + const opts = await parseArgs(process.argv.slice(2)) + + const root = path.resolve(__dirname, '..', '..') + const chartIndex = path.join(root, 'chart/chart-index/Chart.yaml') + const defaultsFile = path.join(root, 'helmfile.d/snippets/defaults.yaml') + const coreFile = path.join(root, 'core.yaml') + const helmfileDir = path.join(root, 'helmfile.d') + const valuesFile = path.join(root, 'values', opts.name, `${opts.name}.gotmpl`) + const rawValuesFile = path.join(root, 'values', opts.name, `${opts.name}-raw.gotmpl`) + const fixtureAppFile = path.join(root, 'tests/fixtures/env/apps', `${opts.name}.yaml`) + const integrationDir = path.join(root, 'tests/integration') + + requireFile(chartIndex) + requireFile(defaultsFile) + requireFile(coreFile) + + if (hasReleaseNameInHelmfiles(helmfileDir, opts.name)) { + throw new Error(`A release named ${opts.name} already exists in helmfile.d; refusing to create duplicate release`) + } + + const resolvedVersion = resolveLatestOfficialSemver(opts.name, opts.repository, deps) + deps.log(`Resolved latest stable chart version for ${opts.name}: ${resolvedVersion}`) + + ensureChartDependency(chartIndex, opts.name, resolvedVersion, opts.repository) + vendorChart(root, opts.name, resolvedVersion, opts.repository, deps) + createHelmfile(root, opts.name, opts.public, opts.configurePublicSecurity) + ensureCoreNamespace(coreFile, opts.name, opts.public) + if (opts.public) ensureAdminApp(coreFile, opts.name) + ensureDefaultsEntry(defaultsFile, opts.name) + ensureValuesFile(valuesFile, opts.name) + if (opts.public && opts.configurePublicSecurity) ensureRawValuesFile(rawValuesFile, opts.name) + ensureFixtureAppFile(fixtureAppFile, opts.name) + ensureIntegrationEntries(integrationDir, opts.name) + runCompareIfNeeded(root, opts.runCompare, deps) + + deps.log('Done. Next: npm run validate-values') +} + +if (require.main === module) { + main().catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + console.error(message) + process.exit(1) + }) +} diff --git a/jest.config.ts b/jest.config.ts index bd131cd235..8f00a2c2b1 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -2,7 +2,7 @@ import type { Config } from '@jest/types' const config: Config.InitialOptions = { preset: 'ts-jest', - roots: ['/src'], + roots: ['/src', '/ci/src'], setupFilesAfterEnv: ['/src/test-init.ts'], moduleDirectories: ['node_modules', __dirname], modulePathIgnorePatterns: ['src/cmd/test.ts'], From 7ff4225702a52ce753804ca0b279eea654dc2e6b Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 12:17:39 +0200 Subject: [PATCH 03/21] ci: add skill to add new helm chart --- ci/src/add-helm-chart.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/src/add-helm-chart.ts b/ci/src/add-helm-chart.ts index 117c97268e..de081fb378 100644 --- a/ci/src/add-helm-chart.ts +++ b/ci/src/add-helm-chart.ts @@ -23,6 +23,7 @@ export type RuntimeDeps = { const defaultRunCommand: CommandRunner = (command, options = {}) => { const result = execSync(command, options) + if (result === null) return '' return typeof result === 'string' ? result : result.toString('utf8') } From 96bb0f03bb36d2d761b83b0a18bcea94ad0df899 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:31:22 +0200 Subject: [PATCH 04/21] ci: use dyff to show content of new files --- bin/dyff.sh | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/bin/dyff.sh b/bin/dyff.sh index c3d59c7f19..03d5a98e7c 100755 --- a/bin/dyff.sh +++ b/bin/dyff.sh @@ -92,10 +92,46 @@ echo "$diff_output" | while read -r line; do elif [[ $line =~ ^Only[[:space:]]+in[[:space:]]+(.+):[[:space:]]+(.+)$ ]]; then only_in_dir="${BASH_REMATCH[1]}" only_in_file="${BASH_REMATCH[2]}" + full_path="$only_in_dir/$only_in_file" + if [[ "$only_in_dir" == "$targetDirA"* ]]; then - print_comment "New file added: $(join_relative_path "$(to_relative_path "$only_in_dir" "$targetDirA")" "$only_in_file")" + file_path=$(join_relative_path "$(to_relative_path "$only_in_dir" "$targetDirA")" "$only_in_file") + if [[ -d "$full_path" ]]; then + print_comment "New directory added: $file_path" + # Recursively show all files in the new directory + find "$full_path" -type f | sort | while read -r new_file; do + rel_file=$(to_relative_path "$new_file" "$targetDirA") + print_comment " New file: $rel_file" + dyff between /dev/null "$new_file" --omit-header \ + --exclude "data.tls.key" --exclude "/data/ca.crt" --exclude "/data/tls.crt" --exclude "/data/tls.key" \ + --exclude-regexp "/checksum" --exclude-regexp "/webhooks.*" --ignore-order-changes "${miscArgs[@]}" || true + done + else + print_comment "New file added: $file_path" + # Show the contents of the new file + dyff between /dev/null "$full_path" --omit-header \ + --exclude "data.tls.key" --exclude "/data/ca.crt" --exclude "/data/tls.crt" --exclude "/data/tls.key" \ + --exclude-regexp "/checksum" --exclude-regexp "/webhooks.*" --ignore-order-changes "${miscArgs[@]}" || true + fi elif [[ "$only_in_dir" == "$targetDirB"* ]]; then - print_comment "Old file deleted: $(join_relative_path "$(to_relative_path "$only_in_dir" "$targetDirB")" "$only_in_file")" + file_path=$(join_relative_path "$(to_relative_path "$only_in_dir" "$targetDirB")" "$only_in_file") + if [[ -d "$full_path" ]]; then + print_comment "Old directory deleted: $file_path" + # Recursively show all files in the deleted directory + find "$full_path" -type f | sort | while read -r old_file; do + rel_file=$(to_relative_path "$old_file" "$targetDirB") + print_comment " Old file: $rel_file" + dyff between "$old_file" /dev/null --omit-header \ + --exclude "data.tls.key" --exclude "/data/ca.crt" --exclude "/data/tls.crt" --exclude "/data/tls.key" \ + --exclude-regexp "/checksum" --exclude-regexp "/webhooks.*" --ignore-order-changes "${miscArgs[@]}" || true + done + else + print_comment "Old file deleted: $file_path" + # Show the contents of the deleted file + dyff between "$full_path" /dev/null --omit-header \ + --exclude "data.tls.key" --exclude "/data/ca.crt" --exclude "/data/tls.crt" --exclude "/data/tls.key" \ + --exclude-regexp "/checksum" --exclude-regexp "/webhooks.*" --ignore-order-changes "${miscArgs[@]}" || true + fi else print_comment "$line" fi From c1f56a691bdebe562d8451ced3774fc4aeb663ca Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:53:23 +0200 Subject: [PATCH 05/21] ci: add app ingress skill --- .../SKILL.md | 20 +- .../scripts/add-helm-chart.sh | 2 +- .github/skills/add-app-ingress/SKILL.md | 41 +++ .../scripts/configure-public-app-exposure.sh | 7 + ci/src/add-helm-chart.test.ts | 4 +- ci/src/add-helm-chart.ts | 204 +---------- ci/src/configure-public-exposure.ts | 327 ++++++++++++++++++ 7 files changed, 392 insertions(+), 213 deletions(-) rename .github/skills/{add-helm-chart => add-app-helm-chart}/SKILL.md (75%) rename .github/skills/{add-helm-chart => add-app-helm-chart}/scripts/add-helm-chart.sh (72%) create mode 100644 .github/skills/add-app-ingress/SKILL.md create mode 100755 .github/skills/add-app-ingress/scripts/configure-public-app-exposure.sh create mode 100644 ci/src/configure-public-exposure.ts diff --git a/.github/skills/add-helm-chart/SKILL.md b/.github/skills/add-app-helm-chart/SKILL.md similarity index 75% rename from .github/skills/add-helm-chart/SKILL.md rename to .github/skills/add-app-helm-chart/SKILL.md index 8b4caad305..1b41a26f10 100644 --- a/.github/skills/add-helm-chart/SKILL.md +++ b/.github/skills/add-app-helm-chart/SKILL.md @@ -1,5 +1,5 @@ --- -name: add-helm-chart +name: add-app-helm-chart description: Add and wire a new Helm chart into APL Core by updating chart index, vendoring the chart, scaffolding Helmfile/defaults/values files, and running required validation checks. Use when adding a new app chart and the user provides or asks for name and repository; the script resolves the latest official stable semver chart version automatically. argument-hint: --- @@ -9,20 +9,15 @@ argument-hint: ## Quick start 1. Ask for required inputs: chart name and chart repository. -2. Ask if the app must be publicly exposed. -3. If public is needed, offer security scaffolding for HTTPRoute + Istio RequestAuthentication + AuthorizationPolicy. -4. Run: +2. Run: ```bash -npx tsx ci/src/add-helm-chart.ts +npx tsx ci/src/add-app-helm-chart.ts ``` 3. Validate: -```bash -npm run validate-values -bin/compare.sh -``` +Use `/test-gotemplate-renders` skill to validate Go template rendering. ## Workflow @@ -31,17 +26,16 @@ Checklist: - [ ] Collect required parameters: name and repository. - [ ] Resolve latest official stable semver chart version automatically. - [ ] Keep chart name, app key, release name, and namespace identical. -- [ ] Ask if the app is public and whether to scaffold HTTPRoute/authentication/authorization resources. - [ ] Add dependency entry in chart/chart-index/Chart.yaml. - [ ] Vendor chart into charts//. - [ ] Create a new Helmfile file with the next highest numeric prefix in helmfile.d. - [ ] Add Helmfile release stanza in that new file using \*default anchor. - [ ] Always add namespace entry in core.yaml. -- [ ] If public, ensure sidecar injection is enabled for that namespace. - [ ] Add empty defaults entry in helmfile.d/snippets/defaults.yaml in alphabetical order. - [ ] Create values//.gotmpl. - [ ] Add tests fixture app file in tests/fixtures/env/apps/.yaml. - [ ] Add corresponding app entries in tests/integration/\*.yaml. +- [ ] If public exposure is needed, run the configure-public-app-exposure skill afterward. - [ ] Run schema and template-diff checks. - [ ] Summarize generated diffs and touched files. @@ -52,6 +46,6 @@ Checklist: - New Chart.yaml dependency entry is inserted in alphabetical order by dependency name. - If chart source is OCI, repository should be oci://... (script pulls from /). - Chart version is auto-selected as latest stable semver; pre-release versions are ignored. -- Main implementation is in ci/src/add-helm-chart.ts. -- .github/skills/add-helm-chart/scripts/add-helm-chart.sh is a compatibility wrapper. +- Main implementation is in ci/src/add-app-helm-chart.ts. +- .github/skills/add-app-helm-chart/scripts/add-app-helm-chart.sh is a compatibility wrapper. - Script confirms rendering path by running bin/compare.sh. diff --git a/.github/skills/add-helm-chart/scripts/add-helm-chart.sh b/.github/skills/add-app-helm-chart/scripts/add-helm-chart.sh similarity index 72% rename from .github/skills/add-helm-chart/scripts/add-helm-chart.sh rename to .github/skills/add-app-helm-chart/scripts/add-helm-chart.sh index 70a9bc1064..e0be59dcd9 100755 --- a/.github/skills/add-helm-chart/scripts/add-helm-chart.sh +++ b/.github/skills/add-app-helm-chart/scripts/add-helm-chart.sh @@ -4,4 +4,4 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)" cd "$repo_root" -exec npx tsx ci/src/add-helm-chart.ts "$@" +exec npx tsx ci/src/add-app-helm-chart.ts "$@" diff --git a/.github/skills/add-app-ingress/SKILL.md b/.github/skills/add-app-ingress/SKILL.md new file mode 100644 index 0000000000..0dc6db7375 --- /dev/null +++ b/.github/skills/add-app-ingress/SKILL.md @@ -0,0 +1,41 @@ +--- +name: add-app-ingress +description: Configure public exposure for a selected app by updating core namespace/admin app configuration and optionally scaffolding HTTPRoute + Istio authn/authz resources. +argument-hint: [--configure-public-security true|false] +--- + +# Configure Public App Exposure + +## Quick start + +1. Ask for required input: app name. +2. Ask if HTTPRoute + Istio authn/authz scaffolding should be created. +3. Run: + +```bash +npx tsx ci/src/configure-public-exposure.ts [--configure-public-security true|false] +``` + +4. Validate: + +Use `/test-gotemplate-renders` skill to validate Go template rendering. + +## Workflow + +Checklist: + +- [ ] Confirm selected app name. +- [ ] Confirm whether to scaffold public-security resources. +- [ ] Ensure namespace is configured for public exposure in core.yaml. +- [ ] Ensure adminApps entry exists in core.yaml. +- [ ] If security scaffolding is enabled, add `-artifacts` release in the app Helmfile. +- [ ] If security scaffolding is enabled, create values//-raw.gotmpl. +- [ ] Run validation and template-diff checks. +- [ ] Summarize touched files and rendered diffs. + +## Notes + +- The script expects an existing Helmfile release named `` when `--configure-public-security` is true. +- The script is idempotent for existing entries and files. +- Main implementation is in ci/src/configure-public-exposure.ts. +- .github/skills/add-app-ingress/scripts/add-app-ingress.sh is a compatibility wrapper. diff --git a/.github/skills/add-app-ingress/scripts/configure-public-app-exposure.sh b/.github/skills/add-app-ingress/scripts/configure-public-app-exposure.sh new file mode 100755 index 0000000000..ad125e2b5e --- /dev/null +++ b/.github/skills/add-app-ingress/scripts/configure-public-app-exposure.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)" +cd "$repo_root" + +exec npx tsx ci/src/configure-public-exposure.ts "$@" diff --git a/ci/src/add-helm-chart.test.ts b/ci/src/add-helm-chart.test.ts index deb3f664c6..541f89bea6 100644 --- a/ci/src/add-helm-chart.test.ts +++ b/ci/src/add-helm-chart.test.ts @@ -1,6 +1,6 @@ -import { parseStableSemver, resolveLatestOfficialSemver, type RuntimeDeps } from './add-helm-chart' +import { parseStableSemver, resolveLatestOfficialSemver, type RuntimeDeps } from './add-app-helm-chart' -describe('add-helm-chart semver resolution', () => { +describe('add-app-helm-chart semver resolution', () => { const makeDeps = (runCommand: RuntimeDeps['runCommand']): RuntimeDeps => ({ runCommand, pid: 4242, diff --git a/ci/src/add-helm-chart.ts b/ci/src/add-helm-chart.ts index de081fb378..6641d03c4a 100644 --- a/ci/src/add-helm-chart.ts +++ b/ci/src/add-helm-chart.ts @@ -2,15 +2,11 @@ import type { ExecSyncOptions } from 'node:child_process' import { execSync } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' -import readline from 'node:readline/promises' import semver from 'semver' type Opts = { name: string repository: string - public: boolean - configurePublicSecurity: boolean - runCompare: boolean } export type CommandRunner = (command: string, options?: ExecSyncOptions) => string @@ -34,79 +30,19 @@ const defaultDeps: RuntimeDeps = { } function usage(): never { - console.error( - 'Usage: npx tsx ci/src/add-helm-chart.ts [--public true|false] [--configure-public-security true|false] [--run-compare true|false]', - ) + console.error('Usage: npx tsx ci/src/add-app-helm-chart.ts ') process.exit(2) } -function parseBool(value: string, flag: string): boolean { - if (value === 'true') return true - if (value === 'false') return false - throw new Error(`Invalid value for ${flag}: ${value}. Expected true|false.`) -} - async function parseArgs(argv: string[]): Promise { - if (argv.length < 2) usage() + if (argv.length !== 2) usage() const name = argv[0] const repository = argv[1] - let isPublic: boolean | undefined - let configurePublicSecurity: boolean | undefined - let runCompare = true - - for (let i = 2; i < argv.length; i += 1) { - const arg = argv[i] - if (arg === '--public') { - const value = argv[i + 1] - if (!value) usage() - isPublic = parseBool(value, '--public') - i += 1 - continue - } - if (arg === '--configure-public-security') { - const value = argv[i + 1] - if (!value) usage() - configurePublicSecurity = parseBool(value, '--configure-public-security') - i += 1 - continue - } - if (arg === '--run-compare') { - const value = argv[i + 1] - if (!value) usage() - runCompare = parseBool(value, '--run-compare') - i += 1 - continue - } - throw new Error(`Unknown argument: ${arg}`) - } - - if (isPublic === undefined && process.stdin.isTTY) { - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) - const ans = await rl.question('Expose app publicly? (true/false) [false]: ') - rl.close() - isPublic = ans.trim() === '' ? false : parseBool(ans.trim(), '--public') - } - if (isPublic === undefined) isPublic = false - - if (configurePublicSecurity === undefined) { - if (isPublic && process.stdin.isTTY) { - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) - const ans = await rl.question('Scaffold HTTPRoute + Istio authn/authz resources? (true/false) [true]: ') - rl.close() - configurePublicSecurity = ans.trim() === '' ? true : parseBool(ans.trim(), '--configure-public-security') - } else { - configurePublicSecurity = false - } - } - return { name, repository, - public: isPublic, - configurePublicSecurity, - runCompare, } } @@ -283,7 +219,7 @@ function vendorChart(root: string, name: string, version: string, repository: st console.log(`Vendored chart into charts/${name}`) } -function createHelmfile(root: string, name: string, isPublic: boolean, configurePublicSecurity: boolean): string { +function createHelmfile(root: string, name: string): string { const next = findHighestHelmfileNumber(path.join(root, 'helmfile.d')) + 1 const padded = String(next).padStart(2, '0') const relPath = `helmfile.d/helmfile-${padded}.${name}.yaml.gotmpl` @@ -306,18 +242,6 @@ function createHelmfile(root: string, name: string, isPublic: boolean, configure 'releases:', ] - if (isPublic && configurePublicSecurity) { - parts.push( - ` - name: ${name}-artifacts`, - ` installed: {{ $a | get "${name}.enabled" }}`, - ` namespace: ${name}`, - ' labels:', - ` pkg: ${name}`, - ' app: core', - ' <<: *raw', - ) - } - parts.push( ` - name: ${name}`, ` installed: {{ $a | get "${name}.enabled" }}`, @@ -334,7 +258,7 @@ function createHelmfile(root: string, name: string, isPublic: boolean, configure return relPath } -function ensureCoreNamespace(corePath: string, name: string, isPublic: boolean): void { +function ensureCoreNamespace(corePath: string, name: string): void { const lines = readLines(corePath) if (lines.some((line) => line === ` - name: ${name}`)) { console.log(`core.yaml already contains namespace ${name}`) @@ -348,49 +272,13 @@ function ensureCoreNamespace(corePath: string, name: string, isPublic: boolean): (line) => line === 'adminApps:', (m) => m[1], name, - () => { - const block = [` - name: ${name}`, ` app: ${name}`] - if (!isPublic) block.push(' disableIstioInjection: true') - return block - }, + () => [` - name: ${name}`, ` app: ${name}`, ' disableIstioInjection: true'], ) writeLines(corePath, next) console.log(`Added namespace ${name} to core.yaml`) } -function ensureAdminApp(corePath: string, name: string): void { - const lines = readLines(corePath) - - let inAdmin = false - for (const line of lines) { - if (line === 'adminApps:') { - inAdmin = true - continue - } - if (line === 'teamApps:') { - inAdmin = false - } - if (inAdmin && line === ` - name: ${name}`) { - console.log(`core.yaml already contains adminApps entry for ${name}`) - return - } - } - - const next = insertSortedBlock( - lines, - (line) => line === 'adminApps:', - (line) => line.match(/^ - name: (.+)$/), - (line) => line === 'teamApps:', - (m) => m[1], - name, - () => [` - name: ${name}`, ' tags: [custom]', ' ownHost: true'], - ) - - writeLines(corePath, next) - console.log(`Added adminApps public entry for ${name} in core.yaml`) -} - function ensureDefaultsEntry(defaultsPath: string, name: string): void { const lines = readLines(defaultsPath) if (lines.some((line) => line === ` ${name}:`)) { @@ -422,72 +310,6 @@ function ensureValuesFile(valuesFile: string, name: string): void { console.log(`Created values/${name}/${name}.gotmpl`) } -function ensureRawValuesFile(rawValuesFile: string, name: string): void { - if (fs.existsSync(rawValuesFile)) { - console.log(`raw values file already exists: values/${name}/${name}-raw.gotmpl`) - return - } - - const content = `{{- $v := .Values }} -{{- $httpRoute := tpl (readFile "../../helmfile.d/snippets/routes.gotmpl") $v | fromYaml }} -resources: - - apiVersion: gateway.networking.k8s.io/v1 - kind: HTTPRoute - metadata: - name: ${name} - {{ with $httpRoute.annotations }} - annotations: - {{ . | toYaml | nindent 8 }} - {{- end }} - spec: - parentRefs: - {{- $httpRoute.parentRefs | toYaml | nindent 8 }} - hostnames: - - {{ printf "${name}.%s" $v.cluster.domainSuffix }} - rules: - {{- $httpRoute.authRules | toYaml | nindent 8 }} - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - kind: Service - name: ${name} - port: 80 - - apiVersion: security.istio.io/v1 - kind: RequestAuthentication - metadata: - name: ${name}-auth - namespace: ${name} - spec: - selector: - matchLabels: - app.kubernetes.io/name: ${name} - jwtRules: - - issuer: {{ $v._derived.oidcBaseUrl }} - jwksUri: {{ $v._derived.oidcBaseUrlBackchannel }}/protocol/openid-connect/certs - fromHeaders: - - name: Authorization - prefix: "Bearer " - - apiVersion: security.istio.io/v1 - kind: AuthorizationPolicy - metadata: - name: ${name}-authz - namespace: ${name} - spec: - action: ALLOW - selector: - matchLabels: - app.kubernetes.io/name: ${name} - rules: - - from: - - source: - requestPrincipals: ["*"] -` - - fs.writeFileSync(rawValuesFile, content, 'utf8') - console.log(`Created values/${name}/${name}-raw.gotmpl for public route + auth scaffolding`) -} function ensureFixtureAppFile(fixturePath: string, name: string): void { if (fs.existsSync(fixturePath)) { @@ -532,14 +354,6 @@ function ensureIntegrationEntries(integrationDir: string, name: string): void { } } -function runCompareIfNeeded(root: string, runCompare: boolean, deps: RuntimeDeps = defaultDeps): void { - if (!runCompare) return - const env = { ...process.env } - if (!env.ENV_DIR) env.ENV_DIR = path.join(root, 'tests/fixtures') - console.log('Running bin/compare.sh to confirm rendering with new files...') - deps.runCommand(path.join(root, 'bin/compare.sh'), { stdio: 'inherit', env }) -} - export async function main(deps: RuntimeDeps = defaultDeps): Promise { const opts = await parseArgs(process.argv.slice(2)) @@ -549,7 +363,6 @@ export async function main(deps: RuntimeDeps = defaultDeps): Promise { const coreFile = path.join(root, 'core.yaml') const helmfileDir = path.join(root, 'helmfile.d') const valuesFile = path.join(root, 'values', opts.name, `${opts.name}.gotmpl`) - const rawValuesFile = path.join(root, 'values', opts.name, `${opts.name}-raw.gotmpl`) const fixtureAppFile = path.join(root, 'tests/fixtures/env/apps', `${opts.name}.yaml`) const integrationDir = path.join(root, 'tests/integration') @@ -566,15 +379,12 @@ export async function main(deps: RuntimeDeps = defaultDeps): Promise { ensureChartDependency(chartIndex, opts.name, resolvedVersion, opts.repository) vendorChart(root, opts.name, resolvedVersion, opts.repository, deps) - createHelmfile(root, opts.name, opts.public, opts.configurePublicSecurity) - ensureCoreNamespace(coreFile, opts.name, opts.public) - if (opts.public) ensureAdminApp(coreFile, opts.name) + createHelmfile(root, opts.name) + ensureCoreNamespace(coreFile, opts.name) ensureDefaultsEntry(defaultsFile, opts.name) ensureValuesFile(valuesFile, opts.name) - if (opts.public && opts.configurePublicSecurity) ensureRawValuesFile(rawValuesFile, opts.name) ensureFixtureAppFile(fixtureAppFile, opts.name) ensureIntegrationEntries(integrationDir, opts.name) - runCompareIfNeeded(root, opts.runCompare, deps) deps.log('Done. Next: npm run validate-values') } diff --git a/ci/src/configure-public-exposure.ts b/ci/src/configure-public-exposure.ts new file mode 100644 index 0000000000..5ce37ac5d1 --- /dev/null +++ b/ci/src/configure-public-exposure.ts @@ -0,0 +1,327 @@ +import fs from 'node:fs' +import path from 'node:path' +import readline from 'node:readline/promises' + +type Opts = { + name: string + configurePublicSecurity: boolean +} + +function usage(): never { + console.error('Usage: npx tsx ci/src/configure-public-exposure.ts [--configure-public-security true|false]') + process.exit(2) +} + +function parseBool(value: string, flag: string): boolean { + if (value === 'true') return true + if (value === 'false') return false + throw new Error(`Invalid value for ${flag}: ${value}. Expected true|false.`) +} + +async function parseArgs(argv: string[]): Promise { + if (argv.length < 1) usage() + + const name = argv[0] + let configurePublicSecurity: boolean | undefined + + for (let i = 1; i < argv.length; i += 1) { + const arg = argv[i] + if (arg === '--configure-public-security') { + const value = argv[i + 1] + if (!value) usage() + configurePublicSecurity = parseBool(value, '--configure-public-security') + i += 1 + continue + } + throw new Error(`Unknown argument: ${arg}`) + } + + if (configurePublicSecurity === undefined && process.stdin.isTTY) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) + const ans = await rl.question('Scaffold HTTPRoute + Istio authn/authz resources? (true/false) [true]: ') + rl.close() + configurePublicSecurity = ans.trim() === '' ? true : parseBool(ans.trim(), '--configure-public-security') + } + if (configurePublicSecurity === undefined) configurePublicSecurity = true + + return { + name, + configurePublicSecurity, + } +} + +function readLines(filePath: string): string[] { + return fs.readFileSync(filePath, 'utf8').split(/\r?\n/) +} + +function writeLines(filePath: string, lines: string[]): void { + fs.writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf8') +} + +function insertSortedBlock( + lines: string[], + startMatcher: (line: string) => boolean, + itemMatcher: (line: string) => RegExpMatchArray | null, + sectionEndMatcher: (line: string) => boolean, + getKey: (m: RegExpMatchArray) => string, + newKey: string, + emitBlock: () => string[], +): string[] { + const out: string[] = [] + let inSection = false + let inserted = false + + for (const line of lines) { + if (!inSection && startMatcher(line)) { + out.push(line) + inSection = true + continue + } + + if (inSection && !inserted) { + const item = itemMatcher(line) + if (item) { + const key = getKey(item) + if (key > newKey) { + out.push(...emitBlock()) + inserted = true + } + } else if (sectionEndMatcher(line)) { + out.push(...emitBlock()) + inserted = true + inSection = false + } + } + + out.push(line) + } + + if (inSection && !inserted) { + out.push(...emitBlock()) + inserted = true + } + + if (!inserted) { + throw new Error('Failed to insert block in target section') + } + + return out +} + +function findHelmfileForRelease(helmfileDir: string, releaseName: string): string { + const files = fs.readdirSync(helmfileDir).filter((f) => f.endsWith('.yaml') || f.endsWith('.gotmpl')) + const rx = new RegExp(`^\\s*-\\s+name:\\s+${releaseName}\\s*$`, 'm') + const matches = files.filter((file) => rx.test(fs.readFileSync(path.join(helmfileDir, file), 'utf8'))) + + if (matches.length === 0) { + throw new Error(`Could not find a Helmfile release named ${releaseName} in helmfile.d`) + } + if (matches.length > 1) { + throw new Error(`Found multiple Helmfile files with release ${releaseName}: ${matches.join(', ')}`) + } + + return path.join(helmfileDir, matches[0]) +} + +function ensureArtifactsRelease(helmfilePath: string, name: string): void { + const lines = readLines(helmfilePath) + if (lines.some((line) => line.trim() === `- name: ${name}-artifacts`)) { + console.log(`Helmfile already contains ${name}-artifacts release`) + return + } + + const releaseIdx = lines.findIndex((line) => line === ` - name: ${name}`) + if (releaseIdx < 0) { + throw new Error(`Could not locate release ${name} in ${path.basename(helmfilePath)}`) + } + + const block = [ + ` - name: ${name}-artifacts`, + ` installed: {{ $a | get "${name}.enabled" }}`, + ` namespace: ${name}`, + ' labels:', + ` pkg: ${name}`, + ' app: core', + ' <<: *raw', + ] + + lines.splice(releaseIdx, 0, ...block) + writeLines(helmfilePath, lines) + console.log(`Added ${name}-artifacts release in ${path.basename(helmfilePath)}`) +} + +function ensureCoreNamespacePublic(corePath: string, name: string): void { + const lines = readLines(corePath) + const namespaceStart = lines.findIndex((line) => line === ` - name: ${name}`) + + if (namespaceStart < 0) { + const next = insertSortedBlock( + lines, + (line) => line.trim() === 'namespaces:', + (line) => line.match(/^ - name: (.+)$/), + (line) => line === 'adminApps:', + (m) => m[1], + name, + () => [` - name: ${name}`, ` app: ${name}`], + ) + writeLines(corePath, next) + console.log(`Added public namespace ${name} to core.yaml`) + return + } + + const sectionEnd = (() => { + for (let i = namespaceStart + 1; i < lines.length; i += 1) { + if (lines[i].startsWith(' - name: ') || lines[i] === 'adminApps:') { + return i + } + } + return lines.length + })() + + const block = lines.slice(namespaceStart, sectionEnd) + const hadDisableIstio = block.includes(' disableIstioInjection: true') + const hasApp = block.includes(` app: ${name}`) + + const normalizedBlock = block.filter((line) => line !== ' disableIstioInjection: true') + if (!hasApp) normalizedBlock.splice(1, 0, ` app: ${name}`) + + if (!hadDisableIstio && hasApp) { + console.log(`Namespace ${name} already configured for public exposure`) + return + } + + lines.splice(namespaceStart, sectionEnd - namespaceStart, ...normalizedBlock) + writeLines(corePath, lines) + console.log(`Updated namespace ${name} for public exposure in core.yaml`) +} + +function ensureAdminApp(corePath: string, name: string): void { + const lines = readLines(corePath) + + let inAdmin = false + for (const line of lines) { + if (line === 'adminApps:') { + inAdmin = true + continue + } + if (line === 'teamApps:') { + inAdmin = false + } + if (inAdmin && line === ` - name: ${name}`) { + console.log(`core.yaml already contains adminApps entry for ${name}`) + return + } + } + + const next = insertSortedBlock( + lines, + (line) => line === 'adminApps:', + (line) => line.match(/^ - name: (.+)$/), + (line) => line === 'teamApps:', + (m) => m[1], + name, + () => [` - name: ${name}`, ' tags: [custom]', ' ownHost: true'], + ) + + writeLines(corePath, next) + console.log(`Added adminApps public entry for ${name} in core.yaml`) +} + +function ensureRawValuesFile(rawValuesFile: string, name: string): void { + fs.mkdirSync(path.dirname(rawValuesFile), { recursive: true }) + if (fs.existsSync(rawValuesFile)) { + console.log(`raw values file already exists: values/${name}/${name}-raw.gotmpl`) + return + } + + const content = `{{- $v := .Values }} +{{- $httpRoute := tpl (readFile "../../helmfile.d/snippets/routes.gotmpl") $v | fromYaml }} +resources: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: ${name} + {{ with $httpRoute.annotations }} + annotations: + {{ . | toYaml | nindent 8 }} + {{- end }} + spec: + parentRefs: + {{- $httpRoute.parentRefs | toYaml | nindent 8 }} + hostnames: + - {{ printf "${name}.%s" $v.cluster.domainSuffix }} + rules: + {{- $httpRoute.authRules | toYaml | nindent 8 }} + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - kind: Service + name: ${name} + port: 80 + - apiVersion: security.istio.io/v1 + kind: RequestAuthentication + metadata: + name: ${name}-auth + namespace: ${name} + spec: + selector: + matchLabels: + app.kubernetes.io/name: ${name} + jwtRules: + - issuer: {{ $v._derived.oidcBaseUrl }} + jwksUri: {{ $v._derived.oidcBaseUrlBackchannel }}/protocol/openid-connect/certs + fromHeaders: + - name: Authorization + prefix: "Bearer " + - apiVersion: security.istio.io/v1 + kind: AuthorizationPolicy + metadata: + name: ${name}-authz + namespace: ${name} + spec: + action: ALLOW + selector: + matchLabels: + app.kubernetes.io/name: ${name} + rules: + - from: + - source: + requestPrincipals: ["*"] +` + + fs.writeFileSync(rawValuesFile, content, 'utf8') + console.log(`Created values/${name}/${name}-raw.gotmpl for public route + auth scaffolding`) +} + +export async function main(): Promise { + const opts = await parseArgs(process.argv.slice(2)) + + const root = path.resolve(__dirname, '..', '..') + const coreFile = path.join(root, 'core.yaml') + const helmfileDir = path.join(root, 'helmfile.d') + const rawValuesFile = path.join(root, 'values', opts.name, `${opts.name}-raw.gotmpl`) + + if (!fs.existsSync(coreFile)) throw new Error(`Missing required file: ${coreFile}`) + if (!fs.existsSync(helmfileDir)) throw new Error(`Missing required directory: ${helmfileDir}`) + + ensureCoreNamespacePublic(coreFile, opts.name) + ensureAdminApp(coreFile, opts.name) + + if (opts.configurePublicSecurity) { + const helmfilePath = findHelmfileForRelease(helmfileDir, opts.name) + ensureArtifactsRelease(helmfilePath, opts.name) + ensureRawValuesFile(rawValuesFile, opts.name) + } + + console.log('Done. Next: npm run validate-values') +} + +if (require.main === module) { + main().catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + console.error(message) + process.exit(1) + }) +} From 5e7b3b2df491df99b8204faad0cc148c1fd5b268 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:01:41 +0200 Subject: [PATCH 06/21] ci: add app ingress skill --- .github/skills/add-app-ingress/SKILL.md | 7 +- ci/src/configure-public-exposure.test.ts | 105 +++++++++++++++++++++++ ci/src/configure-public-exposure.ts | 76 +++++++++++++++- 3 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 ci/src/configure-public-exposure.test.ts diff --git a/.github/skills/add-app-ingress/SKILL.md b/.github/skills/add-app-ingress/SKILL.md index 0dc6db7375..658ed1bbb0 100644 --- a/.github/skills/add-app-ingress/SKILL.md +++ b/.github/skills/add-app-ingress/SKILL.md @@ -1,6 +1,6 @@ --- name: add-app-ingress -description: Configure public exposure for a selected app by updating core namespace/admin app configuration and optionally scaffolding HTTPRoute + Istio authn/authz resources. +description: Configure public exposure for a selected app by updating core namespace/admin app configuration and optionally scaffolding HTTPRoute auth-redirects plus authentication/authorization resources. argument-hint: [--configure-public-security true|false] --- @@ -9,7 +9,7 @@ argument-hint: [--configure-public-security true|false] ## Quick start 1. Ask for required input: app name. -2. Ask if HTTPRoute + Istio authn/authz scaffolding should be created. +2. Ask if HTTPRoute auth-redirects + authentication/authorization scaffolding should be created. 3. Run: ```bash @@ -29,7 +29,8 @@ Checklist: - [ ] Ensure namespace is configured for public exposure in core.yaml. - [ ] Ensure adminApps entry exists in core.yaml. - [ ] If security scaffolding is enabled, add `-artifacts` release in the app Helmfile. -- [ ] If security scaffolding is enabled, create values//-raw.gotmpl. +- [ ] If security scaffolding is enabled, create values//-raw.gotmpl with an HTTPRoute named `-auth-redirects`. +- [ ] If security scaffolding is enabled, ensure values//.gotmpl defines podLabels with `otomi.io/auth: platform` and `otomi.io/auth-policy: platform`. - [ ] Run validation and template-diff checks. - [ ] Summarize touched files and rendered diffs. diff --git a/ci/src/configure-public-exposure.test.ts b/ci/src/configure-public-exposure.test.ts new file mode 100644 index 0000000000..4f38265330 --- /dev/null +++ b/ci/src/configure-public-exposure.test.ts @@ -0,0 +1,105 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { ensurePodAuthLabels, ensureRawValuesFile, parseBool } from './configure-public-exposure' + +describe('configure-public-exposure helpers', () => { + let tmpDir: string + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'configure-public-exposure-')) + }) + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }) + }) + + it('parseBool accepts true/false and rejects other values', () => { + expect(parseBool('true', '--flag')).toBe(true) + expect(parseBool('false', '--flag')).toBe(false) + expect(() => parseBool('yes', '--flag')).toThrow('Invalid value for --flag: yes. Expected true|false.') + }) + + it('ensureRawValuesFile scaffolds auth-redirect route and AuthorizationPolicy', () => { + const rawValuesFile = path.join(tmpDir, 'values', 'demo', 'demo-raw.gotmpl') + + ensureRawValuesFile(rawValuesFile, 'demo') + + const content = fs.readFileSync(rawValuesFile, 'utf8') + expect(content).toContain('name: demo-auth-redirects') + expect(content).toContain('kind: HTTPRoute') + expect(content).toContain('$httpRoute.authRules') + expect(content).toContain('kind: AuthorizationPolicy') + expect(content).toContain('name: demo-authz') + }) + + it('ensurePodAuthLabels replaces empty object values with required labels', () => { + const valuesFile = path.join(tmpDir, 'values', 'demo', 'demo.gotmpl') + fs.mkdirSync(path.dirname(valuesFile), { recursive: true }) + fs.writeFileSync(valuesFile, '{}\n', 'utf8') + + ensurePodAuthLabels(valuesFile, 'demo') + + const content = fs.readFileSync(valuesFile, 'utf8') + expect(content).toContain('podLabels:') + expect(content).toContain('otomi.io/auth: platform') + expect(content).toContain('otomi.io/auth-policy: platform') + }) + + it('ensurePodAuthLabels inserts missing keys in existing podLabels block', () => { + const valuesFile = path.join(tmpDir, 'values', 'demo', 'demo.gotmpl') + fs.mkdirSync(path.dirname(valuesFile), { recursive: true }) + fs.writeFileSync( + valuesFile, + [ + 'replicaCount: 1', + 'podLabels:', + ' app.kubernetes.io/name: demo', + 'service:', + ' type: ClusterIP', + '', + ].join('\n'), + 'utf8', + ) + + ensurePodAuthLabels(valuesFile, 'demo') + + const content = fs.readFileSync(valuesFile, 'utf8') + expect(content).toContain(' app.kubernetes.io/name: demo') + expect(content).toContain(' otomi.io/auth: platform') + expect(content).toContain(' otomi.io/auth-policy: platform') + expect(content).toContain('service:\n type: ClusterIP') + }) + + it('ensurePodAuthLabels appends podLabels block when absent', () => { + const valuesFile = path.join(tmpDir, 'values', 'demo', 'demo.gotmpl') + fs.mkdirSync(path.dirname(valuesFile), { recursive: true }) + fs.writeFileSync(valuesFile, 'replicaCount: 1\n', 'utf8') + + ensurePodAuthLabels(valuesFile, 'demo') + + const content = fs.readFileSync(valuesFile, 'utf8') + expect(content).toContain('replicaCount: 1') + expect(content).toContain('podLabels:') + expect(content).toContain('otomi.io/auth: platform') + expect(content).toContain('otomi.io/auth-policy: platform') + }) + + it('ensurePodAuthLabels keeps file unchanged when both labels already exist', () => { + const valuesFile = path.join(tmpDir, 'values', 'demo', 'demo.gotmpl') + fs.mkdirSync(path.dirname(valuesFile), { recursive: true }) + const initial = [ + 'podLabels:', + ' otomi.io/auth: platform', + ' otomi.io/auth-policy: platform', + '', + ].join('\n') + fs.writeFileSync(valuesFile, initial, 'utf8') + + ensurePodAuthLabels(valuesFile, 'demo') + + const content = fs.readFileSync(valuesFile, 'utf8') + expect(content).toBe(initial) + }) +}) diff --git a/ci/src/configure-public-exposure.ts b/ci/src/configure-public-exposure.ts index 5ce37ac5d1..b0370c5c6a 100644 --- a/ci/src/configure-public-exposure.ts +++ b/ci/src/configure-public-exposure.ts @@ -12,7 +12,7 @@ function usage(): never { process.exit(2) } -function parseBool(value: string, flag: string): boolean { +export function parseBool(value: string, flag: string): boolean { if (value === 'true') return true if (value === 'false') return false throw new Error(`Invalid value for ${flag}: ${value}. Expected true|false.`) @@ -227,7 +227,7 @@ function ensureAdminApp(corePath: string, name: string): void { console.log(`Added adminApps public entry for ${name} in core.yaml`) } -function ensureRawValuesFile(rawValuesFile: string, name: string): void { +export function ensureRawValuesFile(rawValuesFile: string, name: string): void { fs.mkdirSync(path.dirname(rawValuesFile), { recursive: true }) if (fs.existsSync(rawValuesFile)) { console.log(`raw values file already exists: values/${name}/${name}-raw.gotmpl`) @@ -240,7 +240,7 @@ resources: - apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: - name: ${name} + name: ${name}-auth-redirects {{ with $httpRoute.annotations }} annotations: {{ . | toYaml | nindent 8 }} @@ -252,6 +252,20 @@ resources: - {{ printf "${name}.%s" $v.cluster.domainSuffix }} rules: {{- $httpRoute.authRules | toYaml | nindent 8 }} + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: ${name} + {{ with $httpRoute.annotations }} + annotations: + {{ . | toYaml | nindent 8 }} + {{- end }} + spec: + parentRefs: + {{- $httpRoute.parentRefs | toYaml | nindent 8 }} + hostnames: + - {{ printf "${name}.%s" $v.cluster.domainSuffix }} + rules: - matches: - path: type: PathPrefix @@ -295,6 +309,60 @@ resources: console.log(`Created values/${name}/${name}-raw.gotmpl for public route + auth scaffolding`) } +export function ensurePodAuthLabels(valuesFile: string, name: string): void { + if (!fs.existsSync(valuesFile)) { + throw new Error(`Missing values file for ${name}: ${valuesFile}`) + } + + const content = fs.readFileSync(valuesFile, 'utf8') + if (content.includes('otomi.io/auth:') && content.includes('otomi.io/auth-policy:')) { + console.log(`values/${name}/${name}.gotmpl already contains auth pod labels`) + return + } + + const labelsBlock = ['podLabels:', ' otomi.io/auth: platform', ' otomi.io/auth-policy: platform'] + const trimmed = content.trim() + + if (trimmed === '' || trimmed === '{}') { + fs.writeFileSync(valuesFile, `${labelsBlock.join('\n')}\n`, 'utf8') + console.log(`Set auth pod labels in values/${name}/${name}.gotmpl`) + return + } + + const lines = readLines(valuesFile) + const podLabelsIdx = lines.findIndex((line) => /^podLabels:\s*$/.test(line)) + if (podLabelsIdx >= 0) { + const insertAt = (() => { + for (let i = podLabelsIdx + 1; i < lines.length; i += 1) { + const line = lines[i] + if (line.trim() === '') continue + if (!line.startsWith(' ')) return i + } + return lines.length + })() + + const hasAuth = lines.slice(podLabelsIdx + 1, insertAt).some((line) => line.trim().startsWith('otomi.io/auth:')) + const hasAuthPolicy = lines.slice(podLabelsIdx + 1, insertAt).some((line) => line.trim().startsWith('otomi.io/auth-policy:')) + + const toInsert: string[] = [] + if (!hasAuth) toInsert.push(' otomi.io/auth: platform') + if (!hasAuthPolicy) toInsert.push(' otomi.io/auth-policy: platform') + + if (toInsert.length === 0) { + console.log(`values/${name}/${name}.gotmpl already contains auth pod labels`) + return + } + + lines.splice(insertAt, 0, ...toInsert) + writeLines(valuesFile, lines) + console.log(`Updated podLabels auth entries in values/${name}/${name}.gotmpl`) + return + } + + fs.writeFileSync(valuesFile, `${content.replace(/\s*$/, '')}\n\n${labelsBlock.join('\n')}\n`, 'utf8') + console.log(`Appended auth pod labels to values/${name}/${name}.gotmpl`) +} + export async function main(): Promise { const opts = await parseArgs(process.argv.slice(2)) @@ -302,6 +370,7 @@ export async function main(): Promise { const coreFile = path.join(root, 'core.yaml') const helmfileDir = path.join(root, 'helmfile.d') const rawValuesFile = path.join(root, 'values', opts.name, `${opts.name}-raw.gotmpl`) + const valuesFile = path.join(root, 'values', opts.name, `${opts.name}.gotmpl`) if (!fs.existsSync(coreFile)) throw new Error(`Missing required file: ${coreFile}`) if (!fs.existsSync(helmfileDir)) throw new Error(`Missing required directory: ${helmfileDir}`) @@ -313,6 +382,7 @@ export async function main(): Promise { const helmfilePath = findHelmfileForRelease(helmfileDir, opts.name) ensureArtifactsRelease(helmfilePath, opts.name) ensureRawValuesFile(rawValuesFile, opts.name) + ensurePodAuthLabels(valuesFile, opts.name) } console.log('Done. Next: npm run validate-values') From aac7a7acb79cb6d41c3a062135188b34d54df43c Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:08:02 +0200 Subject: [PATCH 07/21] ci: add skill to add new helm chart --- .vscode/settings.json | 4 ++++ ci/src/{add-helm-chart.test.ts => add-app-helm-chart.test.ts} | 0 ci/src/{add-helm-chart.ts => add-app-helm-chart.ts} | 0 3 files changed, 4 insertions(+) rename ci/src/{add-helm-chart.test.ts => add-app-helm-chart.test.ts} (100%) rename ci/src/{add-helm-chart.ts => add-app-helm-chart.ts} (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json index d3e5f9dad9..872b204618 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -60,5 +60,9 @@ }, "[dockerfile]": { "editor.defaultFormatter": "ms-azuretools.vscode-docker" + }, + "chat.tools.terminal.autoApprove": { + "npx tsx add-app-helm-chart.ts": true, + "npx tsx configure-public-exposure.ts": true } } diff --git a/ci/src/add-helm-chart.test.ts b/ci/src/add-app-helm-chart.test.ts similarity index 100% rename from ci/src/add-helm-chart.test.ts rename to ci/src/add-app-helm-chart.test.ts diff --git a/ci/src/add-helm-chart.ts b/ci/src/add-app-helm-chart.ts similarity index 100% rename from ci/src/add-helm-chart.ts rename to ci/src/add-app-helm-chart.ts From ab3fedda6e0a90d79dce7f276fed956d7846b2fc Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:13:27 +0200 Subject: [PATCH 08/21] ci: add skill to add new helm chart --- .vscode/settings.json | 8 ++++++-- helmfile.d/helmfile-92.headlamp.yaml.gotmpl | 21 +++++++++++++++++++++ helmfile.d/snippets/defaults.yaml | 4 ++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 helmfile.d/helmfile-92.headlamp.yaml.gotmpl diff --git a/.vscode/settings.json b/.vscode/settings.json index 872b204618..bb5e266d46 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -62,7 +62,11 @@ "editor.defaultFormatter": "ms-azuretools.vscode-docker" }, "chat.tools.terminal.autoApprove": { - "npx tsx add-app-helm-chart.ts": true, - "npx tsx configure-public-exposure.ts": true + "npx tsx ci/src/configure-public-exposure.ts headlamp": true, + "npx tsx ci/src/add-app-helm-chart.ts headlamp": true, + "export ENV_DIR=$PWD/tests/fixtures": true, + "bin/compare.sh": true, + "npm run validate-values": true, + "npm run validate-templates": true } } diff --git a/helmfile.d/helmfile-92.headlamp.yaml.gotmpl b/helmfile.d/helmfile-92.headlamp.yaml.gotmpl new file mode 100644 index 0000000000..0dfd14e4a1 --- /dev/null +++ b/helmfile.d/helmfile-92.headlamp.yaml.gotmpl @@ -0,0 +1,21 @@ +bases: + - snippets/defaults.yaml +--- +bases: + - snippets/env.gotmpl +--- +bases: + - snippets/derived.gotmpl +--- +{{ readFile "snippets/templates.gotmpl" }} +{{- $v := .Values }} +{{- $a := $v.apps }} + +releases: + - name: headlamp + installed: {{ $a | get "headlamp.enabled" }} + namespace: headlamp + labels: + pkg: headlamp + app: core + <<: *default diff --git a/helmfile.d/snippets/defaults.yaml b/helmfile.d/snippets/defaults.yaml index 0b378a4387..77025fec31 100644 --- a/helmfile.d/snippets/defaults.yaml +++ b/helmfile.d/snippets/defaults.yaml @@ -264,6 +264,9 @@ environments: memory: 512Mi cpu: 200m _rawValues: {} + headlamp: + enabled: false + _rawValues: {} istio: autoscaling: egressgateway: @@ -1166,3 +1169,4 @@ environments: users: [] versions: specVersion: 70 + From 8e481cc4fd8e6db32d2f0ce9a82a84137902547d Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:17:03 +0200 Subject: [PATCH 09/21] ci: add skill to add new helm chart --- .github/workflows/ci-helm-chart.yml | 6 +++++- ci/package.json | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-helm-chart.yml b/.github/workflows/ci-helm-chart.yml index d9cee6d233..216ea2d597 100644 --- a/.github/workflows/ci-helm-chart.yml +++ b/.github/workflows/ci-helm-chart.yml @@ -60,7 +60,11 @@ jobs: - name: Install packages run: | cd ci/ - npm install + npm ci + - name: Run CI script tests + run: | + cd ci/ + npm run tests - name: Update helm charts env: CI_UPDATE_TYPE: ${{ inputs.CI_UPDATE_TYPE }} diff --git a/ci/package.json b/ci/package.json index 8175f77edc..1fcf71b073 100644 --- a/ci/package.json +++ b/ci/package.json @@ -26,7 +26,8 @@ }, "scripts": { "update-helm-chart-deps": "src/update-helm-chart-deps.mjs", - "check-schema-versions": "src/check-schema-versions.mjs" + "check-schema-versions": "src/check-schema-versions.mjs", + "tests": "npm --prefix .. run test:ts -- ci/src" }, "type": "commonjs", "version": "1.0.0" From 54782b2fcd126d4d4b91762d3917e87f30e61705 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:18:20 +0200 Subject: [PATCH 10/21] ci: add skill to add new helm chart --- .../scripts/configure-public-app-exposure.sh | 7 ------- 1 file changed, 7 deletions(-) delete mode 100755 .github/skills/add-app-ingress/scripts/configure-public-app-exposure.sh diff --git a/.github/skills/add-app-ingress/scripts/configure-public-app-exposure.sh b/.github/skills/add-app-ingress/scripts/configure-public-app-exposure.sh deleted file mode 100755 index ad125e2b5e..0000000000 --- a/.github/skills/add-app-ingress/scripts/configure-public-app-exposure.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)" -cd "$repo_root" - -exec npx tsx ci/src/configure-public-exposure.ts "$@" From 5ddd8d60efbe44b812a3b83993c2e688baf69f99 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:20:44 +0200 Subject: [PATCH 11/21] ci: add skill to add new helm chart --- .../skills/add-app-helm-chart/scripts/add-helm-chart.sh | 7 ------- ci/src/add-app-helm-chart.ts | 2 ++ 2 files changed, 2 insertions(+), 7 deletions(-) delete mode 100755 .github/skills/add-app-helm-chart/scripts/add-helm-chart.sh diff --git a/.github/skills/add-app-helm-chart/scripts/add-helm-chart.sh b/.github/skills/add-app-helm-chart/scripts/add-helm-chart.sh deleted file mode 100755 index e0be59dcd9..0000000000 --- a/.github/skills/add-app-helm-chart/scripts/add-helm-chart.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)" -cd "$repo_root" - -exec npx tsx ci/src/add-app-helm-chart.ts "$@" diff --git a/ci/src/add-app-helm-chart.ts b/ci/src/add-app-helm-chart.ts index 6641d03c4a..dcea4f349b 100644 --- a/ci/src/add-app-helm-chart.ts +++ b/ci/src/add-app-helm-chart.ts @@ -229,6 +229,8 @@ function createHelmfile(root: string, name: string): string { 'bases:', ' - snippets/defaults.yaml', '---', + ' - snippets/defaults.gotmpl', + '---', 'bases:', ' - snippets/env.gotmpl', '---', From 1dd5af8c568ba2a24abd249c8963f6c41b73d736 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:24:04 +0200 Subject: [PATCH 12/21] ci: add app ingress skill --- helmfile.d/helmfile-92.headlamp.yaml.gotmpl | 21 --------------------- helmfile.d/snippets/defaults.yaml | 4 ---- 2 files changed, 25 deletions(-) delete mode 100644 helmfile.d/helmfile-92.headlamp.yaml.gotmpl diff --git a/helmfile.d/helmfile-92.headlamp.yaml.gotmpl b/helmfile.d/helmfile-92.headlamp.yaml.gotmpl deleted file mode 100644 index 0dfd14e4a1..0000000000 --- a/helmfile.d/helmfile-92.headlamp.yaml.gotmpl +++ /dev/null @@ -1,21 +0,0 @@ -bases: - - snippets/defaults.yaml ---- -bases: - - snippets/env.gotmpl ---- -bases: - - snippets/derived.gotmpl ---- -{{ readFile "snippets/templates.gotmpl" }} -{{- $v := .Values }} -{{- $a := $v.apps }} - -releases: - - name: headlamp - installed: {{ $a | get "headlamp.enabled" }} - namespace: headlamp - labels: - pkg: headlamp - app: core - <<: *default diff --git a/helmfile.d/snippets/defaults.yaml b/helmfile.d/snippets/defaults.yaml index 77025fec31..0b378a4387 100644 --- a/helmfile.d/snippets/defaults.yaml +++ b/helmfile.d/snippets/defaults.yaml @@ -264,9 +264,6 @@ environments: memory: 512Mi cpu: 200m _rawValues: {} - headlamp: - enabled: false - _rawValues: {} istio: autoscaling: egressgateway: @@ -1169,4 +1166,3 @@ environments: users: [] versions: specVersion: 70 - From e4f085e70688a877800e490fd452ef447ba46e4b Mon Sep 17 00:00:00 2001 From: jeho <17126497+j-zimnowoda@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:26:54 +0200 Subject: [PATCH 13/21] fix: finding CodeQL / Regular expression injection Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- ci/src/add-app-helm-chart.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ci/src/add-app-helm-chart.ts b/ci/src/add-app-helm-chart.ts index dcea4f349b..14be45edbf 100644 --- a/ci/src/add-app-helm-chart.ts +++ b/ci/src/add-app-helm-chart.ts @@ -120,7 +120,8 @@ function findHighestHelmfileNumber(helmfileDir: string): number { function hasReleaseNameInHelmfiles(helmfileDir: string, releaseName: string): boolean { const files = fs.readdirSync(helmfileDir).filter((f) => f.endsWith('.yaml') || f.endsWith('.gotmpl')) - const rx = new RegExp(`^\\s*-\\s+name:\\s+${releaseName}\\s*$`, 'm') + const escapedReleaseName = releaseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const rx = new RegExp(`^\\s*-\\s+name:\\s+${escapedReleaseName}\\s*$`, 'm') for (const file of files) { const content = fs.readFileSync(path.join(helmfileDir, file), 'utf8') if (rx.test(content)) return true From eebaac54f2928d0414faaa3ffc94230458988905 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:47:46 +0200 Subject: [PATCH 14/21] ci: rework --- ci/src/add-app-helm-chart.test.ts | 37 +++++++++++- ci/src/add-app-helm-chart.ts | 3 +- ci/src/configure-public-exposure.test.ts | 73 +++++++++++++++++++++++- ci/src/configure-public-exposure.ts | 36 +++++++----- 4 files changed, 133 insertions(+), 16 deletions(-) diff --git a/ci/src/add-app-helm-chart.test.ts b/ci/src/add-app-helm-chart.test.ts index 541f89bea6..79c291ad20 100644 --- a/ci/src/add-app-helm-chart.test.ts +++ b/ci/src/add-app-helm-chart.test.ts @@ -1,4 +1,39 @@ -import { parseStableSemver, resolveLatestOfficialSemver, type RuntimeDeps } from './add-app-helm-chart' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' + +import { createHelmfile, parseStableSemver, resolveLatestOfficialSemver, type RuntimeDeps } from './add-app-helm-chart' + +describe('createHelmfile', () => { + let tmpDir: string + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'add-app-helm-chart-')) + fs.mkdirSync(path.join(tmpDir, 'helmfile.d')) + }) + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }) + }) + + it('generates a helmfile with four valid bases documents before releases', () => { + const relPath = createHelmfile(tmpDir, 'myapp') + const content = fs.readFileSync(path.join(tmpDir, relPath), 'utf8') + const docs = content.split(/^---$/m).map((d) => d.trim()) + + expect(docs[0]).toBe('bases:\n - snippets/defaults.yaml') + expect(docs[1]).toBe('bases:\n - snippets/defaults.gotmpl') + expect(docs[2]).toBe('bases:\n - snippets/env.gotmpl') + expect(docs[3]).toBe('bases:\n - snippets/derived.gotmpl') + expect(docs[4]).toContain('releases:') + }) + + it('names the file using the next helmfile sequence number', () => { + fs.writeFileSync(path.join(tmpDir, 'helmfile.d', 'helmfile-92.other.yaml.gotmpl'), '') + const relPath = createHelmfile(tmpDir, 'myapp') + expect(path.basename(relPath)).toBe('helmfile-93.myapp.yaml.gotmpl') + }) +}) describe('add-app-helm-chart semver resolution', () => { const makeDeps = (runCommand: RuntimeDeps['runCommand']): RuntimeDeps => ({ diff --git a/ci/src/add-app-helm-chart.ts b/ci/src/add-app-helm-chart.ts index 14be45edbf..ecb45db4ba 100644 --- a/ci/src/add-app-helm-chart.ts +++ b/ci/src/add-app-helm-chart.ts @@ -220,7 +220,7 @@ function vendorChart(root: string, name: string, version: string, repository: st console.log(`Vendored chart into charts/${name}`) } -function createHelmfile(root: string, name: string): string { +export function createHelmfile(root: string, name: string): string { const next = findHighestHelmfileNumber(path.join(root, 'helmfile.d')) + 1 const padded = String(next).padStart(2, '0') const relPath = `helmfile.d/helmfile-${padded}.${name}.yaml.gotmpl` @@ -230,6 +230,7 @@ function createHelmfile(root: string, name: string): string { 'bases:', ' - snippets/defaults.yaml', '---', + 'bases:', ' - snippets/defaults.gotmpl', '---', 'bases:', diff --git a/ci/src/configure-public-exposure.test.ts b/ci/src/configure-public-exposure.test.ts index 4f38265330..1c638c9203 100644 --- a/ci/src/configure-public-exposure.test.ts +++ b/ci/src/configure-public-exposure.test.ts @@ -2,7 +2,36 @@ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' -import { ensurePodAuthLabels, ensureRawValuesFile, parseBool } from './configure-public-exposure' +import { ensurePodAuthLabels, ensureRawValuesFile, findHelmfileForRelease, parseBool } from './configure-public-exposure' + +describe('findHelmfileForRelease', () => { + let tmpDir: string + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'find-helmfile-')) + }) + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }) + }) + + it('finds helmfile containing the named release', () => { + fs.writeFileSync(path.join(tmpDir, 'helmfile-01.foo.yaml'), ' - name: foo\n') + expect(findHelmfileForRelease(tmpDir, 'foo')).toBe(path.join(tmpDir, 'helmfile-01.foo.yaml')) + }) + + it('treats release name regex metacharacters as literals', () => { + fs.writeFileSync(path.join(tmpDir, 'helmfile-01.my.app.yaml'), ' - name: my.app\n') + fs.writeFileSync(path.join(tmpDir, 'helmfile-02.myXapp.yaml'), ' - name: myXapp\n') + // 'my.app' as unescaped regex would also match 'myXapp' (dot matches any char) + expect(findHelmfileForRelease(tmpDir, 'my.app')).toBe(path.join(tmpDir, 'helmfile-01.my.app.yaml')) + }) + + it('throws when no helmfile contains the named release', () => { + fs.writeFileSync(path.join(tmpDir, 'helmfile-01.foo.yaml'), ' - name: foo\n') + expect(() => findHelmfileForRelease(tmpDir, 'bar')).toThrow('Could not find a Helmfile release named bar') + }) +}) describe('configure-public-exposure helpers', () => { let tmpDir: string @@ -86,6 +115,48 @@ describe('configure-public-exposure helpers', () => { expect(content).toContain('otomi.io/auth-policy: platform') }) + it('ensurePodAuthLabels corrects wrong auth label values inside existing podLabels block', () => { + const valuesFile = path.join(tmpDir, 'values', 'demo', 'demo.gotmpl') + fs.mkdirSync(path.dirname(valuesFile), { recursive: true }) + fs.writeFileSync( + valuesFile, + [ + 'podLabels:', + ' otomi.io/auth: platform-admin', + ' otomi.io/auth-policy: platform-admin', + '', + ].join('\n'), + 'utf8', + ) + + ensurePodAuthLabels(valuesFile, 'demo') + + const content = fs.readFileSync(valuesFile, 'utf8') + expect(content).toContain(' otomi.io/auth: platform') + expect(content).toContain(' otomi.io/auth-policy: platform') + expect(content).not.toContain('platform-admin') + }) + + it('ensurePodAuthLabels injects labels when auth keys appear only outside podLabels', () => { + const valuesFile = path.join(tmpDir, 'values', 'demo', 'demo.gotmpl') + fs.mkdirSync(path.dirname(valuesFile), { recursive: true }) + // auth keys appear in a comment, not in a podLabels block + const initial = [ + '# otomi.io/auth: platform', + '# otomi.io/auth-policy: platform', + 'replicaCount: 1', + '', + ].join('\n') + fs.writeFileSync(valuesFile, initial, 'utf8') + + ensurePodAuthLabels(valuesFile, 'demo') + + const content = fs.readFileSync(valuesFile, 'utf8') + expect(content).toContain('podLabels:') + expect(content).toContain(' otomi.io/auth: platform') + expect(content).toContain(' otomi.io/auth-policy: platform') + }) + it('ensurePodAuthLabels keeps file unchanged when both labels already exist', () => { const valuesFile = path.join(tmpDir, 'values', 'demo', 'demo.gotmpl') fs.mkdirSync(path.dirname(valuesFile), { recursive: true }) diff --git a/ci/src/configure-public-exposure.ts b/ci/src/configure-public-exposure.ts index b0370c5c6a..0160762df0 100644 --- a/ci/src/configure-public-exposure.ts +++ b/ci/src/configure-public-exposure.ts @@ -108,9 +108,10 @@ function insertSortedBlock( return out } -function findHelmfileForRelease(helmfileDir: string, releaseName: string): string { +export function findHelmfileForRelease(helmfileDir: string, releaseName: string): string { const files = fs.readdirSync(helmfileDir).filter((f) => f.endsWith('.yaml') || f.endsWith('.gotmpl')) - const rx = new RegExp(`^\\s*-\\s+name:\\s+${releaseName}\\s*$`, 'm') + const escapedReleaseName = releaseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const rx = new RegExp(`^\\s*-\\s+name:\\s+${escapedReleaseName}\\s*$`, 'm') const matches = files.filter((file) => rx.test(fs.readFileSync(path.join(helmfileDir, file), 'utf8'))) if (matches.length === 0) { @@ -315,10 +316,6 @@ export function ensurePodAuthLabels(valuesFile: string, name: string): void { } const content = fs.readFileSync(valuesFile, 'utf8') - if (content.includes('otomi.io/auth:') && content.includes('otomi.io/auth-policy:')) { - console.log(`values/${name}/${name}.gotmpl already contains auth pod labels`) - return - } const labelsBlock = ['podLabels:', ' otomi.io/auth: platform', ' otomi.io/auth-policy: platform'] const trimmed = content.trim() @@ -341,19 +338,32 @@ export function ensurePodAuthLabels(valuesFile: string, name: string): void { return lines.length })() - const hasAuth = lines.slice(podLabelsIdx + 1, insertAt).some((line) => line.trim().startsWith('otomi.io/auth:')) - const hasAuthPolicy = lines.slice(podLabelsIdx + 1, insertAt).some((line) => line.trim().startsWith('otomi.io/auth-policy:')) + const blockLines = lines.slice(podLabelsIdx + 1, insertAt) + const authIdx = blockLines.findIndex((line) => line.trim().startsWith('otomi.io/auth:')) + const authPolicyIdx = blockLines.findIndex((line) => line.trim().startsWith('otomi.io/auth-policy:')) - const toInsert: string[] = [] - if (!hasAuth) toInsert.push(' otomi.io/auth: platform') - if (!hasAuthPolicy) toInsert.push(' otomi.io/auth-policy: platform') + const authCorrect = authIdx >= 0 && blockLines[authIdx].trim() === 'otomi.io/auth: platform' + const authPolicyCorrect = authPolicyIdx >= 0 && blockLines[authPolicyIdx].trim() === 'otomi.io/auth-policy: platform' - if (toInsert.length === 0) { + if (authCorrect && authPolicyCorrect) { console.log(`values/${name}/${name}.gotmpl already contains auth pod labels`) return } - lines.splice(insertAt, 0, ...toInsert) + const updatedBlock = [...blockLines] + if (authIdx >= 0) { + updatedBlock[authIdx] = ' otomi.io/auth: platform' + } else { + updatedBlock.push(' otomi.io/auth: platform') + } + const updatedAuthPolicyIdx = updatedBlock.findIndex((line) => line.trim().startsWith('otomi.io/auth-policy:')) + if (updatedAuthPolicyIdx >= 0) { + updatedBlock[updatedAuthPolicyIdx] = ' otomi.io/auth-policy: platform' + } else { + updatedBlock.push(' otomi.io/auth-policy: platform') + } + + lines.splice(podLabelsIdx + 1, insertAt - (podLabelsIdx + 1), ...updatedBlock) writeLines(valuesFile, lines) console.log(`Updated podLabels auth entries in values/${name}/${name}.gotmpl`) return From a72981f8485d87a14eae449b964f55ebf313b6b4 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:52:23 +0200 Subject: [PATCH 15/21] ci: make skills available to claude --- .claude/skills | 1 + 1 file changed, 1 insertion(+) create mode 120000 .claude/skills diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000000..3e73f3a383 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.github/skills \ No newline at end of file From d655c10071ce19f2b65639c9cf055bcf2aaf9269 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:54:17 +0200 Subject: [PATCH 16/21] ci: rename skill --- .github/skills/{add-app-helm-chart => apl-add-app}/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .github/skills/{add-app-helm-chart => apl-add-app}/SKILL.md (94%) diff --git a/.github/skills/add-app-helm-chart/SKILL.md b/.github/skills/apl-add-app/SKILL.md similarity index 94% rename from .github/skills/add-app-helm-chart/SKILL.md rename to .github/skills/apl-add-app/SKILL.md index 1b41a26f10..bd8bc41600 100644 --- a/.github/skills/add-app-helm-chart/SKILL.md +++ b/.github/skills/apl-add-app/SKILL.md @@ -1,5 +1,5 @@ --- -name: add-app-helm-chart +name: apl-add-app description: Add and wire a new Helm chart into APL Core by updating chart index, vendoring the chart, scaffolding Helmfile/defaults/values files, and running required validation checks. Use when adding a new app chart and the user provides or asks for name and repository; the script resolves the latest official stable semver chart version automatically. argument-hint: --- @@ -47,5 +47,5 @@ Checklist: - If chart source is OCI, repository should be oci://... (script pulls from /). - Chart version is auto-selected as latest stable semver; pre-release versions are ignored. - Main implementation is in ci/src/add-app-helm-chart.ts. -- .github/skills/add-app-helm-chart/scripts/add-app-helm-chart.sh is a compatibility wrapper. +- .github/skills/apl-add-app/scripts/add-app-helm-chart.sh is a compatibility wrapper. - Script confirms rendering path by running bin/compare.sh. From 94ac6f25896e0d2daf9741e25ea6f4965bedada1 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:55:13 +0200 Subject: [PATCH 17/21] ci: rename skill --- .../skills/{add-app-ingress => apl-add-app-ingress}/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/skills/{add-app-ingress => apl-add-app-ingress}/SKILL.md (98%) diff --git a/.github/skills/add-app-ingress/SKILL.md b/.github/skills/apl-add-app-ingress/SKILL.md similarity index 98% rename from .github/skills/add-app-ingress/SKILL.md rename to .github/skills/apl-add-app-ingress/SKILL.md index 658ed1bbb0..d8836b7f5a 100644 --- a/.github/skills/add-app-ingress/SKILL.md +++ b/.github/skills/apl-add-app-ingress/SKILL.md @@ -1,5 +1,5 @@ --- -name: add-app-ingress +name: apl-add-app-ingress description: Configure public exposure for a selected app by updating core namespace/admin app configuration and optionally scaffolding HTTPRoute auth-redirects plus authentication/authorization resources. argument-hint: [--configure-public-security true|false] --- From f24bfdb939da67906c2a87e9f99ac9cf88ee66ad Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:55:49 +0200 Subject: [PATCH 18/21] ci: rename skill --- .github/skills/apl-add-app-ingress/SKILL.md | 2 +- .github/skills/apl-add-app/SKILL.md | 2 +- .../SKILL.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename .github/skills/{test-gotemplate-renders => apl-test-gotemplate-renders}/SKILL.md (96%) diff --git a/.github/skills/apl-add-app-ingress/SKILL.md b/.github/skills/apl-add-app-ingress/SKILL.md index d8836b7f5a..1f2c5dbb4d 100644 --- a/.github/skills/apl-add-app-ingress/SKILL.md +++ b/.github/skills/apl-add-app-ingress/SKILL.md @@ -18,7 +18,7 @@ npx tsx ci/src/configure-public-exposure.ts [--configure-public-security 4. Validate: -Use `/test-gotemplate-renders` skill to validate Go template rendering. +Use `/apl-test-gotemplate-renders` skill to validate Go template rendering. ## Workflow diff --git a/.github/skills/apl-add-app/SKILL.md b/.github/skills/apl-add-app/SKILL.md index bd8bc41600..46c79b61bb 100644 --- a/.github/skills/apl-add-app/SKILL.md +++ b/.github/skills/apl-add-app/SKILL.md @@ -17,7 +17,7 @@ npx tsx ci/src/add-app-helm-chart.ts 3. Validate: -Use `/test-gotemplate-renders` skill to validate Go template rendering. +Use `/apl-test-gotemplate-renders` skill to validate Go template rendering. ## Workflow diff --git a/.github/skills/test-gotemplate-renders/SKILL.md b/.github/skills/apl-test-gotemplate-renders/SKILL.md similarity index 96% rename from .github/skills/test-gotemplate-renders/SKILL.md rename to .github/skills/apl-test-gotemplate-renders/SKILL.md index 2630697a25..351af2e1c6 100644 --- a/.github/skills/test-gotemplate-renders/SKILL.md +++ b/.github/skills/apl-test-gotemplate-renders/SKILL.md @@ -1,5 +1,5 @@ --- -name: test-gotemplate-renders +name: apl-test-gotemplate-renders description: Enforces rendered Go template comparison before and after templating-related changes using bin/compare.sh. --- From 63c794cebeb144c512baadfd71637315a7d45bdf Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:02:29 +0200 Subject: [PATCH 19/21] chore: settings rework --- .vscode/settings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index bb5e266d46..b1c0f314c8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -62,8 +62,8 @@ "editor.defaultFormatter": "ms-azuretools.vscode-docker" }, "chat.tools.terminal.autoApprove": { - "npx tsx ci/src/configure-public-exposure.ts headlamp": true, - "npx tsx ci/src/add-app-helm-chart.ts headlamp": true, + "npx tsx ci/src/configure-public-exposure.ts": true, + "npx tsx ci/src/add-app-helm-chart.ts": true, "export ENV_DIR=$PWD/tests/fixtures": true, "bin/compare.sh": true, "npm run validate-values": true, From bae1f9b5633f69b0317941f38fa127dc9c82b9f1 Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:36:39 +0200 Subject: [PATCH 20/21] ci: fixtures --- .github/skills/apl-add-app/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/skills/apl-add-app/SKILL.md b/.github/skills/apl-add-app/SKILL.md index 39852f3962..9bd4dc6f2e 100644 --- a/.github/skills/apl-add-app/SKILL.md +++ b/.github/skills/apl-add-app/SKILL.md @@ -33,8 +33,8 @@ Checklist: - [ ] Always add namespace entry in core.yaml. - [ ] Add empty defaults entry in helmfile.d/snippets/defaults.yaml in alphabetical order. - [ ] Create values//.gotmpl. -- [ ] Add tests fixture app file in tests/fixtures/env/apps/.yaml. -- [ ] Add corresponding app entries in tests/integration/\*.yaml. +- [ ] Add tests fixture app file in tests/fixtures/env/apps/.yaml with enabled: true. +- [ ] Add corresponding app entries in tests/integration/\*.yaml with enabled: false. - [ ] If public exposure is needed, run the apl-add-app-ingressskill afterward. - [ ] Run schema and template-diff checks. - [ ] Summarize generated diffs and touched files. From 8358c20e3ade2806ffb993f88df50528c60a5bfb Mon Sep 17 00:00:00 2001 From: Jehoszafat Zimnowoda <17126497+j-zimnowoda@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:44:54 +0200 Subject: [PATCH 21/21] feat: add headlamp test --- chart/chart-index/Chart.yaml | 4 + charts/headlamp/.helmignore | 23 + charts/headlamp/Chart.yaml | 43 + charts/headlamp/README.md | 635 +++++++++++ charts/headlamp/templates/NOTES.txt | 40 + charts/headlamp/templates/_helpers.tpl | 119 +++ .../cluster-inventory-configmap.yaml | 39 + .../templates/clusterrolebinding.yaml | 20 + charts/headlamp/templates/deployment.yaml | 559 ++++++++++ .../headlamp/templates/extra-manifests.yaml | 6 + charts/headlamp/templates/httproute.yaml | 37 + charts/headlamp/templates/ingress.yaml | 66 ++ charts/headlamp/templates/pdb.yaml | 26 + .../headlamp/templates/plugin-configmap.yaml | 11 + charts/headlamp/templates/pvc.yaml | 32 + charts/headlamp/templates/secret.yaml | 41 + charts/headlamp/templates/service.yaml | 47 + charts/headlamp/templates/serviceaccount.yaml | 13 + .../azure-oidc-with-validators.yaml | 157 +++ .../cluster-inventory-plugins.yaml | 183 ++++ .../expected_templates/cluster-inventory.yaml | 173 +++ .../tests/expected_templates/default.yaml | 142 +++ .../disable-static-plugins.yaml | 144 +++ .../tests/expected_templates/extra-args.yaml | 143 +++ .../expected_templates/extra-manifests.yaml | 159 +++ .../expected_templates/host-aliases.yaml | 150 +++ .../host-users-override.yaml | 142 +++ .../expected_templates/httproute-enabled.yaml | 172 +++ .../ingress-multi-backend.yaml | 193 ++++ .../me-user-info-url-directly.yaml | 145 +++ .../expected_templates/me-user-info-url.yaml | 149 +++ ...namespace-override-oidc-create-secret.yaml | 174 +++ .../namespace-override.yaml | 142 +++ .../expected_templates/non-azure-oidc.yaml | 149 +++ .../oidc-create-secret.yaml | 174 +++ .../expected_templates/oidc-directly-env.yaml | 158 +++ .../expected_templates/oidc-directly.yaml | 149 +++ .../oidc-external-secret-with-scopes.yaml | 139 +++ .../oidc-external-secret.yaml | 138 +++ .../tests/expected_templates/oidc-pkce.yaml | 152 +++ .../oidc-validator-overrides.yaml | 161 +++ .../expected_templates/pod-disruption.yaml | 162 +++ ...nly-root-filesystem-custom-tmp-volume.yaml | 150 +++ .../readonly-root-filesystem-custom-tmp.yaml | 149 +++ ...donly-root-filesystem-plugins-inherit.yaml | 203 ++++ ...readonly-root-filesystem-plugins-only.yaml | 195 ++++ .../readonly-root-filesystem.yaml | 149 +++ .../expected_templates/security-context.yaml | 215 ++++ .../service-account-token-custom-token.yaml | 144 +++ .../service-account-token-out-of-cluster.yaml | 140 +++ .../service-account-token.yaml | 143 +++ .../service-appprotocol.yaml | 143 +++ .../service-extra-ports.yaml | 152 +++ .../tests/expected_templates/tls-added.yaml | 156 +++ ...gy-spread-constraints-custom-selector.yaml | 153 +++ .../topology-spread-constraints.yaml | 163 +++ .../expected_templates/volumes-added.yaml | 145 +++ ...ster-inventory-empty-access-providers.yaml | 3 + ...tory-plugin-command-equals-mount-path.yaml | 15 + ...cluster-inventory-plugin-invalid-name.yaml | 15 + ...uster-inventory-plugin-mount-mismatch.yaml | 15 + ...-inventory-plugin-relative-mount-path.yaml | 15 + charts/headlamp/tests/readme.md | 96 ++ charts/headlamp/tests/test.sh | 142 +++ .../azure-oidc-with-validators.yaml | 12 + .../test_cases/cluster-inventory-plugins.yaml | 24 + .../tests/test_cases/cluster-inventory.yaml | 8 + .../test_cases/disable-static-plugins.yaml | 7 + .../headlamp/tests/test_cases/extra-args.yaml | 5 + .../tests/test_cases/extra-manifests.yaml | 16 + .../tests/test_cases/host-aliases.yaml | 8 + .../tests/test_cases/host-users-override.yaml | 1 + .../tests/test_cases/httproute-enabled.yaml | 12 + .../test_cases/ingress-multi-backend.yaml | 32 + .../test_cases/me-user-info-url-directly.yaml | 3 + .../tests/test_cases/me-user-info-url.yaml | 4 + ...namespace-override-oidc-create-secret.yaml | 18 + .../tests/test_cases/namespace-override.yaml | 1 + .../tests/test_cases/non-azure-oidc.yaml | 10 + .../tests/test_cases/oidc-create-secret.yaml | 16 + .../tests/test_cases/oidc-directly-env.yaml | 10 + .../tests/test_cases/oidc-directly.yaml | 14 + .../oidc-external-secret-with-scopes.yaml | 11 + .../test_cases/oidc-external-secret.yaml | 10 + .../headlamp/tests/test_cases/oidc-pkce.yaml | 15 + .../test_cases/oidc-validator-overrides.yaml | 20 + .../tests/test_cases/pod-disruption.yaml | 5 + ...nly-root-filesystem-custom-tmp-volume.yaml | 7 + .../readonly-root-filesystem-custom-tmp.yaml | 10 + ...donly-root-filesystem-plugins-inherit.yaml | 5 + ...readonly-root-filesystem-plugins-only.yaml | 7 + .../test_cases/readonly-root-filesystem.yaml | 2 + .../tests/test_cases/security-context.yaml | 31 + .../service-account-token-custom-token.yaml | 5 + .../service-account-token-out-of-cluster.yaml | 5 + .../test_cases/service-account-token.yaml | 4 + .../tests/test_cases/service-appprotocol.yaml | 2 + .../tests/test_cases/service-extra-ports.yaml | 10 + .../headlamp/tests/test_cases/tls-added.yaml | 20 + ...gy-spread-constraints-custom-selector.yaml | 11 + .../topology-spread-constraints.yaml | 12 + .../tests/test_cases/volumes-added.yaml | 5 + charts/headlamp/tests/update-version.sh | 93 ++ charts/headlamp/values.schema.json | 989 ++++++++++++++++++ charts/headlamp/values.yaml | 533 ++++++++++ core.yaml | 5 + helmfile.d/helmfile-92.headlamp.yaml.gotmpl | 32 + helmfile.d/snippets/defaults.yaml | 4 + tests/fixtures/env/apps/headlamp.yaml | 6 + tests/integration/full.yaml | 3 + tests/integration/minimal-with-team.yaml | 3 + tests/integration/minimal.yaml | 3 + tests/integration/monitoring-with-team.yaml | 3 + values/headlamp/headlamp-raw.gotmpl | 69 ++ values/headlamp/headlamp.gotmpl | 3 + 115 files changed, 10356 insertions(+) create mode 100644 charts/headlamp/.helmignore create mode 100644 charts/headlamp/Chart.yaml create mode 100644 charts/headlamp/README.md create mode 100644 charts/headlamp/templates/NOTES.txt create mode 100644 charts/headlamp/templates/_helpers.tpl create mode 100644 charts/headlamp/templates/cluster-inventory-configmap.yaml create mode 100644 charts/headlamp/templates/clusterrolebinding.yaml create mode 100644 charts/headlamp/templates/deployment.yaml create mode 100644 charts/headlamp/templates/extra-manifests.yaml create mode 100644 charts/headlamp/templates/httproute.yaml create mode 100644 charts/headlamp/templates/ingress.yaml create mode 100644 charts/headlamp/templates/pdb.yaml create mode 100644 charts/headlamp/templates/plugin-configmap.yaml create mode 100644 charts/headlamp/templates/pvc.yaml create mode 100644 charts/headlamp/templates/secret.yaml create mode 100644 charts/headlamp/templates/service.yaml create mode 100644 charts/headlamp/templates/serviceaccount.yaml create mode 100644 charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml create mode 100644 charts/headlamp/tests/expected_templates/cluster-inventory-plugins.yaml create mode 100644 charts/headlamp/tests/expected_templates/cluster-inventory.yaml create mode 100644 charts/headlamp/tests/expected_templates/default.yaml create mode 100644 charts/headlamp/tests/expected_templates/disable-static-plugins.yaml create mode 100644 charts/headlamp/tests/expected_templates/extra-args.yaml create mode 100644 charts/headlamp/tests/expected_templates/extra-manifests.yaml create mode 100644 charts/headlamp/tests/expected_templates/host-aliases.yaml create mode 100644 charts/headlamp/tests/expected_templates/host-users-override.yaml create mode 100644 charts/headlamp/tests/expected_templates/httproute-enabled.yaml create mode 100644 charts/headlamp/tests/expected_templates/ingress-multi-backend.yaml create mode 100644 charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml create mode 100644 charts/headlamp/tests/expected_templates/me-user-info-url.yaml create mode 100644 charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml create mode 100644 charts/headlamp/tests/expected_templates/namespace-override.yaml create mode 100644 charts/headlamp/tests/expected_templates/non-azure-oidc.yaml create mode 100644 charts/headlamp/tests/expected_templates/oidc-create-secret.yaml create mode 100644 charts/headlamp/tests/expected_templates/oidc-directly-env.yaml create mode 100644 charts/headlamp/tests/expected_templates/oidc-directly.yaml create mode 100644 charts/headlamp/tests/expected_templates/oidc-external-secret-with-scopes.yaml create mode 100644 charts/headlamp/tests/expected_templates/oidc-external-secret.yaml create mode 100644 charts/headlamp/tests/expected_templates/oidc-pkce.yaml create mode 100644 charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml create mode 100644 charts/headlamp/tests/expected_templates/pod-disruption.yaml create mode 100644 charts/headlamp/tests/expected_templates/readonly-root-filesystem-custom-tmp-volume.yaml create mode 100644 charts/headlamp/tests/expected_templates/readonly-root-filesystem-custom-tmp.yaml create mode 100644 charts/headlamp/tests/expected_templates/readonly-root-filesystem-plugins-inherit.yaml create mode 100644 charts/headlamp/tests/expected_templates/readonly-root-filesystem-plugins-only.yaml create mode 100644 charts/headlamp/tests/expected_templates/readonly-root-filesystem.yaml create mode 100644 charts/headlamp/tests/expected_templates/security-context.yaml create mode 100644 charts/headlamp/tests/expected_templates/service-account-token-custom-token.yaml create mode 100644 charts/headlamp/tests/expected_templates/service-account-token-out-of-cluster.yaml create mode 100644 charts/headlamp/tests/expected_templates/service-account-token.yaml create mode 100644 charts/headlamp/tests/expected_templates/service-appprotocol.yaml create mode 100644 charts/headlamp/tests/expected_templates/service-extra-ports.yaml create mode 100644 charts/headlamp/tests/expected_templates/tls-added.yaml create mode 100644 charts/headlamp/tests/expected_templates/topology-spread-constraints-custom-selector.yaml create mode 100644 charts/headlamp/tests/expected_templates/topology-spread-constraints.yaml create mode 100644 charts/headlamp/tests/expected_templates/volumes-added.yaml create mode 100644 charts/headlamp/tests/failing_test_cases/cluster-inventory-empty-access-providers.yaml create mode 100644 charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-command-equals-mount-path.yaml create mode 100644 charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-invalid-name.yaml create mode 100644 charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-mount-mismatch.yaml create mode 100644 charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-relative-mount-path.yaml create mode 100644 charts/headlamp/tests/readme.md create mode 100644 charts/headlamp/tests/test.sh create mode 100644 charts/headlamp/tests/test_cases/azure-oidc-with-validators.yaml create mode 100644 charts/headlamp/tests/test_cases/cluster-inventory-plugins.yaml create mode 100644 charts/headlamp/tests/test_cases/cluster-inventory.yaml create mode 100644 charts/headlamp/tests/test_cases/disable-static-plugins.yaml create mode 100644 charts/headlamp/tests/test_cases/extra-args.yaml create mode 100644 charts/headlamp/tests/test_cases/extra-manifests.yaml create mode 100644 charts/headlamp/tests/test_cases/host-aliases.yaml create mode 100644 charts/headlamp/tests/test_cases/host-users-override.yaml create mode 100644 charts/headlamp/tests/test_cases/httproute-enabled.yaml create mode 100644 charts/headlamp/tests/test_cases/ingress-multi-backend.yaml create mode 100644 charts/headlamp/tests/test_cases/me-user-info-url-directly.yaml create mode 100644 charts/headlamp/tests/test_cases/me-user-info-url.yaml create mode 100644 charts/headlamp/tests/test_cases/namespace-override-oidc-create-secret.yaml create mode 100644 charts/headlamp/tests/test_cases/namespace-override.yaml create mode 100644 charts/headlamp/tests/test_cases/non-azure-oidc.yaml create mode 100644 charts/headlamp/tests/test_cases/oidc-create-secret.yaml create mode 100644 charts/headlamp/tests/test_cases/oidc-directly-env.yaml create mode 100644 charts/headlamp/tests/test_cases/oidc-directly.yaml create mode 100644 charts/headlamp/tests/test_cases/oidc-external-secret-with-scopes.yaml create mode 100644 charts/headlamp/tests/test_cases/oidc-external-secret.yaml create mode 100644 charts/headlamp/tests/test_cases/oidc-pkce.yaml create mode 100644 charts/headlamp/tests/test_cases/oidc-validator-overrides.yaml create mode 100644 charts/headlamp/tests/test_cases/pod-disruption.yaml create mode 100644 charts/headlamp/tests/test_cases/readonly-root-filesystem-custom-tmp-volume.yaml create mode 100644 charts/headlamp/tests/test_cases/readonly-root-filesystem-custom-tmp.yaml create mode 100644 charts/headlamp/tests/test_cases/readonly-root-filesystem-plugins-inherit.yaml create mode 100644 charts/headlamp/tests/test_cases/readonly-root-filesystem-plugins-only.yaml create mode 100644 charts/headlamp/tests/test_cases/readonly-root-filesystem.yaml create mode 100644 charts/headlamp/tests/test_cases/security-context.yaml create mode 100644 charts/headlamp/tests/test_cases/service-account-token-custom-token.yaml create mode 100644 charts/headlamp/tests/test_cases/service-account-token-out-of-cluster.yaml create mode 100644 charts/headlamp/tests/test_cases/service-account-token.yaml create mode 100644 charts/headlamp/tests/test_cases/service-appprotocol.yaml create mode 100644 charts/headlamp/tests/test_cases/service-extra-ports.yaml create mode 100644 charts/headlamp/tests/test_cases/tls-added.yaml create mode 100644 charts/headlamp/tests/test_cases/topology-spread-constraints-custom-selector.yaml create mode 100644 charts/headlamp/tests/test_cases/topology-spread-constraints.yaml create mode 100644 charts/headlamp/tests/test_cases/volumes-added.yaml create mode 100644 charts/headlamp/tests/update-version.sh create mode 100644 charts/headlamp/values.schema.json create mode 100644 charts/headlamp/values.yaml create mode 100644 helmfile.d/helmfile-92.headlamp.yaml.gotmpl create mode 100644 tests/fixtures/env/apps/headlamp.yaml create mode 100644 values/headlamp/headlamp-raw.gotmpl create mode 100644 values/headlamp/headlamp.gotmpl diff --git a/chart/chart-index/Chart.yaml b/chart/chart-index/Chart.yaml index dcdc5adb0d..b6e9d3c3e3 100644 --- a/chart/chart-index/Chart.yaml +++ b/chart/chart-index/Chart.yaml @@ -17,6 +17,9 @@ dependencies: - name: cloudnative-pg version: 0.28.3 repository: https://cloudnative-pg.github.io/charts + - name: headlamp + version: 0.44.0 + repository: https://kubernetes-sigs.github.io/headlamp/ - name: plugin-barman-cloud alias: cloudnative-pg-plugin-barman-cloud version: 0.6.0 @@ -114,3 +117,4 @@ dependencies: - name: trivy-operator version: 0.32.1 repository: https://aquasecurity.github.io/helm-charts/ + diff --git a/charts/headlamp/.helmignore b/charts/headlamp/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/charts/headlamp/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/headlamp/Chart.yaml b/charts/headlamp/Chart.yaml new file mode 100644 index 0000000000..1a297f0170 --- /dev/null +++ b/charts/headlamp/Chart.yaml @@ -0,0 +1,43 @@ +annotations: + artifacthub.io/category: monitoring-logging + artifacthub.io/license: Apache-2.0 + artifacthub.io/screenshots: | + - title: Cluster Overview + url: https://raw.githubusercontent.com/kubernetes-sigs/headlamp/screenshots/screenshots/cluster_overview.png + - title: Cluster Chooser + url: https://raw.githubusercontent.com/kubernetes-sigs/headlamp/screenshots/screenshots/cluster_chooser.png + - title: Nodes + url: https://raw.githubusercontent.com/kubernetes-sigs/headlamp/screenshots/screenshots/nodes.png + - title: Resource edition + url: https://raw.githubusercontent.com/kubernetes-sigs/headlamp/screenshots/screenshots/resource_edition.png + - title: Editor Documentation + url: https://raw.githubusercontent.com/kubernetes-sigs/headlamp/screenshots/screenshots/editor_documentation.png + - title: Terminal + url: https://raw.githubusercontent.com/kubernetes-sigs/headlamp/screenshots/screenshots/terminal.png + artifacthub.io/signKey: | + fingerprint: 2956B7F7167769370C93730C7264DA7B85D08A37 + url: https://keys.openpgp.org/vks/v1/by-fingerprint/2956B7F7167769370C93730C7264DA7B85D08A37 +apiVersion: v2 +appVersion: 0.44.0 +description: Headlamp is an easy-to-use and extensible Kubernetes web UI. +home: https://headlamp.dev/ +icon: https://raw.githubusercontent.com/kubernetes-sigs/headlamp/main/docs/headlamp_light.svg +keywords: +- kubernetes +- plugins +- kinvolk +- headlamp +- dashboard +- ui +- web +- monitoring +- logging +maintainers: +- name: kinvolk + url: https://kinvolk.io/ +name: headlamp +sources: +- https://github.com/kubernetes-sigs/headlamp/tree/main/charts/headlamp +- https://github.com/kubernetes-sigs/headlamp +type: application +version: 0.44.0 diff --git a/charts/headlamp/README.md b/charts/headlamp/README.md new file mode 100644 index 0000000000..d706efc1b8 --- /dev/null +++ b/charts/headlamp/README.md @@ -0,0 +1,635 @@ +# Headlamp Helm Chart + +Headlamp is an easy-to-use and extensible Kubernetes web UI that provides: +- 🚀 Modern, fast, and responsive interface +- 🔒 OIDC authentication support +- 🔌 Plugin system for extensibility +- 🎯 Real-time cluster state updates + +## Prerequisites + +- Kubernetes 1.21+ +- Helm 3.x +- Cluster admin access for initial setup + +## Quick Start + +Add the Headlamp repository and install the chart: + +```console +$ helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/ +$ helm repo update +$ helm install my-headlamp headlamp/headlamp --namespace kube-system +``` + +Access Headlamp: +```console +$ kubectl port-forward -n kube-system svc/my-headlamp 8080:80 +``` +Then open http://localhost:8080 in your browser. + +## Installation + +### Basic Installation +```console +$ helm install my-headlamp headlamp/headlamp --namespace kube-system +``` + +### Installation with OIDC +```console +$ helm install my-headlamp headlamp/headlamp \ + --namespace kube-system \ + --set config.oidc.clientID=your-client-id \ + --set config.oidc.clientSecret=your-client-secret \ + --set config.oidc.issuerURL=https://your-issuer-url +``` + +### Installation with Ingress +```console +$ helm install my-headlamp headlamp/headlamp \ + --namespace kube-system \ + --set ingress.enabled=true \ + --set ingress.hosts[0].host=headlamp.example.com \ + --set ingress.hosts[0].paths[0].path=/ +``` + +### Upgrade note about image tags + +If `image.tag` is set explicitly (for example in a values file or via `--set image.tag=...`), Helm upgrades will keep that value. +This means the release can show a newer chart/app version while still running an older container image. + +To ensure the running image matches the chart version during upgrade, set the tag explicitly to the chart's appVersion-derived format. +```console +$ helm upgrade my-headlamp headlamp/headlamp \ + --namespace kube-system \ + --reuse-values \ + --set image.tag=v +``` + +### Installation with Cluster Inventory + +> **Warning** +> Cluster Inventory support in Headlamp is alpha/experimental and disabled by +> default. The upstream Cluster Inventory API is currently `v1alpha1` and this +> integration uses the `v0.1.x` API, so fields and behavior may change. + +```console +$ helm install my-headlamp headlamp/headlamp \ + --namespace kube-system \ + --values cluster-inventory-values.yaml +``` + +`cluster-inventory-values.yaml`: + +```yaml +config: + clusterInventory: + enabled: true + accessProvidersConfig: + providers: + - name: secretreader + execConfig: + apiVersion: client.authentication.k8s.io/v1 + command: /access-plugins/secretreader/bin/secretreader-plugin + interactiveMode: Never + provideClusterInfo: true + - name: kubeconfig-secretreader + execConfig: + apiVersion: client.authentication.k8s.io/v1 + command: /access-plugins/kubeconfig-secretreader/bin/kubeconfig-secretreader-plugin + interactiveMode: Never + provideClusterInfo: true + plugins: + - name: secretreader + image: registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3@sha256:ec3090dc166aa2b42fb35d714d161c417d8b27bbc463404c8f615f5f4c610a1d + mountPath: /access-plugins/secretreader + - name: kubeconfig-secretreader + image: registry.k8s.io/cluster-inventory-api/kubeconfig-secretreader:v0.1.3@sha256:b92966cc6e4ac78002a63862921022a71d54956826f6e4febcb7247495eb98c0 + mountPath: /access-plugins/kubeconfig-secretreader +``` + +`plugins[]` mounts Cluster Inventory access provider binaries as Kubernetes +`image` volumes; it is not for Headlamp UI plugins. + +## Configuration + +### Core Parameters + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| replicaCount | int | `1` | Number of desired pods | +| image.registry | string | `"ghcr.io"` | Container image registry | +| image.repository | string | `"headlamp-k8s/headlamp"` | Container image name | +| image.tag | string | `""` | Container image tag (defaults to Chart appVersion) | +| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | + +### Application Configuration + +| Key | Type | Default | Description | +|--------------------|--------|-----------------------|---------------------------------------------------------------------------| +| config.inCluster | bool | `true` | Run Headlamp in-cluster | +| config.baseURL | string | `""` | Base URL path for Headlamp UI | +| config.sessionTTL | int | `86400` | The time in seconds for the internal session to remain valid (Default: 86400/24h, Min: 1 , Max: 31536000/1yr) | +| config.unsafeUseServiceAccountToken | bool | `false` | UNSAFE: authenticate every user as the pod's service account when running in-cluster. Only safe behind an auth proxy | +| config.serviceAccountTokenPath | string | `""` | Path to the service account token file. Used only when `unsafeUseServiceAccountToken` is true | +| config.pluginsDir | string | `"/headlamp/plugins"` | Directory to load Headlamp plugins from | +| config.staticPlugins.enabled | bool | `true` | Serve the bundled static plugins shipped in the image (e.g. the Prometheus "Show Prometheus metrics" plugin). Set to false to disable them | +| config.enableHelm | bool | `false` | Enable Helm operations like install, upgrade and uninstall of Helm charts | +| config.podDebugImage | string | `""` | Default image to use when creating pod debug containers | +| config.nodeShellImage | string | `""` | Default image to use when creating node shell pods | +| config.nodeShellNamespace | string | `""` | Default namespace to use when creating node shell pods | +| config.clusterInventory.enabled | bool | `false` | Enable experimental/alpha Cluster Inventory discovery | +| config.clusterInventory.accessProvidersConfig | object | `{}` | Experimental/alpha Cluster Inventory access providers config. Required when Cluster Inventory is enabled | +| config.clusterInventory.plugins | list | `[]` | Kubernetes image volumes that provide experimental/alpha Cluster Inventory access provider binaries | +| config.clusterInventory.labelSelector | string | `"!headlamp.dev/ignore"` | Kubernetes label selector used to filter experimental/alpha ClusterProfile resources | +| config.clusterInventory.rootReconcileInterval | string | `""` | Override the experimental/alpha Cluster Inventory root reconcile interval. Empty uses the Headlamp default | +| config.clusterInventory.noCRDCacheTTL | string | `""` | Override the experimental/alpha Cluster Inventory no-CRD cache TTL. Empty uses the Headlamp default | +| config.extraArgs | array | `[]` | Additional arguments for Headlamp server | +| config.tlsCertPath | string | `""` | Certificate for serving TLS | +| config.tlsKeyPath | string | `""` | Key for serving TLS | + +When `config.unsafeUseServiceAccountToken` is enabled, the token file must be +mounted and readable in the Headlamp container. The default path is provided by +`automountServiceAccountToken`; custom paths require a matching projected +volume or volume mount. + +### OIDC Configuration + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| config.oidc.clientID | string | `""` | OIDC client ID | +| config.oidc.clientSecret | string | `""` | OIDC client secret | +| config.oidc.issuerURL | string | `""` | OIDC issuer URL | +| config.oidc.scopes | string | `""` | OIDC scopes to be used | +| config.oidc.usePKCE | bool | `false` | Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow | +| config.oidc.useCookie | bool | `false` | Enable using OIDC cookie for authentication outside of cluster | +| config.oidc.secret.create | bool | `true` | Create OIDC secret using provided values | +| config.oidc.secret.name | string | `"oidc"` | Name of the OIDC secret | +| config.oidc.externalSecret.enabled | bool | `false` | Enable using external secret for OIDC | +| config.oidc.externalSecret.name | string | `""` | Name of external OIDC secret | +| config.oidc.meUserInfoURL | string | `""` | URL to fetch additional user info for the /me endpoint. Useful for providers like oauth2-proxy. | + +There are three ways to configure OIDC: + +1. Using direct configuration: +```yaml +config: + oidc: + clientID: "your-client-id" + clientSecret: "your-client-secret" + issuerURL: "https://your-issuer" + scopes: "openid profile email" + meUserInfoURL: "https://headlamp.example.com/oauth2/userinfo" +``` + +2. Using automatic secret creation: +```yaml +config: + oidc: + secret: + create: true + name: oidc +``` + +3. Using external secret: +```yaml +config: + oidc: + secret: + create: false + externalSecret: + enabled: true + name: your-oidc-secret +``` + +### Cluster Inventory Configuration + +> **Warning** +> Cluster Inventory support in Headlamp is alpha/experimental and disabled by +> default. The upstream Cluster Inventory API is currently `v1alpha1` and this +> integration uses the `v0.1.x` API, so fields and behavior may change. + +When `config.clusterInventory.enabled` is true, the chart creates a provider +ConfigMap, makes it available read-only at `/etc/cluster-inventory/config.json`, +and adds the Headlamp Cluster Inventory flags automatically. + +```yaml +config: + clusterInventory: + enabled: true + accessProvidersConfig: + providers: + - name: secretreader + execConfig: + apiVersion: client.authentication.k8s.io/v1 + command: /access-plugins/secretreader/bin/secretreader-plugin + interactiveMode: Never + provideClusterInfo: true + - name: kubeconfig-secretreader + execConfig: + apiVersion: client.authentication.k8s.io/v1 + command: /access-plugins/kubeconfig-secretreader/bin/kubeconfig-secretreader-plugin + interactiveMode: Never + provideClusterInfo: true + plugins: + - name: secretreader + image: registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3@sha256:ec3090dc166aa2b42fb35d714d161c417d8b27bbc463404c8f615f5f4c610a1d + mountPath: /access-plugins/secretreader + - name: kubeconfig-secretreader + image: registry.k8s.io/cluster-inventory-api/kubeconfig-secretreader:v0.1.3@sha256:b92966cc6e4ac78002a63862921022a71d54956826f6e4febcb7247495eb98c0 + mountPath: /access-plugins/kubeconfig-secretreader +``` + +`plugins[]` is for Cluster Inventory access provider binaries, not Headlamp UI +plugins. Each entry renders as a Kubernetes `image` volume and is mounted +read-only into the Headlamp container. If an access provider `execConfig.command` +is configured, the command must be under one of the absolute +`plugins[].mountPath` values. + +### Deployment Configuration + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| replicaCount | int | `1` | Number of desired pods | +| image.registry | string | `"ghcr.io"` | Container image registry | +| image.repository | string | `"headlamp-k8s/headlamp"` | Container image name | +| image.tag | string | `""` | Container image tag (defaults to Chart appVersion) | +| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | +| imagePullSecrets | list | `[]` | Image pull secrets references | +| nameOverride | string | `""` | Override the name of the chart | +| fullnameOverride | string | `""` | Override the full name of the chart | +| namespaceOverride | string | `""` | Override the deployment namespace; defaults to .Release.Namespace | +| initContainers | list | `[]` | Init containers to run before main container | + +### Security Configuration + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| automountServiceAccountToken | bool | `true` | Mount Service Account token in pod | +| serviceAccount.create | bool | `true` | Create service account | +| serviceAccount.name | string | `""` | Service account name | +| serviceAccount.annotations | object | `{}` | Service account annotations | +| clusterRoleBinding.create | bool | `true` | Create cluster role binding | +| clusterRoleBinding.clusterRoleName | string | `"cluster-admin"` | Kubernetes ClusterRole name | +| clusterRoleBinding.annotations | object | `{}` | Cluster role binding annotations | +| hostUsers | bool | `true` | Run in host uid namespace | +| podSecurityContext | object | `{}` | Pod security context (e.g., fsGroup: 2000) | +| securityContext.runAsNonRoot | bool | `true` | Run container as non-root | +| securityContext.privileged | bool | `false` | Run container in privileged mode | +| securityContext.runAsUser | int | `100` | User ID to run container | +| securityContext.runAsGroup | int | `101` | Group ID to run container | +| securityContext.capabilities | object | `{}` | Container capabilities (e.g., drop: [ALL]) | +| securityContext.readOnlyRootFilesystem | bool | `false` | Mount root filesystem as read-only | + +NOTE: for `hostUsers=false` user namespaces must be supported. See: https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/ + +### Storage Configuration + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| persistentVolumeClaim.enabled | bool | `false` | Enable PVC | +| persistentVolumeClaim.annotations | object | `{}` | PVC annotations | +| persistentVolumeClaim.size | string | `""` | PVC size (required if enabled) | +| persistentVolumeClaim.storageClassName | string | `""` | Storage class name | +| persistentVolumeClaim.accessModes | list | `[]` | PVC access modes | +| persistentVolumeClaim.selector | object | `{}` | PVC selector | +| persistentVolumeClaim.volumeMode | string | `""` | PVC volume mode | +| volumeMounts | list | `[]` | Container volume mounts | +| volumes | list | `[]` | Pod volumes | + +### Read-only root filesystem + +When `securityContext.readOnlyRootFilesystem: true` is set, the application needs a writable `/tmp` directory. The chart handles this automatically: + +- An `emptyDir` volume named `headlamp-tmp` is created and mounted at `/tmp` in the main container. +- If `pluginsManager` is enabled and its effective security context (own or inherited) also sets `readOnlyRootFilesystem: true`, a separate `emptyDir` volume named `headlamp-plugins-tmp` is created and mounted at `/tmp` in the plugin manager container. + +**Overriding the automatic `/tmp` volume:** + +You can customise this behaviour without losing the automatic mount: + +```yaml +# Provide your own headlamp-tmp volume (e.g. to set a size limit). +# The chart will skip creating the volume but will still add the /tmp mount. +volumes: + - name: headlamp-tmp + emptyDir: + sizeLimit: 256Mi +``` + +To take full control of `/tmp` (and suppress both the automatic mount and volume entirely), add your own `volumeMount` with `mountPath: /tmp`: + +```yaml +volumeMounts: + - name: my-tmp + mountPath: /tmp +volumes: + - name: my-tmp + emptyDir: {} +``` + +### Network Configuration + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| service.type | string | `"ClusterIP"` | Kubernetes service type | +| service.port | int | `80` | Kubernetes service port | +| service.extraServicePorts | list | `[]` | Additional ports to expose on the Service in addition to the default http port | +| ingress.enabled | bool | `false` | Enable ingress | +| ingress.ingressClassName | string | `""` | Ingress class name | +| ingress.annotations | object | `{}` | Ingress annotations (e.g., kubernetes.io/tls-acme: "true") | +| ingress.labels | object | `{}` | Additional labels for the Ingress resource | +| ingress.hosts | list | `[]` | Ingress hosts configuration | +| ingress.tls | list | `[]` | Ingress TLS configuration | + +Example ingress configuration: +```yaml +ingress: + enabled: true + annotations: + kubernetes.io/tls-acme: "true" + labels: + app.kubernetes.io/part-of: traefik + environment: prod + hosts: + - host: headlamp.example.com + paths: + - path: / + type: ImplementationSpecific + tls: + - secretName: headlamp-tls + hosts: + - headlamp.example.com +``` + +Each path under `ingress.hosts[].paths[]` may optionally specify +`backend.service.{name,port}` to override the default Headlamp Service / +`service.port`. This is typically combined with `service.extraServicePorts` to +route different paths to different ports of the same Service: + +```yaml +service: + extraServicePorts: + - name: extra + port: 9090 + targetPort: extra + +ingress: + enabled: true + hosts: + - host: headlamp.example.com + paths: + - path: / + type: Prefix + - path: /extra + type: Prefix + backend: + service: + # name is optional; defaults to the Headlamp Service. + # When set, it is rendered with `tpl` so values like + # "{{ .Release.Name }}-other" are supported. + port: + name: extra # or: number: 9090 +``` + +The same approach works for the Gateway API: define additional ports under +`service.extraServicePorts` and reference them from `httpRoute.rules[].backendRefs[].port`. + + +### HTTPRoute Configuration (Gateway API) + +For users who prefer Gateway API over classic Ingress resources, Headlamp supports HTTPRoute configuration. + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| httpRoute.enabled | bool | `false` | Enable HTTPRoute resource for Gateway API | +| httpRoute.annotations | object | `{}` | Annotations for HTTPRoute resource | +| httpRoute.labels | object | `{}` | Additional labels for HTTPRoute resource | +| httpRoute.parentRefs | list | `[]` | Parent gateway references (REQUIRED when enabled) | +| httpRoute.hostnames | list | `[]` | Hostnames for the HTTPRoute | +| httpRoute.rules | list | `[]` | Custom routing rules (optional, defaults to path prefix /) | + +Example HTTPRoute configuration: +```yaml +httpRoute: + enabled: true + annotations: + gateway.example.com/custom-annotation: "value" + labels: + app.kubernetes.io/component: ingress + parentRefs: + - name: my-gateway + namespace: gateway-namespace + hostnames: + - headlamp.example.com + # Optional custom rules (defaults to path prefix / if not specified) + rules: + - matches: + - path: + type: PathPrefix + value: /headlamp + backendRefs: + - name: my-headlamp + port: 80 +``` + +### Probe Configuration + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| probes.scheme | string | `"HTTP"` | Scheme for liveness/readiness probes (HTTP or HTTPS). Set to HTTPS when TLS is enabled at the backend. | +| probes.livenessProbe.initialDelaySeconds | int | `0` | Initial delay before liveness probe starts | +| probes.livenessProbe.periodSeconds | int | `10` | Period between liveness checks | +| probes.livenessProbe.timeoutSeconds | int | `1` | Timeout for liveness probe | +| probes.livenessProbe.successThreshold | int | `1` | Must be 1 for liveness probes (Kubernetes requirement) | +| probes.livenessProbe.failureThreshold | int | `3` | Minimum consecutive failures | +| probes.readinessProbe.initialDelaySeconds | int | `0` | Initial delay before readiness probe starts | +| probes.readinessProbe.periodSeconds | int | `10` | Period between readiness checks | +| probes.readinessProbe.timeoutSeconds | int | `1` | Timeout for readiness probe | +| probes.readinessProbe.successThreshold | int | `1` | Minimum consecutive successes | +| probes.readinessProbe.failureThreshold | int | `3` | Minimum consecutive failures | + +When using TLS termination at the backend server, you must set `probes.scheme` to `HTTPS`: + +```yaml +config: + tlsCertPath: "/headlamp-cert/tls.crt" + tlsKeyPath: "/headlamp-cert/tls.key" + +probes: + scheme: HTTPS # Required when TLS is enabled at backend + +volumes: + - name: headlamp-cert + secret: + secretName: headlamp-tls + +volumeMounts: + - name: headlamp-cert + mountPath: /headlamp-cert +``` + +### Resource Management + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| resources | object | `{}` | Container resource requests/limits | +| nodeSelector | object | `{}` | Node labels for pod assignment | +| tolerations | list | `[]` | Pod tolerations | +| affinity | object | `{}` | Pod affinity settings | +| hostAliases | list | `[]` | Optional list of host/IP mappings injected into the pod's /etc/hosts. Useful when an external host (e.g. an OIDC issuer behind cluster ingress) is not resolvable via cluster DNS. | +| topologySpreadConstraints | list | `[]` | Topology spread constraints for pod assignment | +| podAnnotations | object | `{}` | Pod annotations | +| podLabels | object | `{}` | Pod labels | +| env | list | `[]` | Additional environment variables | + +Example resource configuration: +```yaml +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi +``` + +Example environment variables: +```yaml +env: + - name: KUBERNETES_SERVICE_HOST + value: "localhost" + - name: KUBERNETES_SERVICE_PORT + value: "6443" +``` + +Example topology spread constraints: +```yaml +# Spread pods across availability zones with best-effort scheduling +topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway # Prefer spreading but allow scheduling even if it violates the constraint + matchLabelKeys: + - pod-template-hash + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule # Hard requirement - don't schedule if it violates the constraint + matchLabelKeys: + - pod-template-hash +``` + +The `labelSelector` is automatically populated with the pod's selector labels if not specified. You can also provide a custom `labelSelector`: +```yaml +topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: headlamp + custom-label: value +``` + +### Pod Disruption Budget (PDB) + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| podDisruptionBudget.enabled | bool | `false` | Create a PodDisruptionBudget resource | +| podDisruptionBudget.minAvailable | integer \| string \| null | `0` | Minimum pods that must be available. Rendered only when set to a positive integer or a percentage string (e.g. `"1"` or `"50%"`). Schema default is 0, but the chart skips rendering `0`. | +| podDisruptionBudget.maxUnavailable | integer \| string \| null | `null` | Maximum pods allowed to be unavailable. Accepts integer >= 0 or percentage string. Mutually exclusive with `minAvailable`; the template renders this field when set. | +| podDisruptionBudget.unhealthyPodEvictionPolicy | string \| null | `null` | Eviction policy: `"IfHealthyBudget"` or `"AlwaysAllow"`. Emitted only on clusters running Kubernetes >= 1.27 and when explicitly set in values. | + +Note: Ensure `minAvailable` and `maxUnavailable` are not both set (use `null` to disable one). To include `minAvailable` in the rendered PDB, set a positive integer or percentage; the template omits a `0` value. + +Example, Require at least 1 pod available (ensure maxUnavailable is disabled): +```yaml +podDisruptionBudget: + enabled: true + minAvailable: 1 + maxUnavailable: null +``` + +Example, Allow up to 50% of pods to be unavailable: +```yaml +podDisruptionBudget: + enabled: true + maxUnavailable: "50%" + minAvailable: null +``` + +Example, Set unhealthyPodEvictionPolicy (requires Kubernetes >= 1.27): +```yaml +podDisruptionBudget: + enabled: true + maxUnavailable: 1 + minAvailable: null + unhealthyPodEvictionPolicy: "IfHealthyBudget" +``` + +Ensure your replicaCount and maintenance procedures respect the configured PDB to avoid blocking intended operations. + +### pluginsManager Configuration + +| Key | Type | Default | Description | +|-----------------|---------|-------------------|-------------------------------------------------------------------------------------------| +| enabled | boolean | `false` | Enable plugin manager | +| configFile | string | `plugin.yml` | Plugin configuration file name | +| configContent | string | `""` | Plugin configuration content in YAML format. This is required if plugins.enabled is true. | +| baseImage | string | `node:lts-alpine` | Base node image to use | +| version | string | `latest` | Headlamp plugin package version to install | +| env | list | `[]` | Plugin manager env variable configuration | +| resources | object | `{}` | Plugin manager resource requests/limits | +| volumeMounts | list | `[]` | Plugin manager volume mounts | +| securityContext | object | `{}` | Plugin manager security context. If omitted, inherits the global `securityContext`. | + +Example resource configuration: + +```yaml +pluginsManager: + enabled: true + baseImage: node:lts-alpine + version: latest + env: + - name: HTTPS_PROXY + value: "proxy.example.com:8080" + resources: + requests: + cpu: "500m" + memory: "2048Mi" + limits: + cpu: "1" + memory: "4Gi" +``` +## Contributing + +We welcome contributions to the Headlamp Helm chart! To contribute: + +1. Fork the repository and create your branch from `main`. +2. Make your changes and test them thoroughly. +3. Run Helm chart template tests to ensure your changes don't break existing functionality: + + ```console + $ make helm-template-test + ``` + This command executes the script at `charts/headlamp/tests/test.sh` to validate Helm chart templates against expected templates. + +4. If you've made changes that intentionally affect the rendered templates (like version updates or new features): + + ```console + $ make helm-update-template-version + ``` + This updates the expected templates with the current versions from Chart.yaml and only shows files where versions changed. + +5. Review the updated templates carefully to ensure they contain only your intended changes. + +6. Submit a pull request with a clear description of your changes. + + +For more details, refer to our [contributing guidelines](https://github.com/kubernetes-sigs/headlamp/blob/main/CONTRIBUTING.md). + +## Links + +- [GitHub Repository](https://github.com/kubernetes-sigs/headlamp) +- [Documentation](https://headlamp.dev/) +- [Maintainers](https://github.com/kubernetes-sigs/headlamp/blob/main/OWNERS_ALIASES) diff --git a/charts/headlamp/templates/NOTES.txt b/charts/headlamp/templates/NOTES.txt new file mode 100644 index 0000000000..8f375d5c8f --- /dev/null +++ b/charts/headlamp/templates/NOTES.txt @@ -0,0 +1,40 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ include "headlamp.namespace" . }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "headlamp.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ include "headlamp.namespace" . }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ include "headlamp.namespace" . }} svc -w {{ include "headlamp.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ include "headlamp.namespace" . }} {{ include "headlamp.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ include "headlamp.namespace" . }} -l "app.kubernetes.io/name={{ include "headlamp.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ include "headlamp.namespace" . }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ include "headlamp.namespace" . }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} +{{- if .Values.clusterRoleBinding.create }} + {{- if and ( ge .Capabilities.KubeVersion.Major "1" ) ( ge .Capabilities.KubeVersion.Minor "24" ) }} +2. Get the token using + kubectl create token {{ include "headlamp.serviceAccountName" . }} --namespace {{ include "headlamp.namespace" . }} + {{- else }} +2. Get the clusterrolebinding token using + export SECRET=$(kubectl get secrets --namespace {{ include "headlamp.namespace" . }} -o custom-columns=":metadata.name" | grep "{{ include "headlamp.fullname" . }}-token") + kubectl get secret $SECRET --namespace {{ include "headlamp.namespace" . }} --template=\{\{.data.token\}\} | base64 --decode + {{- end }} +{{- end }} +{{- if .Values.image.tag }} + +NOTE: + image.tag is explicitly set to {{ .Values.image.tag }}. + Chart upgrades do not change this pinned value automatically. + If the UI shows an older version than the chart's appVersion, update image.tag explicitly, for example: + helm upgrade {{ .Release.Name }} headlamp/{{ .Chart.Name }} --namespace {{ .Release.Namespace }} --reuse-values --set image.tag=v{{ .Chart.AppVersion }} +{{- end }} diff --git a/charts/headlamp/templates/_helpers.tpl b/charts/headlamp/templates/_helpers.tpl new file mode 100644 index 0000000000..669b353e62 --- /dev/null +++ b/charts/headlamp/templates/_helpers.tpl @@ -0,0 +1,119 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "headlamp.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "headlamp.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Expand the namespace of the release. +Allows overriding it for multi-namespace deployments in combined charts. +*/}} +{{- define "headlamp.namespace" -}} + {{- if .Values.namespaceOverride }} + {{- .Values.namespaceOverride | trunc 63 | trimSuffix "-" -}} + {{- else if .Release.Namespace }} + {{- .Release.Namespace | trunc 63 | trimSuffix "-" -}} + {{- else -}} + default + {{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "headlamp.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "headlamp.labels" -}} +helm.sh/chart: {{ include "headlamp.chart" . }} +{{ include "headlamp.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "headlamp.selectorLabels" -}} +app.kubernetes.io/name: {{ include "headlamp.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "headlamp.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "headlamp.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + + +{{/* +Check if readOnlyRootFilesystem is enabled, returns string "true" if enabled, otherwise returns "false". +*/}} +{{- define "headlamp.readOnlyRootFilesystem" -}} +{{- $securityContextReadOnly := and .securityContext (hasKey .securityContext "readOnlyRootFilesystem") .securityContext.readOnlyRootFilesystem -}} +{{- if $securityContextReadOnly -}}true{{- else -}}false{{- end -}} +{{- end }} + +{{/* +Compute whether to auto-add a writable /tmp emptyDir for a container with +readOnlyRootFilesystem: true. + +- addMount is false when the user already has a volumeMount at /tmp + (avoids duplicate mountPath). +- addVolume is false when the user already has a /tmp mount (avoids an + orphaned volume) OR when a volume with mountName already exists (allows + users to supply their own headlamp-tmp with custom emptyDir settings + such as sizeLimit, while the chart still wires up the /tmp mount). + +Input (dict): + volumeMounts - list of existing volumeMounts for this container + volumes - list of existing pod-level volumes + readOnly - bool: is readOnlyRootFilesystem active for this container + mountName - string: name for the auto-created volume (e.g. "headlamp-tmp") + +Output (YAML dict, intended for use with fromYaml): + addMount: bool + addVolume: bool +*/}} +{{- define "headlamp.tmpVolumeContext" -}} +{{- $hasTmpMount := false -}} +{{- range .volumeMounts -}} + {{- if eq .mountPath "/tmp" -}}{{- $hasTmpMount = true -}}{{- end -}} +{{- end -}} +{{- $hasTmpVolume := false -}} +{{- range .volumes -}} + {{- if eq .name $.mountName -}}{{- $hasTmpVolume = true -}}{{- end -}} +{{- end -}} +addMount: {{ and .readOnly (not $hasTmpMount) }} +addVolume: {{ and .readOnly (not $hasTmpMount) (not $hasTmpVolume) }} +{{- end }} diff --git a/charts/headlamp/templates/cluster-inventory-configmap.yaml b/charts/headlamp/templates/cluster-inventory-configmap.yaml new file mode 100644 index 0000000000..f24d6082e1 --- /dev/null +++ b/charts/headlamp/templates/cluster-inventory-configmap.yaml @@ -0,0 +1,39 @@ +{{- $clusterInventory := .Values.config.clusterInventory | default dict }} +{{- $clusterInventoryProviderConfigPath := "config.json" }} +{{- if $clusterInventory.enabled }} +{{- if not $clusterInventory.accessProvidersConfig }} +{{- fail "config.clusterInventory.enabled is true but config.clusterInventory.accessProvidersConfig is empty; either provide an access providers config or disable Cluster Inventory" }} +{{- end }} +{{- $mountPaths := list }} +{{- range $index, $plugin := ($clusterInventory.plugins | default list) }} +{{- $mountPath := $plugin.mountPath | default "" | toString | clean }} +{{- if not (hasPrefix "/" $mountPath) }} +{{- fail (printf "config.clusterInventory.plugins[%d].mountPath must be an absolute path, got %q" $index ($plugin.mountPath | default "" | toString)) }} +{{- end }} +{{- $mountPaths = append $mountPaths $mountPath }} +{{- end }} +{{- range ($clusterInventory.accessProvidersConfig.providers | default list) }} +{{- $rawCmd := (.execConfig | default dict).command | default "" | toString }} +{{- $cmd := $rawCmd | clean }} +{{- if $rawCmd }} +{{- $matched := false }} +{{- range $mountPath := $mountPaths }} +{{- if hasPrefix (printf "%s/" $mountPath) $cmd }} +{{- $matched = true }} +{{- end }} +{{- end }} +{{- if not $matched }} +{{- fail (printf "provider %q: command %q is not under any config.clusterInventory.plugins[].mountPath %v" .name $cmd $mountPaths) }} +{{- end }} +{{- end }} +{{- end }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "headlamp.fullname" . }}-cluster-inventory + namespace: {{ include "headlamp.namespace" . }} + labels: + {{- include "headlamp.labels" . | nindent 4 }} +data: + {{ $clusterInventoryProviderConfigPath }}: {{ $clusterInventory.accessProvidersConfig | mustToJson | quote }} +{{- end }} diff --git a/charts/headlamp/templates/clusterrolebinding.yaml b/charts/headlamp/templates/clusterrolebinding.yaml new file mode 100644 index 0000000000..42075c4a99 --- /dev/null +++ b/charts/headlamp/templates/clusterrolebinding.yaml @@ -0,0 +1,20 @@ +{{- if .Values.clusterRoleBinding.create -}} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "headlamp.fullname" . }}-admin + labels: + {{- include "headlamp.labels" . | nindent 4 }} + {{- with .Values.clusterRoleBinding.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .Values.clusterRoleBinding.clusterRoleName }} +subjects: +- kind: ServiceAccount + name: {{ include "headlamp.serviceAccountName" . }} + namespace: {{ include "headlamp.namespace" . }} +{{- end }} diff --git a/charts/headlamp/templates/deployment.yaml b/charts/headlamp/templates/deployment.yaml new file mode 100644 index 0000000000..5fb2e8b723 --- /dev/null +++ b/charts/headlamp/templates/deployment.yaml @@ -0,0 +1,559 @@ +{{- $oidc := .Values.config.oidc }} +{{- $env := .Values.env }} + +{{- /* Disable the bundled static plugins (e.g. Prometheus) by clearing + HEADLAMP_STATIC_PLUGINS_DIR. `hasKey` guards against false being treated + as empty by `default`, and an unset value defaults to serving them. */}} +{{- $staticPlugins := .Values.config.staticPlugins | default dict }} +{{- $staticPluginsDisabled := and (hasKey $staticPlugins "enabled") (not $staticPlugins.enabled) }} + +{{- $clientID := "" }} +{{- $clientSecret := "" }} +{{- $issuerURL := "" }} +{{- $scopes := "" }} +{{- $callbackURL := "" }} +{{- $validatorClientID := "" }} +{{- $validatorIssuerURL := "" }} +{{- $usePKCE := "" }} +{{- $useAccessToken := "" }} +{{- $meUserInfoURL := "" }} +{{- $clusterInventory := .Values.config.clusterInventory | default dict }} +{{- $clusterInventoryEnabled := $clusterInventory.enabled | default false }} +{{- $clusterInventoryProviderFile := "/etc/cluster-inventory/config.json" }} +{{- $clusterInventoryProviderMountDir := "/etc/cluster-inventory" }} +{{- $clusterInventoryProviderConfigPath := "config.json" }} +{{- $readOnlyRootFs := eq (include "headlamp.readOnlyRootFilesystem" .Values) "true" }} +{{- $pluginManagerSecurityContext := dict }} +{{- if .Values.pluginsManager.securityContext }} +{{- $pluginManagerSecurityContext = .Values.pluginsManager.securityContext }} +{{- else if .Values.securityContext }} +{{- $pluginManagerSecurityContext = .Values.securityContext }} +{{- end }} +{{- $readOnlyRootFsPlugins := and .Values.pluginsManager.enabled (eq (include "headlamp.readOnlyRootFilesystem" (dict "securityContext" $pluginManagerSecurityContext)) "true") }} +{{- $tmpCtx := include "headlamp.tmpVolumeContext" (dict "volumeMounts" .Values.volumeMounts "volumes" .Values.volumes "readOnly" $readOnlyRootFs "mountName" "headlamp-tmp") | fromYaml }} +{{- $pluginsTmpCtx := include "headlamp.tmpVolumeContext" (dict "volumeMounts" .Values.pluginsManager.volumeMounts "volumes" .Values.volumes "readOnly" $readOnlyRootFsPlugins "mountName" "headlamp-plugins-tmp") | fromYaml }} + +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. +{{- range $env }} + {{- if eq .name "OIDC_CLIENT_ID" }} + {{- $clientID = .value }} + {{- end }} + {{- if eq .name "OIDC_CLIENT_SECRET" }} + {{- $clientSecret = .value }} + {{- end }} + {{- if eq .name "OIDC_ISSUER_URL" }} + {{- $issuerURL = .value }} + {{- end }} + {{- if eq .name "OIDC_SCOPES" }} + {{- $scopes = .value }} + {{- end }} + {{- if eq .name "OIDC_CALLBACK_URL" }} + {{- $callbackURL = .value }} + {{- end }} + {{- if eq .name "OIDC_VALIDATOR_CLIENT_ID" }} + {{- $validatorClientID = .value }} + {{- end }} + {{- if eq .name "OIDC_VALIDATOR_ISSUER_URL" }} + {{- $validatorIssuerURL = .value }} + {{- end }} + {{- if eq .name "OIDC_USE_ACCESS_TOKEN" }} + {{- $useAccessToken = .value | toString }} + {{- end }} + {{- if eq .name "OIDC_USE_PKCE" }} + {{- $usePKCE = .value | toString }} + {{- end }} + {{- if eq .name "ME_USER_INFO_URL" }} + {{- $meUserInfoURL = .value | toString }} + {{- end }} +{{- end }} + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "headlamp.fullname" . }} + namespace: {{ include "headlamp.namespace" . }} + labels: + {{- include "headlamp.labels" . | nindent 4 }} + {{- with .Values.deploymentAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "headlamp.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "headlamp.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- tpl (toYaml .) $ | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "headlamp.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.automountServiceAccountToken }} + hostUsers: {{ .Values.hostUsers }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + {{- with .Values.initContainers }} + initContainers: + {{ toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- if .Values.securityContext }} + {{- toYaml .Values.securityContext | nindent 12 }} + {{- else }} + {{- $defaultSC := dict "allowPrivilegeEscalation" false "runAsNonRoot" true "seccompProfile" (dict "type" "RuntimeDefault") "capabilities" (dict "drop" (list "ALL")) }} + {{- toYaml $defaultSC | nindent 12 }} + {{- end }} + image: "{{ .Values.image.registry}}/{{ .Values.image.repository }}:{{ .Values.image.tag | default (printf "v%s" .Chart.AppVersion) }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{ if or $oidc .Values.env $staticPluginsDisabled }} + {{- if $oidc.externalSecret.enabled }} + # Check if externalSecret is enabled + envFrom: + - secretRef: + name: {{ $oidc.externalSecret.name }} + {{- if or .Values.env $staticPluginsDisabled }} + env: + {{- with .Values.env }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if $staticPluginsDisabled }} + - name: HEADLAMP_STATIC_PLUGINS_DIR + value: "" + {{- end }} + {{- end }} + {{- else }} + env: + {{- if $oidc.secret.create }} + {{- if $oidc.clientID }} + - name: OIDC_CLIENT_ID + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: clientID + {{- end }} + {{- if $oidc.clientSecret }} + - name: OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: clientSecret + {{- end }} + {{- if $oidc.issuerURL }} + - name: OIDC_ISSUER_URL + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: issuerURL + {{- end }} + {{- if $oidc.scopes }} + - name: OIDC_SCOPES + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: scopes + {{- end }} + {{- if $oidc.callbackURL }} + - name: OIDC_CALLBACK_URL + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: callbackURL + {{- end }} + {{- if $oidc.validatorClientID }} + - name: OIDC_VALIDATOR_CLIENT_ID + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: validatorClientID + {{- end }} + {{- if $oidc.validatorIssuerURL }} + - name: OIDC_VALIDATOR_ISSUER_URL + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: validatorIssuerURL + {{- end }} + {{- if $oidc.useAccessToken }} + - name: OIDC_USE_ACCESS_TOKEN + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: useAccessToken + {{- end }} + {{- if $oidc.usePKCE }} + - name: OIDC_USE_PKCE + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: usePKCE + {{- end }} + {{- if $oidc.meUserInfoURL }} + - name: ME_USER_INFO_URL + valueFrom: + secretKeyRef: + name: {{ $oidc.secret.name }} + key: meUserInfoURL + {{- end }} + {{- else }} + {{- if $oidc.clientID }} + - name: OIDC_CLIENT_ID + value: {{ $oidc.clientID }} + {{- end }} + {{- if $oidc.clientSecret }} + - name: OIDC_CLIENT_SECRET + value: {{ $oidc.clientSecret }} + {{- end }} + {{- if $oidc.issuerURL }} + - name: OIDC_ISSUER_URL + value: {{ $oidc.issuerURL }} + {{- end }} + {{- if $oidc.scopes }} + - name: OIDC_SCOPES + value: {{ $oidc.scopes }} + {{- end }} + {{- if $oidc.callbackURL }} + - name: OIDC_CALLBACK_URL + value: {{ $oidc.callbackURL }} + {{- end }} + {{- if $oidc.validatorClientID }} + - name: OIDC_VALIDATOR_CLIENT_ID + value: {{ $oidc.validatorClientID }} + {{- end }} + {{- if $oidc.validatorIssuerURL }} + - name: OIDC_VALIDATOR_ISSUER_URL + value: {{ $oidc.validatorIssuerURL }} + {{- end }} + {{- if $oidc.useAccessToken }} + - name: OIDC_USE_ACCESS_TOKEN + value: {{ $oidc.useAccessToken | quote }} + {{- end }} + {{- if $oidc.usePKCE }} + - name: OIDC_USE_PKCE + value: {{ $oidc.usePKCE | quote }} + {{- end }} + {{- if $oidc.meUserInfoURL }} + - name: ME_USER_INFO_URL + value: {{ $oidc.meUserInfoURL }} + {{- end }} + {{- end }} + {{- if .Values.env }} + {{- toYaml .Values.env | nindent 12 }} + {{- end }} + {{- if $staticPluginsDisabled }} + - name: HEADLAMP_STATIC_PLUGINS_DIR + value: "" + {{- end }} + {{- end }} + {{- end }} + args: + {{- if .Values.config.inCluster }} + - "-in-cluster" + {{- if .Values.config.inClusterContextName }} + - "-in-cluster-context-name={{ .Values.config.inClusterContextName }}" + {{- end }} + {{- end }} + {{- with .Values.config.enableHelm }} + - "-enable-helm" + {{- end }} + {{- if .Values.config.watchPlugins }} + - "-watch-plugins-changes" + {{- end }} + {{- if .Values.config.oidc.useCookie }} + - "-oidc-use-cookie" + {{- end }} + {{- with .Values.config.pluginsDir}} + - "-plugins-dir={{ . }}" + {{- end }} + {{- if hasKey .Values.config "sessionTTL" }} + - "-session-ttl={{ .Values.config.sessionTTL }}" + {{- end }} + {{- with .Values.config.podDebugImage }} + - "-pod-debug-image={{ . }}" + {{- end }} + {{- with .Values.config.nodeShellImage }} + - "-node-shell-image={{ . }}" + {{- end }} + {{- with .Values.config.nodeShellNamespace }} + - "-node-shell-namespace={{ . }}" + {{- end }} + {{- if and .Values.config.inCluster .Values.config.unsafeUseServiceAccountToken }} + - "-unsafe-use-service-account-token" + {{- end }} + {{- if and .Values.config.inCluster .Values.config.unsafeUseServiceAccountToken .Values.config.serviceAccountTokenPath }} + - "-service-account-token-path={{ .Values.config.serviceAccountTokenPath }}" + {{- end }} + {{- if $clusterInventoryEnabled }} + - "-enable-cluster-inventory" + - "-cluster-inventory-provider-file={{ $clusterInventoryProviderFile }}" + {{- with $clusterInventory.labelSelector }} + - "-cluster-inventory-label-selector={{ . }}" + {{- end }} + {{- with $clusterInventory.rootReconcileInterval }} + - "-cluster-inventory-root-reconcile-interval={{ . }}" + {{- end }} + {{- with $clusterInventory.noCRDCacheTTL }} + - "-cluster-inventory-no-crd-cache-ttl={{ . }}" + {{- end }} + {{- end }} + {{- if not $oidc.externalSecret.enabled}} + # Check if externalSecret is disabled + {{- if or (ne $oidc.clientID "") (ne $clientID "") }} + # Check if clientID is non empty either from env or oidc.config + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + {{- end }} + {{- if or (ne $oidc.clientSecret "") (ne $clientSecret "") }} + # Check if clientSecret is non empty either from env or oidc.config + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + {{- end }} + {{- if or (ne $oidc.issuerURL "") (ne $issuerURL "") }} + # Check if issuerURL is non empty either from env or oidc.config + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + {{- end }} + {{- if or (ne $oidc.scopes "") (ne $scopes "") }} + # Check if scopes are non empty either from env or oidc.config + - "-oidc-scopes=$(OIDC_SCOPES)" + {{- end }} + {{- if or (ne $oidc.callbackURL "") (ne $callbackURL "") }} + # Check if callbackURL is non empty either from env or oidc.config + - "-oidc-callback-url=$(OIDC_CALLBACK_URL)" + {{- end }} + {{- if or (ne $oidc.validatorClientID "") (ne $validatorClientID "") }} + # Check if validatorClientID is non empty either from env or oidc.config + - "-oidc-validator-client-id=$(OIDC_VALIDATOR_CLIENT_ID)" + {{- end }} + {{- if or (ne $oidc.validatorIssuerURL "") (ne $validatorIssuerURL "") }} + # Check if validatorIssuerURL is non empty either from env or oidc.config + - "-oidc-validator-idp-issuer-url=$(OIDC_VALIDATOR_ISSUER_URL)" + {{- end }} + {{- if or (ne ($oidc.useAccessToken | toString) "false") (ne $useAccessToken "") }} + # Check if useAccessToken is non false either from env or oidc.config + - "-oidc-use-access-token=$(OIDC_USE_ACCESS_TOKEN)" + {{- end }} + {{- if or (eq ($oidc.usePKCE | toString) "true") (eq $usePKCE "true") }} + - "-oidc-use-pkce=$(OIDC_USE_PKCE)" + {{- end }} + {{- if or (ne $oidc.meUserInfoURL "") (ne $meUserInfoURL "") }} + - "-me-user-info-url=$(ME_USER_INFO_URL)" + {{- end }} + {{- else }} + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + {{- if $oidc.externalSecret.hasScopes }} + - "-oidc-scopes=$(OIDC_SCOPES)" + {{- end }} + {{- if or (ne $oidc.callbackURL "") (ne $callbackURL "") }} + # Check if callbackURL is non empty either from env or oidc.config + - "-oidc-callback-url=$(OIDC_CALLBACK_URL)" + {{- end }} + {{- if or (eq ($oidc.usePKCE | toString) "true") (eq $usePKCE "true") }} + - "-oidc-use-pkce=$(OIDC_USE_PKCE)" + {{- end }} + {{- if or (ne $oidc.validatorClientID "") (ne $validatorClientID "") }} + # Check if validatorClientID is non empty either from env or oidc.config + - "-oidc-validator-client-id=$(OIDC_VALIDATOR_CLIENT_ID)" + {{- end }} + {{- if or (ne $oidc.validatorIssuerURL "") (ne $validatorIssuerURL "") }} + # Check if validatorIssuerURL is non empty either from env or oidc.config + - "-oidc-validator-idp-issuer-url=$(OIDC_VALIDATOR_ISSUER_URL)" + {{- end }} + {{- if or (eq ($oidc.useAccessToken | toString) "true") (eq $useAccessToken "true") }} + # Check if useAccessToken is non false either from env or oidc.config + - "-oidc-use-access-token=$(OIDC_USE_ACCESS_TOKEN)" + {{- end }} + {{- if or (ne $oidc.meUserInfoURL "") (ne $meUserInfoURL "") }} + - "-me-user-info-url=$(ME_USER_INFO_URL)" + {{- end }} + {{- end }} + {{- with .Values.config.baseURL }} + - "-base-url={{ . }}" + {{- end }} + {{- with .Values.config.tlsCertPath }} + - "-tls-cert-path={{ . }}" + {{- end }} + {{- with .Values.config.tlsKeyPath }} + - "-tls-key-path={{ . }}" + {{- end }} + {{- with .Values.config.extraArgs }} + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "{{ .Values.config.baseURL }}/" + port: http + scheme: {{ .Values.probes.scheme | default "HTTP" }} + initialDelaySeconds: {{ .Values.probes.livenessProbe.initialDelaySeconds | default 0 }} + periodSeconds: {{ .Values.probes.livenessProbe.periodSeconds | default 10 }} + timeoutSeconds: {{ .Values.probes.livenessProbe.timeoutSeconds | default 1 }} + successThreshold: 1 + failureThreshold: {{ .Values.probes.livenessProbe.failureThreshold | default 3 }} + readinessProbe: + httpGet: + path: "{{ .Values.config.baseURL }}/" + port: http + scheme: {{ .Values.probes.scheme | default "HTTP" }} + initialDelaySeconds: {{ .Values.probes.readinessProbe.initialDelaySeconds | default 0 }} + periodSeconds: {{ .Values.probes.readinessProbe.periodSeconds | default 10 }} + timeoutSeconds: {{ .Values.probes.readinessProbe.timeoutSeconds | default 1 }} + successThreshold: {{ .Values.probes.readinessProbe.successThreshold | default 1 }} + failureThreshold: {{ .Values.probes.readinessProbe.failureThreshold | default 3 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- if or .Values.pluginsManager.enabled $clusterInventoryEnabled .Values.volumeMounts $tmpCtx.addMount }} + volumeMounts: + {{- if .Values.pluginsManager.enabled }} + - name: plugins-dir + mountPath: {{ .Values.config.pluginsDir }} + {{- end }} + {{- if $clusterInventoryEnabled }} + - name: cluster-inventory-config + mountPath: {{ $clusterInventoryProviderMountDir }} + readOnly: true + {{- range $index, $plugin := ($clusterInventory.plugins | default list) }} + {{- $mountPath := $plugin.mountPath | default "" | toString | clean }} + {{- if not (hasPrefix "/" $mountPath) }} + {{- fail (printf "config.clusterInventory.plugins[%d].mountPath must be an absolute path, got %q" $index ($plugin.mountPath | default "" | toString)) }} + {{- end }} + - name: {{ $plugin.name }} + mountPath: {{ $mountPath }} + readOnly: true + {{- end }} + {{- end }} + {{- with .Values.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + {{- if $tmpCtx.addMount }} + - name: headlamp-tmp + mountPath: /tmp + {{- end }} + {{- end }} + {{- if .Values.pluginsManager.enabled }} + - name: headlamp-plugin + image: {{ .Values.pluginsManager.baseImage }} + command: ["/bin/sh", "-c"] + {{- if .Values.pluginsManager.env }} + env: + {{- toYaml .Values.pluginsManager.env | nindent 12 }} + {{- end }} + args: + - | + if [ -f "/config/plugin.yml" ]; then + echo "Installing plugins from config..." + cat /config/plugin.yml + # Use a writable cache directory + export NPM_CONFIG_CACHE=/tmp/npm-cache + # Use a writable config directory + export NPM_CONFIG_USERCONFIG=/tmp/npm-userconfig + mkdir -p /tmp/npm-cache /tmp/npm-userconfig + npx --yes @headlamp-k8s/pluginctl@{{ .Values.pluginsManager.version }} install --config /config/plugin.yml --folderName {{ .Values.config.pluginsDir }} --watch + fi + volumeMounts: + - name: plugins-dir + mountPath: {{ .Values.config.pluginsDir }} + - name: plugin-config + mountPath: /config + {{- if $pluginsTmpCtx.addMount }} + - name: headlamp-plugins-tmp + mountPath: /tmp + {{- end }} + {{- with .Values.pluginsManager.volumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.pluginsManager.resources | nindent 12 }} + securityContext: + {{- if .Values.pluginsManager.securityContext }} + {{- toYaml .Values.pluginsManager.securityContext | nindent 12 }} + {{- else if $.Values.securityContext }} + {{- toYaml $.Values.securityContext | nindent 12 }} + {{- else }} + {{- $defaultSC := dict "allowPrivilegeEscalation" false "runAsNonRoot" true "seccompProfile" (dict "type" "RuntimeDefault") "capabilities" (dict "drop" (list "ALL")) }} + {{- toYaml $defaultSC | nindent 12 }} + {{- end }} + {{- end }} + {{- with .Values.extraContainers }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.hostAliases }} + hostAliases: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- range $constraint := . }} + - {{ toYaml $constraint | nindent 10 }} + {{- if not $constraint.labelSelector }} + labelSelector: + matchLabels: + {{- include "headlamp.selectorLabels" $ | nindent 14 }} + {{- end }} + {{- end }} + {{- end }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + {{- if or .Values.pluginsManager.enabled $clusterInventoryEnabled .Values.volumes $tmpCtx.addVolume $pluginsTmpCtx.addVolume }} + volumes: + {{- if .Values.pluginsManager.enabled }} + - name: plugins-dir + emptyDir: {} + - name: plugin-config + configMap: + name: {{ include "headlamp.fullname" . }}-plugin-config + {{- if $pluginsTmpCtx.addVolume }} + - name: headlamp-plugins-tmp + emptyDir: {} + {{- end }} + {{- end }} + {{- if $clusterInventoryEnabled }} + - name: cluster-inventory-config + configMap: + name: {{ include "headlamp.fullname" . }}-cluster-inventory + items: + - key: config.json + path: {{ $clusterInventoryProviderConfigPath }} + {{- range $plugin := ($clusterInventory.plugins | default list) }} + - name: {{ $plugin.name }} + image: + reference: {{ $plugin.image | quote }} + {{- end }} + {{- end }} + {{- if $tmpCtx.addVolume }} + - name: headlamp-tmp + emptyDir: {} + {{- end }} + {{- with .Values.volumes}} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- end }} diff --git a/charts/headlamp/templates/extra-manifests.yaml b/charts/headlamp/templates/extra-manifests.yaml new file mode 100644 index 0000000000..6a84b43b6b --- /dev/null +++ b/charts/headlamp/templates/extra-manifests.yaml @@ -0,0 +1,6 @@ +{{- if .Values.extraManifests }} +{{- range $manifest := .Values.extraManifests }} +--- +{{- tpl $manifest $ | nindent 0 }} +{{- end }} +{{- end }} diff --git a/charts/headlamp/templates/httproute.yaml b/charts/headlamp/templates/httproute.yaml new file mode 100644 index 0000000000..15fbe38a81 --- /dev/null +++ b/charts/headlamp/templates/httproute.yaml @@ -0,0 +1,37 @@ +{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include "headlamp.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + namespace: {{ include "headlamp.namespace" . }} + labels: + {{- include "headlamp.labels" . | nindent 4 }} + {{- with .Values.httpRoute.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- required "A valid .Values.httpRoute.parentRefs entry is required when httpRoute.enabled is true" .Values.httpRoute.parentRefs | toYaml | nindent 4 }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- if .Values.httpRoute.rules }} + {{- toYaml .Values.httpRoute.rules | nindent 4 }} + {{- else }} + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + {{- end }} +{{- end }} diff --git a/charts/headlamp/templates/ingress.yaml b/charts/headlamp/templates/ingress.yaml new file mode 100644 index 0000000000..c7721c94f2 --- /dev/null +++ b/charts/headlamp/templates/ingress.yaml @@ -0,0 +1,66 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "headlamp.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + namespace: {{ include "headlamp.namespace" . }} + labels: + {{- include "headlamp.labels" . | nindent 4 }} + {{- with .Values.ingress.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.ingressClassName }} + ingressClassName: {{ .Values.ingress.ingressClassName }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .type }} + backend: + service: + {{- $svc := dict }} + {{- if and .backend .backend.service }} + {{- $svc = .backend.service }} + {{- end }} + {{- if $svc.name }} + name: {{ tpl $svc.name $ | trim | quote }} + {{- else }} + name: {{ $fullName }} + {{- end }} + port: + {{- $port := $svc.port | default dict }} + {{- if $port.number }} + number: {{ $port.number }} + {{- else if $port.name }} + name: {{ $port.name }} + {{- else }} + number: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/headlamp/templates/pdb.yaml b/charts/headlamp/templates/pdb.yaml new file mode 100644 index 0000000000..0afd989710 --- /dev/null +++ b/charts/headlamp/templates/pdb.yaml @@ -0,0 +1,26 @@ +{{- if .Values.podDisruptionBudget.enabled }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "headlamp.fullname" . }} + namespace: {{ include "headlamp.namespace" . }} + labels: + {{- include "headlamp.labels" . | nindent 4 }} +spec: + {{- with .Values.podDisruptionBudget.maxUnavailable }} + maxUnavailable: {{ . }} + {{- end }} + {{- with .Values.podDisruptionBudget.minAvailable }} + minAvailable: {{ . }} + {{- end }} + {{- if (semverCompare ">= 1.27-0" .Capabilities.KubeVersion.Version) }} + {{- if hasKey .Values.podDisruptionBudget "unhealthyPodEvictionPolicy" }} + {{- with .Values.podDisruptionBudget.unhealthyPodEvictionPolicy }} + unhealthyPodEvictionPolicy: {{ . }} + {{- end }} + {{- end }} + {{- end }} + selector: + matchLabels: + {{- include "headlamp.selectorLabels" . | nindent 6 }} +{{- end }} diff --git a/charts/headlamp/templates/plugin-configmap.yaml b/charts/headlamp/templates/plugin-configmap.yaml new file mode 100644 index 0000000000..43bf007bde --- /dev/null +++ b/charts/headlamp/templates/plugin-configmap.yaml @@ -0,0 +1,11 @@ +{{- if .Values.pluginsManager.enabled -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "headlamp.fullname" . }}-plugin-config + namespace: {{ include "headlamp.namespace" . }} + labels: + {{- include "headlamp.labels" . | nindent 4 }} +data: + plugin.yml: |{{ .Values.pluginsManager.configContent | nindent 4 }} +{{- end }} diff --git a/charts/headlamp/templates/pvc.yaml b/charts/headlamp/templates/pvc.yaml new file mode 100644 index 0000000000..ba03c09b25 --- /dev/null +++ b/charts/headlamp/templates/pvc.yaml @@ -0,0 +1,32 @@ +{{- if .Values.persistentVolumeClaim.enabled -}} +{{- $fullName := include "headlamp.fullname" . -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ $fullName }} + namespace: {{ include "headlamp.namespace" . }} + labels: + {{- include "headlamp.labels" . | nindent 4 }} + {{- with .Values.persistentVolumeClaim.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- range .Values.persistentVolumeClaim.accessModes}} + accessModes: + - {{ . }} + {{- end}} + resources: + requests: + storage: {{ required "A valid .Values.persistentVolumeClaim.size entry required!" .Values.persistentVolumeClaim.size }} + {{- with .Values.persistentVolumeClaim.volumeMode }} + volumeMode: {{ . }} + {{- end }} + {{- with .Values.persistentVolumeClaim.storageClassName }} + storageClassName: {{ . }} + {{- end }} + {{- with .Values.persistentVolumeClaim.selector }} + selector: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end}} diff --git a/charts/headlamp/templates/secret.yaml b/charts/headlamp/templates/secret.yaml new file mode 100644 index 0000000000..c2a763106e --- /dev/null +++ b/charts/headlamp/templates/secret.yaml @@ -0,0 +1,41 @@ +{{- with .Values.config.oidc }} +{{- if .secret.create -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .secret.name }} + namespace: {{ include "headlamp.namespace" $ }} +type: Opaque +data: +{{- with .clientID }} + clientID: {{ . | b64enc | quote }} +{{- end }} +{{- with .clientSecret }} + clientSecret: {{ . | b64enc | quote }} +{{- end }} +{{- with .issuerURL }} + issuerURL: {{ . | b64enc | quote }} +{{- end }} +{{- with .scopes }} + scopes: {{ . | b64enc | quote }} +{{- end }} +{{- with .callbackURL }} + callbackURL: {{ . | b64enc | quote }} +{{- end }} +{{- with .validatorClientID }} + validatorClientID: {{ . | b64enc | quote }} +{{- end }} +{{- with .validatorIssuerURL }} + validatorIssuerURL: {{ . | b64enc | quote }} +{{- end }} +{{- with .useAccessToken }} + useAccessToken: {{ . | toString | b64enc | quote }} +{{- end }} +{{- with .usePKCE }} + usePKCE: {{ . | toString | b64enc | quote }} +{{- end }} +{{- with .meUserInfoURL }} + meUserInfoURL: {{ . | b64enc | quote }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/headlamp/templates/service.yaml b/charts/headlamp/templates/service.yaml new file mode 100644 index 0000000000..c341053574 --- /dev/null +++ b/charts/headlamp/templates/service.yaml @@ -0,0 +1,47 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "headlamp.fullname" . }} + namespace: {{ include "headlamp.namespace" . }} + labels: + {{- include "headlamp.labels" . | nindent 4 }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + {{- if and .Values.service.clusterIP (eq .Values.service.type "ClusterIP") }} + clusterIP: {{ .Values.service.clusterIP }} + {{- end }} + {{- if (or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort")) }} + externalTrafficPolicy: {{ .Values.service.externalTrafficPolicy | quote }} + {{- end }} + {{ if (and (eq .Values.service.type "LoadBalancer") (not (empty .Values.service.loadBalancerSourceRanges))) }} + loadBalancerSourceRanges: {{ .Values.service.loadBalancerSourceRanges | toYaml | nindent 2 }} + {{ end }} + {{- if (and (eq .Values.service.type "LoadBalancer") (not (empty .Values.service.loadBalancerIP))) }} + loadBalancerIP: {{ .Values.service.loadBalancerIP }} + {{- end }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + {{- if .Values.service.appProtocol }} + appProtocol: {{ .Values.service.appProtocol | quote }} + {{- end }} + name: http + {{- if and (or (eq .Values.service.type "NodePort") (eq .Values.service.type "LoadBalancer")) (not (empty .Values.service.nodePort)) }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} + {{- range .Values.service.extraServicePorts }} + - name: {{ .name }} + port: {{ .port }} + targetPort: {{ .targetPort | default .port }} + protocol: {{ .protocol | default "TCP" }} + {{- if and (or (eq $.Values.service.type "NodePort") (eq $.Values.service.type "LoadBalancer")) .nodePort }} + nodePort: {{ .nodePort }} + {{- end }} + {{- end }} + selector: + {{- include "headlamp.selectorLabels" . | nindent 4 }} diff --git a/charts/headlamp/templates/serviceaccount.yaml b/charts/headlamp/templates/serviceaccount.yaml new file mode 100644 index 0000000000..af377c60bc --- /dev/null +++ b/charts/headlamp/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "headlamp.serviceAccountName" . }} + namespace: {{ include "headlamp.namespace" . }} + labels: + {{- include "headlamp.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml b/charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml new file mode 100644 index 0000000000..787b670b5c --- /dev/null +++ b/charts/headlamp/tests/expected_templates/azure-oidc-with-validators.yaml @@ -0,0 +1,157 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + - name: OIDC_CLIENT_ID + value: azure-client-id + - name: OIDC_CLIENT_SECRET + value: azure-client-secret + - name: OIDC_ISSUER_URL + value: https://login.microsoftonline.com/tenant-id/v2.0 + - name: OIDC_SCOPES + value: openid email profile + - name: OIDC_VALIDATOR_CLIENT_ID + value: azure-validator-client-id + - name: OIDC_VALIDATOR_ISSUER_URL + value: https://login.microsoftonline.com/tenant-id/v2.0 + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + # Check if clientID is non empty either from env or oidc.config + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + # Check if clientSecret is non empty either from env or oidc.config + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + # Check if issuerURL is non empty either from env or oidc.config + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + # Check if scopes are non empty either from env or oidc.config + - "-oidc-scopes=$(OIDC_SCOPES)" + # Check if validatorClientID is non empty either from env or oidc.config + - "-oidc-validator-client-id=$(OIDC_VALIDATOR_CLIENT_ID)" + # Check if validatorIssuerURL is non empty either from env or oidc.config + - "-oidc-validator-idp-issuer-url=$(OIDC_VALIDATOR_ISSUER_URL)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/cluster-inventory-plugins.yaml b/charts/headlamp/tests/expected_templates/cluster-inventory-plugins.yaml new file mode 100644 index 0000000000..7b68835658 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/cluster-inventory-plugins.yaml @@ -0,0 +1,183 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/cluster-inventory-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: headlamp-cluster-inventory + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +data: + config.json: "{\"providers\":[{\"execConfig\":{\"apiVersion\":\"client.authentication.k8s.io/v1\",\"command\":\"/access-plugins/secretreader/bin/secretreader-plugin\",\"interactiveMode\":\"Never\",\"provideClusterInfo\":true},\"name\":\"secretreader\"},{\"execConfig\":{\"apiVersion\":\"client.authentication.k8s.io/v1\",\"command\":\"/access-plugins/kubeconfig-secretreader/bin/kubeconfig-secretreader-plugin\",\"interactiveMode\":\"Never\",\"provideClusterInfo\":true},\"name\":\"kubeconfig-secretreader\"}]}" +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + - "-enable-cluster-inventory" + - "-cluster-inventory-provider-file=/etc/cluster-inventory/config.json" + - "-cluster-inventory-label-selector=!headlamp.dev/ignore" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + volumeMounts: + - name: cluster-inventory-config + mountPath: /etc/cluster-inventory + readOnly: true + - name: secretreader + mountPath: /access-plugins/secretreader + readOnly: true + - name: kubeconfig-secretreader + mountPath: /access-plugins/kubeconfig-secretreader + readOnly: true + volumes: + - name: cluster-inventory-config + configMap: + name: headlamp-cluster-inventory + items: + - key: config.json + path: config.json + - name: secretreader + image: + reference: "registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3@sha256:ec3090dc166aa2b42fb35d714d161c417d8b27bbc463404c8f615f5f4c610a1d" + - name: kubeconfig-secretreader + image: + reference: "registry.k8s.io/cluster-inventory-api/kubeconfig-secretreader:v0.1.3@sha256:b92966cc6e4ac78002a63862921022a71d54956826f6e4febcb7247495eb98c0" diff --git a/charts/headlamp/tests/expected_templates/cluster-inventory.yaml b/charts/headlamp/tests/expected_templates/cluster-inventory.yaml new file mode 100644 index 0000000000..378162d758 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/cluster-inventory.yaml @@ -0,0 +1,173 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/cluster-inventory-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: headlamp-cluster-inventory + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +data: + config.json: "{\"providers\":[{\"name\":\"static-provider\"}]}" +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + - "-enable-cluster-inventory" + - "-cluster-inventory-provider-file=/etc/cluster-inventory/config.json" + - "-cluster-inventory-label-selector=!headlamp.dev/ignore" + - "-cluster-inventory-root-reconcile-interval=10s" + - "-cluster-inventory-no-crd-cache-ttl=30s" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + volumeMounts: + - name: cluster-inventory-config + mountPath: /etc/cluster-inventory + readOnly: true + volumes: + - name: cluster-inventory-config + configMap: + name: headlamp-cluster-inventory + items: + - key: config.json + path: config.json diff --git a/charts/headlamp/tests/expected_templates/default.yaml b/charts/headlamp/tests/expected_templates/default.yaml new file mode 100644 index 0000000000..9672fea622 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/default.yaml @@ -0,0 +1,142 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/disable-static-plugins.yaml b/charts/headlamp/tests/expected_templates/disable-static-plugins.yaml new file mode 100644 index 0000000000..238546ca76 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/disable-static-plugins.yaml @@ -0,0 +1,144 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + - name: HEADLAMP_STATIC_PLUGINS_DIR + value: "" + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/extra-args.yaml b/charts/headlamp/tests/expected_templates/extra-args.yaml new file mode 100644 index 0000000000..2acccfd713 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/extra-args.yaml @@ -0,0 +1,143 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + - -insecure-ssl + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/extra-manifests.yaml b/charts/headlamp/tests/expected_templates/extra-manifests.yaml new file mode 100644 index 0000000000..85035dbb6e --- /dev/null +++ b/charts/headlamp/tests/expected_templates/extra-manifests.yaml @@ -0,0 +1,159 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/extra-manifests.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: dummy-configmap +data: + key1: value1 + key2: value2 +--- +# Source: headlamp/templates/extra-manifests.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: configmap-with-templated-data +data: + injectedKey: headlamp +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/host-aliases.yaml b/charts/headlamp/tests/expected_templates/host-aliases.yaml new file mode 100644 index 0000000000..8303ce88a5 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/host-aliases.yaml @@ -0,0 +1,150 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + hostAliases: + - hostnames: + - keycloak.test + - sso.local + ip: 10.0.0.5 + - hostnames: + - registry.internal + ip: 192.168.1.10 diff --git a/charts/headlamp/tests/expected_templates/host-users-override.yaml b/charts/headlamp/tests/expected_templates/host-users-override.yaml new file mode 100644 index 0000000000..537bba08d9 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/host-users-override.yaml @@ -0,0 +1,142 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: false + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/httproute-enabled.yaml b/charts/headlamp/tests/expected_templates/httproute-enabled.yaml new file mode 100644 index 0000000000..5d8b5ceb54 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/httproute-enabled.yaml @@ -0,0 +1,172 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} +--- +# Source: headlamp/templates/httproute.yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: ingress + annotations: + gateway.example.com/annotation: test +spec: + parentRefs: + - name: my-gateway + namespace: gateway-namespace + hostnames: + - headlamp.example.com + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: headlamp + port: 80 diff --git a/charts/headlamp/tests/expected_templates/ingress-multi-backend.yaml b/charts/headlamp/tests/expected_templates/ingress-multi-backend.yaml new file mode 100644 index 0000000000..93a0a47d2b --- /dev/null +++ b/charts/headlamp/tests/expected_templates/ingress-multi-backend.yaml @@ -0,0 +1,193 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + - name: extra + port: 9090 + targetPort: 9090 + protocol: TCP + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} +--- +# Source: headlamp/templates/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + ingressClassName: nginx + rules: + - host: "chart-example.local" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: headlamp + port: + number: 80 + - path: /extra-by-name + pathType: Prefix + backend: + service: + name: headlamp + port: + name: extra + - path: /extra-by-number + pathType: Prefix + backend: + service: + name: headlamp + port: + number: 9090 + - path: /custom-svc + pathType: Prefix + backend: + service: + name: "headlamp-other" + port: + number: 8080 diff --git a/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml b/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml new file mode 100644 index 0000000000..edb4c70be2 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/me-user-info-url-directly.yaml @@ -0,0 +1,145 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + - name: ME_USER_INFO_URL + value: /oauth2/userinfocustom1 + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + - "-me-user-info-url=$(ME_USER_INFO_URL)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/me-user-info-url.yaml b/charts/headlamp/tests/expected_templates/me-user-info-url.yaml new file mode 100644 index 0000000000..c0d59c810e --- /dev/null +++ b/charts/headlamp/tests/expected_templates/me-user-info-url.yaml @@ -0,0 +1,149 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: + meUserInfoURL: "L29hdXRoMi91c2VyaW5mb2N1c3RvbTI=" +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + - name: ME_USER_INFO_URL + valueFrom: + secretKeyRef: + name: oidc + key: meUserInfoURL + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + - "-me-user-info-url=$(ME_USER_INFO_URL)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml b/charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml new file mode 100644 index 0000000000..7844c53513 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/namespace-override-oidc-create-secret.yaml @@ -0,0 +1,174 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: mynamespace2 + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: mynamespace2 +type: Opaque +data: + clientID: "dGVzdENsaWVudElk" + clientSecret: "dGVzdENsaWVudFNlY3JldA==" + issuerURL: "dGVzdElzc3VlclVSTA==" + scopes: "dGVzdFNjb3Bl" +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: mynamespace2 +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: mynamespace2 + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: mynamespace2 + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + - name: OIDC_CLIENT_ID + valueFrom: + secretKeyRef: + name: oidc + key: clientID + - name: OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: oidc + key: clientSecret + - name: OIDC_ISSUER_URL + valueFrom: + secretKeyRef: + name: oidc + key: issuerURL + - name: OIDC_SCOPES + valueFrom: + secretKeyRef: + name: oidc + key: scopes + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + # Check if clientID is non empty either from env or oidc.config + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + # Check if clientSecret is non empty either from env or oidc.config + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + # Check if issuerURL is non empty either from env or oidc.config + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + # Check if scopes are non empty either from env or oidc.config + - "-oidc-scopes=$(OIDC_SCOPES)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/namespace-override.yaml b/charts/headlamp/tests/expected_templates/namespace-override.yaml new file mode 100644 index 0000000000..6343314bf1 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/namespace-override.yaml @@ -0,0 +1,142 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: mynamespace + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: mynamespace +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: mynamespace +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: mynamespace + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: mynamespace + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/non-azure-oidc.yaml b/charts/headlamp/tests/expected_templates/non-azure-oidc.yaml new file mode 100644 index 0000000000..47dcfa9101 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/non-azure-oidc.yaml @@ -0,0 +1,149 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + - name: OIDC_CLIENT_ID + value: generic-oidc-client + - name: OIDC_CLIENT_SECRET + value: generic-oidc-secret + - name: OIDC_ISSUER_URL + value: https://auth.example.com + - name: OIDC_SCOPES + value: openid email profile + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + # Check if clientID is non empty either from env or oidc.config + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + # Check if clientSecret is non empty either from env or oidc.config + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + # Check if issuerURL is non empty either from env or oidc.config + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + # Check if scopes are non empty either from env or oidc.config + - "-oidc-scopes=$(OIDC_SCOPES)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/oidc-create-secret.yaml b/charts/headlamp/tests/expected_templates/oidc-create-secret.yaml new file mode 100644 index 0000000000..c3328f2d3a --- /dev/null +++ b/charts/headlamp/tests/expected_templates/oidc-create-secret.yaml @@ -0,0 +1,174 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: + clientID: "dGVzdENsaWVudElk" + clientSecret: "dGVzdENsaWVudFNlY3JldA==" + issuerURL: "dGVzdElzc3VlclVSTA==" + scopes: "dGVzdFNjb3Bl" +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + - name: OIDC_CLIENT_ID + valueFrom: + secretKeyRef: + name: oidc + key: clientID + - name: OIDC_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: oidc + key: clientSecret + - name: OIDC_ISSUER_URL + valueFrom: + secretKeyRef: + name: oidc + key: issuerURL + - name: OIDC_SCOPES + valueFrom: + secretKeyRef: + name: oidc + key: scopes + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + # Check if clientID is non empty either from env or oidc.config + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + # Check if clientSecret is non empty either from env or oidc.config + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + # Check if issuerURL is non empty either from env or oidc.config + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + # Check if scopes are non empty either from env or oidc.config + - "-oidc-scopes=$(OIDC_SCOPES)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/oidc-directly-env.yaml b/charts/headlamp/tests/expected_templates/oidc-directly-env.yaml new file mode 100644 index 0000000000..8ddf6e920e --- /dev/null +++ b/charts/headlamp/tests/expected_templates/oidc-directly-env.yaml @@ -0,0 +1,158 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + - name: OIDC_CLIENT_ID + value: testClientId + - name: OIDC_CLIENT_SECRET + value: testClientSecret + - name: OIDC_ISSUER_URL + value: testIssuerURL + - name: OIDC_SCOPES + value: testScope + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + # Check if clientID is non empty either from env or oidc.config + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + # Check if clientSecret is non empty either from env or oidc.config + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + # Check if issuerURL is non empty either from env or oidc.config + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + # Check if scopes are non empty either from env or oidc.config + - "-oidc-scopes=$(OIDC_SCOPES)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/oidc-directly.yaml b/charts/headlamp/tests/expected_templates/oidc-directly.yaml new file mode 100644 index 0000000000..2db6cee4ec --- /dev/null +++ b/charts/headlamp/tests/expected_templates/oidc-directly.yaml @@ -0,0 +1,149 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + - name: OIDC_CLIENT_ID + value: testClientId + - name: OIDC_CLIENT_SECRET + value: testClientSecret + - name: OIDC_ISSUER_URL + value: testIssuerURL + - name: OIDC_SCOPES + value: testScope + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + # Check if clientID is non empty either from env or oidc.config + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + # Check if clientSecret is non empty either from env or oidc.config + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + # Check if issuerURL is non empty either from env or oidc.config + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + # Check if scopes are non empty either from env or oidc.config + - "-oidc-scopes=$(OIDC_SCOPES)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/oidc-external-secret-with-scopes.yaml b/charts/headlamp/tests/expected_templates/oidc-external-secret-with-scopes.yaml new file mode 100644 index 0000000000..115684a6ad --- /dev/null +++ b/charts/headlamp/tests/expected_templates/oidc-external-secret-with-scopes.yaml @@ -0,0 +1,139 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + # Check if externalSecret is enabled + envFrom: + - secretRef: + name: oidc + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + - "-oidc-scopes=$(OIDC_SCOPES)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/oidc-external-secret.yaml b/charts/headlamp/tests/expected_templates/oidc-external-secret.yaml new file mode 100644 index 0000000000..6ee1eb2861 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/oidc-external-secret.yaml @@ -0,0 +1,138 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + # Check if externalSecret is enabled + envFrom: + - secretRef: + name: oidc + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/oidc-pkce.yaml b/charts/headlamp/tests/expected_templates/oidc-pkce.yaml new file mode 100644 index 0000000000..949b2e215d --- /dev/null +++ b/charts/headlamp/tests/expected_templates/oidc-pkce.yaml @@ -0,0 +1,152 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + - name: OIDC_CLIENT_ID + value: testClientId + - name: OIDC_CLIENT_SECRET + value: testClientSecret + - name: OIDC_ISSUER_URL + value: testIssuerURL + - name: OIDC_SCOPES + value: testScope + - name: OIDC_USE_PKCE + value: "true" + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + # Check if clientID is non empty either from env or oidc.config + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + # Check if clientSecret is non empty either from env or oidc.config + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + # Check if issuerURL is non empty either from env or oidc.config + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + # Check if scopes are non empty either from env or oidc.config + - "-oidc-scopes=$(OIDC_SCOPES)" + - "-oidc-use-pkce=$(OIDC_USE_PKCE)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml b/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml new file mode 100644 index 0000000000..148de59345 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/oidc-validator-overrides.yaml @@ -0,0 +1,161 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + - name: OIDC_CLIENT_ID + value: testClientId + - name: OIDC_CLIENT_SECRET + value: testClientSecret + - name: OIDC_ISSUER_URL + value: testIssuerURL + - name: OIDC_SCOPES + value: testScope + - name: OIDC_VALIDATOR_CLIENT_ID + value: overriddenClientID + - name: OIDC_VALIDATOR_ISSUER_URL + value: overriddenIssuerURL + - name: OIDC_USE_ACCESS_TOKEN + value: "true" + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + # Check if clientID is non empty either from env or oidc.config + - "-oidc-client-id=$(OIDC_CLIENT_ID)" + # Check if clientSecret is non empty either from env or oidc.config + - "-oidc-client-secret=$(OIDC_CLIENT_SECRET)" + # Check if issuerURL is non empty either from env or oidc.config + - "-oidc-idp-issuer-url=$(OIDC_ISSUER_URL)" + # Check if scopes are non empty either from env or oidc.config + - "-oidc-scopes=$(OIDC_SCOPES)" + # Check if validatorClientID is non empty either from env or oidc.config + - "-oidc-validator-client-id=$(OIDC_VALIDATOR_CLIENT_ID)" + # Check if validatorIssuerURL is non empty either from env or oidc.config + - "-oidc-validator-idp-issuer-url=$(OIDC_VALIDATOR_ISSUER_URL)" + # Check if useAccessToken is non false either from env or oidc.config + - "-oidc-use-access-token=$(OIDC_USE_ACCESS_TOKEN)" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/pod-disruption.yaml b/charts/headlamp/tests/expected_templates/pod-disruption.yaml new file mode 100644 index 0000000000..0708503125 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/pod-disruption.yaml @@ -0,0 +1,162 @@ +--- +# Source: headlamp/templates/pdb.yaml +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + maxUnavailable: 1 + unhealthyPodEvictionPolicy: IfHealthyBudget + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/readonly-root-filesystem-custom-tmp-volume.yaml b/charts/headlamp/tests/expected_templates/readonly-root-filesystem-custom-tmp-volume.yaml new file mode 100644 index 0000000000..dc0409684a --- /dev/null +++ b/charts/headlamp/tests/expected_templates/readonly-root-filesystem-custom-tmp-volume.yaml @@ -0,0 +1,150 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + volumeMounts: + - name: headlamp-tmp + mountPath: /tmp + volumes: + - emptyDir: + sizeLimit: 100Mi + name: headlamp-tmp diff --git a/charts/headlamp/tests/expected_templates/readonly-root-filesystem-custom-tmp.yaml b/charts/headlamp/tests/expected_templates/readonly-root-filesystem-custom-tmp.yaml new file mode 100644 index 0000000000..c6439c5c6f --- /dev/null +++ b/charts/headlamp/tests/expected_templates/readonly-root-filesystem-custom-tmp.yaml @@ -0,0 +1,149 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + volumeMounts: + - mountPath: /tmp + name: my-tmp + volumes: + - emptyDir: {} + name: my-tmp diff --git a/charts/headlamp/tests/expected_templates/readonly-root-filesystem-plugins-inherit.yaml b/charts/headlamp/tests/expected_templates/readonly-root-filesystem-plugins-inherit.yaml new file mode 100644 index 0000000000..76dbd7ab54 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/readonly-root-filesystem-plugins-inherit.yaml @@ -0,0 +1,203 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/plugin-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: headlamp-plugin-config + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +data: + plugin.yml: | +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + volumeMounts: + - name: plugins-dir + mountPath: /headlamp/plugins + - name: headlamp-tmp + mountPath: /tmp + - name: headlamp-plugin + image: node:lts-alpine + command: ["/bin/sh", "-c"] + args: + - | + if [ -f "/config/plugin.yml" ]; then + echo "Installing plugins from config..." + cat /config/plugin.yml + # Use a writable cache directory + export NPM_CONFIG_CACHE=/tmp/npm-cache + # Use a writable config directory + export NPM_CONFIG_USERCONFIG=/tmp/npm-userconfig + mkdir -p /tmp/npm-cache /tmp/npm-userconfig + npx --yes @headlamp-k8s/pluginctl@latest install --config /config/plugin.yml --folderName /headlamp/plugins --watch + fi + volumeMounts: + - name: plugins-dir + mountPath: /headlamp/plugins + - name: plugin-config + mountPath: /config + - name: headlamp-plugins-tmp + mountPath: /tmp + resources: + null + securityContext: + privileged: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + volumes: + - name: plugins-dir + emptyDir: {} + - name: plugin-config + configMap: + name: headlamp-plugin-config + - name: headlamp-plugins-tmp + emptyDir: {} + - name: headlamp-tmp + emptyDir: {} diff --git a/charts/headlamp/tests/expected_templates/readonly-root-filesystem-plugins-only.yaml b/charts/headlamp/tests/expected_templates/readonly-root-filesystem-plugins-only.yaml new file mode 100644 index 0000000000..02e74b2cb7 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/readonly-root-filesystem-plugins-only.yaml @@ -0,0 +1,195 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/plugin-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: headlamp-plugin-config + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +data: + plugin.yml: | +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + readOnlyRootFilesystem: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + volumeMounts: + - name: plugins-dir + mountPath: /headlamp/plugins + - name: headlamp-plugin + image: node:lts-alpine + command: ["/bin/sh", "-c"] + args: + - | + if [ -f "/config/plugin.yml" ]; then + echo "Installing plugins from config..." + cat /config/plugin.yml + # Use a writable cache directory + export NPM_CONFIG_CACHE=/tmp/npm-cache + # Use a writable config directory + export NPM_CONFIG_USERCONFIG=/tmp/npm-userconfig + mkdir -p /tmp/npm-cache /tmp/npm-userconfig + npx --yes @headlamp-k8s/pluginctl@latest install --config /config/plugin.yml --folderName /headlamp/plugins --watch + fi + volumeMounts: + - name: plugins-dir + mountPath: /headlamp/plugins + - name: plugin-config + mountPath: /config + - name: headlamp-plugins-tmp + mountPath: /tmp + resources: + null + securityContext: + readOnlyRootFilesystem: true + volumes: + - name: plugins-dir + emptyDir: {} + - name: plugin-config + configMap: + name: headlamp-plugin-config + - name: headlamp-plugins-tmp + emptyDir: {} diff --git a/charts/headlamp/tests/expected_templates/readonly-root-filesystem.yaml b/charts/headlamp/tests/expected_templates/readonly-root-filesystem.yaml new file mode 100644 index 0000000000..55c57dc838 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/readonly-root-filesystem.yaml @@ -0,0 +1,149 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + volumeMounts: + - name: headlamp-tmp + mountPath: /tmp + volumes: + - name: headlamp-tmp + emptyDir: {} diff --git a/charts/headlamp/tests/expected_templates/security-context.yaml b/charts/headlamp/tests/expected_templates/security-context.yaml new file mode 100644 index 0000000000..0540ecc674 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/security-context.yaml @@ -0,0 +1,215 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/plugin-configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: headlamp-plugin-config + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +data: + plugin.yml: | +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + fsGroup: 2000 + runAsGroup: 3000 + runAsUser: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: headlamp + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + privileged: false + readOnlyRootFilesystem: true + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + seccompProfile: + type: RuntimeDefault + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + volumeMounts: + - name: plugins-dir + mountPath: /headlamp/plugins + - name: headlamp-tmp + mountPath: /tmp + - name: headlamp-plugin + image: node:18-alpine + command: ["/bin/sh", "-c"] + args: + - | + if [ -f "/config/plugin.yml" ]; then + echo "Installing plugins from config..." + cat /config/plugin.yml + # Use a writable cache directory + export NPM_CONFIG_CACHE=/tmp/npm-cache + # Use a writable config directory + export NPM_CONFIG_USERCONFIG=/tmp/npm-userconfig + mkdir -p /tmp/npm-cache /tmp/npm-userconfig + npx --yes @headlamp-k8s/pluginctl@1.0.0 install --config /config/plugin.yml --folderName /headlamp/plugins --watch + fi + volumeMounts: + - name: plugins-dir + mountPath: /headlamp/plugins + - name: plugin-config + mountPath: /config + - name: headlamp-plugins-tmp + mountPath: /tmp + resources: + null + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1001 + volumes: + - name: plugins-dir + emptyDir: {} + - name: plugin-config + configMap: + name: headlamp-plugin-config + - name: headlamp-plugins-tmp + emptyDir: {} + - name: headlamp-tmp + emptyDir: {} diff --git a/charts/headlamp/tests/expected_templates/service-account-token-custom-token.yaml b/charts/headlamp/tests/expected_templates/service-account-token-custom-token.yaml new file mode 100644 index 0000000000..1ac82076e2 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/service-account-token-custom-token.yaml @@ -0,0 +1,144 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + - "-unsafe-use-service-account-token" + - "-service-account-token-path=/custom/token" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/service-account-token-out-of-cluster.yaml b/charts/headlamp/tests/expected_templates/service-account-token-out-of-cluster.yaml new file mode 100644 index 0000000000..ad2b4c854d --- /dev/null +++ b/charts/headlamp/tests/expected_templates/service-account-token-out-of-cluster.yaml @@ -0,0 +1,140 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/service-account-token.yaml b/charts/headlamp/tests/expected_templates/service-account-token.yaml new file mode 100644 index 0000000000..ab0d003417 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/service-account-token.yaml @@ -0,0 +1,143 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + - "-unsafe-use-service-account-token" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/service-appprotocol.yaml b/charts/headlamp/tests/expected_templates/service-appprotocol.yaml new file mode 100644 index 0000000000..b3949dc4df --- /dev/null +++ b/charts/headlamp/tests/expected_templates/service-appprotocol.yaml @@ -0,0 +1,143 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + appProtocol: "kubernetes.io/ws" + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/service-extra-ports.yaml b/charts/headlamp/tests/expected_templates/service-extra-ports.yaml new file mode 100644 index 0000000000..db645465b1 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/service-extra-ports.yaml @@ -0,0 +1,152 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: NodePort + externalTrafficPolicy: + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + - name: metrics + port: 9090 + targetPort: metrics + protocol: TCP + - name: grpc + port: 9091 + targetPort: 9091 + protocol: TCP + nodePort: 31091 + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} diff --git a/charts/headlamp/tests/expected_templates/tls-added.yaml b/charts/headlamp/tests/expected_templates/tls-added.yaml new file mode 100644 index 0000000000..9a5cc65555 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/tls-added.yaml @@ -0,0 +1,156 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + - "-tls-cert-path=/headlamp-cert/headlamp-ca.crt" + - "-tls-key-path=/headlamp-cert/headlamp-tls.key" + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTPS + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTPS + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + volumeMounts: + - mountPath: /headlamp-cert + name: headlamp-cert + volumes: + - name: headlamp-cert + secret: + items: + - key: tls.crt + path: headlamp-ca.crt + - key: tls.key + path: headlamp-tls.key + secretName: headlamp-tls diff --git a/charts/headlamp/tests/expected_templates/topology-spread-constraints-custom-selector.yaml b/charts/headlamp/tests/expected_templates/topology-spread-constraints-custom-selector.yaml new file mode 100644 index 0000000000..e7874c8c60 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/topology-spread-constraints-custom-selector.yaml @@ -0,0 +1,153 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + topologySpreadConstraints: + - + labelSelector: + matchLabels: + app.kubernetes.io/name: headlamp + custom-label: custom-value + matchLabelKeys: + - pod-template-hash + maxSkew: 2 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway diff --git a/charts/headlamp/tests/expected_templates/topology-spread-constraints.yaml b/charts/headlamp/tests/expected_templates/topology-spread-constraints.yaml new file mode 100644 index 0000000000..9d3631e35d --- /dev/null +++ b/charts/headlamp/tests/expected_templates/topology-spread-constraints.yaml @@ -0,0 +1,163 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + topologySpreadConstraints: + - + matchLabelKeys: + - pod-template-hash + maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + - + matchLabelKeys: + - pod-template-hash + maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp diff --git a/charts/headlamp/tests/expected_templates/volumes-added.yaml b/charts/headlamp/tests/expected_templates/volumes-added.yaml new file mode 100644 index 0000000000..73774d4736 --- /dev/null +++ b/charts/headlamp/tests/expected_templates/volumes-added.yaml @@ -0,0 +1,145 @@ +--- +# Source: headlamp/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +--- +# Source: headlamp/templates/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: oidc + namespace: default +type: Opaque +data: +--- +# Source: headlamp/templates/clusterrolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: headlamp-admin + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: +- kind: ServiceAccount + name: headlamp + namespace: default +--- +# Source: headlamp/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp +--- +# Source: headlamp/templates/deployment.yaml +# This block of code is used to extract the values from the env. +# This is done to check if the values are non-empty and if they are, they are used in the deployment.yaml. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: headlamp + namespace: default + labels: + helm.sh/chart: headlamp-0.44.0 + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + app.kubernetes.io/version: "0.44.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + template: + metadata: + labels: + app.kubernetes.io/name: headlamp + app.kubernetes.io/instance: headlamp + spec: + serviceAccountName: headlamp + automountServiceAccountToken: true + hostUsers: true + securityContext: + {} + containers: + - name: headlamp + securityContext: + privileged: false + runAsGroup: 101 + runAsNonRoot: true + runAsUser: 100 + image: "ghcr.io/headlamp-k8s/headlamp:v0.44.0" + imagePullPolicy: IfNotPresent + + env: + args: + - "-in-cluster" + - "-in-cluster-context-name=main" + - "-plugins-dir=/headlamp/plugins" + - "-session-ttl=86400" + # Check if externalSecret is disabled + ports: + - name: http + containerPort: 4466 + protocol: TCP + livenessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + readinessProbe: + httpGet: + path: "/" + port: http + scheme: HTTP + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + resources: + {} + volumes: + - emptyDir: {} + name: plugins diff --git a/charts/headlamp/tests/failing_test_cases/cluster-inventory-empty-access-providers.yaml b/charts/headlamp/tests/failing_test_cases/cluster-inventory-empty-access-providers.yaml new file mode 100644 index 0000000000..a5506854d9 --- /dev/null +++ b/charts/headlamp/tests/failing_test_cases/cluster-inventory-empty-access-providers.yaml @@ -0,0 +1,3 @@ +config: + clusterInventory: + enabled: true diff --git a/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-command-equals-mount-path.yaml b/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-command-equals-mount-path.yaml new file mode 100644 index 0000000000..90faa7fd3c --- /dev/null +++ b/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-command-equals-mount-path.yaml @@ -0,0 +1,15 @@ +config: + clusterInventory: + enabled: true + accessProvidersConfig: + providers: + - name: secretreader + execConfig: + apiVersion: client.authentication.k8s.io/v1 + command: /access-plugins/secretreader + interactiveMode: Never + provideClusterInfo: true + plugins: + - name: secretreader + image: registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3@sha256:ec3090dc166aa2b42fb35d714d161c417d8b27bbc463404c8f615f5f4c610a1d + mountPath: /access-plugins/secretreader diff --git a/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-invalid-name.yaml b/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-invalid-name.yaml new file mode 100644 index 0000000000..c71a2203e9 --- /dev/null +++ b/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-invalid-name.yaml @@ -0,0 +1,15 @@ +config: + clusterInventory: + enabled: true + accessProvidersConfig: + providers: + - name: secretreader + execConfig: + apiVersion: client.authentication.k8s.io/v1 + command: /access-plugins/secretreader/bin/secretreader-plugin + interactiveMode: Never + provideClusterInfo: true + plugins: + - name: SecretReader + image: registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3@sha256:ec3090dc166aa2b42fb35d714d161c417d8b27bbc463404c8f615f5f4c610a1d + mountPath: /access-plugins/secretreader diff --git a/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-mount-mismatch.yaml b/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-mount-mismatch.yaml new file mode 100644 index 0000000000..a3ac45f3a7 --- /dev/null +++ b/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-mount-mismatch.yaml @@ -0,0 +1,15 @@ +config: + clusterInventory: + enabled: true + accessProvidersConfig: + providers: + - name: secretreader + execConfig: + apiVersion: client.authentication.k8s.io/v1 + command: /access-plugins/secretreader/bin/secretreader-plugin + interactiveMode: Never + provideClusterInfo: true + plugins: + - name: secretreader + image: registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3@sha256:ec3090dc166aa2b42fb35d714d161c417d8b27bbc463404c8f615f5f4c610a1d + mountPath: /access-plugins/other diff --git a/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-relative-mount-path.yaml b/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-relative-mount-path.yaml new file mode 100644 index 0000000000..91e0542b6e --- /dev/null +++ b/charts/headlamp/tests/failing_test_cases/cluster-inventory-plugin-relative-mount-path.yaml @@ -0,0 +1,15 @@ +config: + clusterInventory: + enabled: true + accessProvidersConfig: + providers: + - name: secretreader + execConfig: + apiVersion: client.authentication.k8s.io/v1 + command: /access-plugins/secretreader/bin/secretreader-plugin + interactiveMode: Never + provideClusterInfo: true + plugins: + - name: secretreader + image: registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3@sha256:ec3090dc166aa2b42fb35d714d161c417d8b27bbc463404c8f615f5f4c610a1d + mountPath: access-plugins/secretreader diff --git a/charts/headlamp/tests/readme.md b/charts/headlamp/tests/readme.md new file mode 100644 index 0000000000..90681336c0 --- /dev/null +++ b/charts/headlamp/tests/readme.md @@ -0,0 +1,96 @@ +## Helm Template Testing + +The Helm template testing for the Headlamp chart ensures that the Helm templates generate the expected Kubernetes manifest files under different scenarios. This testing is crucial for validating changes to the Helm chart and ensuring its correctness before deployment. + +### Expected Templates (`charts/headlamp/tests/expected_templates`) + +The `expected_templates` directory contains YAML files representing the expected Kubernetes manifest files generated by the Helm templates. Each YAML file corresponds to a specific Helm template in the `charts/headlamp/templates` directory. These files serve as reference points for comparing the actual rendered templates during testing. + +Example: +- `deployment.yaml`: Represents the expected Kubernetes Deployment manifest. +- `service.yaml`: Represents the expected Kubernetes Service manifest. + +### Test Cases (`charts/headlamp/tests/test_cases`) + +The `test_cases` directory contains YAML files representing different test scenarios or configurations for the Helm chart. Each test case specifies a set of values for Helm chart configuration parameters (defined in `values.yaml`) to test various aspects of the chart under different conditions. + +Example: +- `volumes-added.yaml`: Tests the behavior of the Helm chart when additional volumes are specified. +- `ingress-enabled.yaml`: Tests the behavior of the Helm chart when Ingress is enabled. + +The Helm template testing script (`charts/headlamp/tests/test.sh`) dynamically renders Helm templates for each test case using the specified configuration values and compares them against the corresponding expected templates. This ensures that the Helm chart behaves as expected under different configurations. + +## Adding Test Cases and Expected Templates + +To enhance the coverage of the Helm template testing for the Headlamp chart, you can add more test cases and corresponding expected templates. Follow these guidelines to add new test cases and expected templates effectively: + +### Test Cases + +1. **Create a New Test Case Directory**: Inside the `charts/headlamp/tests/test_cases` directory, create a new directory representing the new test case. Choose a descriptive name for the directory that reflects the purpose or scenario of the test case. + +2. **Define Test Case Configuration**: Within the new test case directory, create a `values.yaml` file with custom name to define the configuration parameters for the Helm chart under the specific test scenario. Customize the values in this file to match the desired configuration for the test case. + +## Expected Templates + +1. **Create Expected Templates**: Inside the `charts/headlamp/tests/expected_templates` directory, create YAML files representing the expected Kubernetes manifest files for the Helm templates under the new test scenarios. Each expected template file should correspond to a Helm template in the templates directory. + +2. **Match Test Cases with Expected Templates**: Ensure that each test case directory in `test_cases` has a corresponding expected template file in `expected_templates`. The expected template file should have the same name as the test case directory to establish the association. + + +## Running Helm Template Testing + +To run the Helm template testing for the Headlamp chart, follow these steps: + +### Prerequisites + +- [Helm](https://helm.sh/) must be installed on your system. + +### Running the Tests + +1. Run the Helm template testing using the provided Make directive from your root headlamp folder: + + ```bash + make helm-template-test + ``` + +This will execute the `charts/headlamp/tests/test.sh` script, which dynamically renders and compares Helm templates for different test cases against their expected templates. + +## Updating Template Versions + +When changes are made to the Helm chart that intentionally modify the rendered templates (such as version updates, new features, or bug fixes), you'll need to update the expected templates to match the new output. The `helm-update-template-version` command simplifies this process. + +### How Template Version Updates Work + +The template version update process: + +1. Reads the current chart version and app version from `Chart.yaml` +2. Checks each expected template file for version references +3. Only updates files where versions are outdated +4. Provides concise output showing only which files required updates + +### When to Use Template Version Updates + +You should run the template version update when: + +- You've updated the Helm chart version in `Chart.yaml` +- You've updated the app version in `Chart.yaml` +- You've made intentional changes to templates that affect their rendered output +- The `helm-template-test` command fails with template differences that are expected + +### Running Template Version Updates + +To update the expected templates with the current versions: + +```bash +make helm-update-template-version +``` + +This executes the `charts/headlamp/tests/update-version.sh` script, which renders templates with current versions and updates the expected templates accordingly. + +### After Updating Template Versions + +After running the template version update: + +1. Review the changes to ensure they're as expected +2. Run `helm-template-test` again to verify that tests now pass +3. Commit the updated expected templates along with your chart changes diff --git a/charts/headlamp/tests/test.sh b/charts/headlamp/tests/test.sh new file mode 100644 index 0000000000..fa2480ea85 --- /dev/null +++ b/charts/headlamp/tests/test.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +# Enable strict mode +set -euo pipefail + +# This script only tests templates - it does not update them +# To update templates, use update-version.sh + +# Set up variables +CHART_DIR="./charts/headlamp" +TEST_CASES_DIR="${CHART_DIR}/tests/test_cases" +FAILING_TEST_CASES_DIR="${CHART_DIR}/tests/failing_test_cases" +EXPECTED_TEMPLATES_DIR="${CHART_DIR}/tests/expected_templates" + +# Print header information +echo "Testing Helm chart templates against expected output..." + +# Function to render templates for a specific values file +render_templates() { + values_file="$1" + output_dir="$2" + # Render templates + helm template headlamp ${CHART_DIR} --values ${values_file} > "${output_dir}/rendered_templates.yaml" + # Verify the file was created successfully + if [ ! -s "${output_dir}/rendered_templates.yaml" ]; then + echo "ERROR: Failed to render templates for ${values_file}" + exit 1 + fi +} + +# Function to verify templates fail to render for a specific values file +render_templates_expect_failure() { + values_file="$1" + if output=$(helm template headlamp ${CHART_DIR} --values ${values_file} 2>&1); then + echo "ERROR: Expected template rendering to fail for ${values_file}, but it succeeded" + echo "${output}" + exit 1 + else + echo "Template failure test PASSED for ${values_file}: $(echo "${output}" | grep -m1 '^Error:' || echo "${output}" | head -n 1)" + fi +} + +# Clean up function to handle errors and cleanup +cleanup() { + # Get exit code + exit_code=$? + + # Clean up any temporary files/directories + if [ -d "${CHART_DIR}/tests/defaultvaluetest" ]; then + rm -rf "${CHART_DIR}/tests/defaultvaluetest" + fi + + # Clean up test case output directories + if [ "$(ls -A ${TEST_CASES_DIR} 2>/dev/null)" ]; then + for values_file in ${TEST_CASES_DIR}/*; do + case_name=$(basename "${values_file}") + if [ -d "${CHART_DIR}/tests/${case_name}_output" ]; then + rm -rf "${CHART_DIR}/tests/${case_name}_output" + fi + done + fi + + # If exiting with error, help user understand what to do + if [ $exit_code -ne 0 ]; then + echo "" + echo "=============================================" + echo "Test failed! To update expected templates to match current output:" + echo " 1. Review the differences above carefully" + echo " 2. If the changes are related to version, run:" + echo " make helm-update-template-version" + echo " This will update ALL expected templates with current Helm version" + echo " 3. Verify the changes and commit them" + echo "=============================================" + fi + + exit $exit_code +} + +# Register cleanup function +trap cleanup EXIT + +# Function to compare rendered templates with expected templates +compare_templates() { + values_file="$1" + output_dir="$2" + expected_file="$3" + + # Compare rendered template with expected template + if ! diff_output=$(diff -u "${output_dir}/rendered_templates.yaml" "${expected_file}" 2>&1); then + echo "Template test FAILED for ${values_file} against ${expected_file}:" + echo "${diff_output}" + echo "=============================================" + echo "The rendered template does not match the expected template!" + echo "This could be due to changes in the chart or an outdated expected template." + echo "If this is an intentional change, update the expected template." + echo "=============================================" + exit 1 + else + echo "Template test PASSED for ${values_file} against ${expected_file}" + fi +} + + +# Check for default values.yaml test case +mkdir -p "${CHART_DIR}/tests/defaultvaluetest" +render_templates "${CHART_DIR}/values.yaml" ${CHART_DIR}/tests/defaultvaluetest +compare_templates "${CHART_DIR}/values.yaml" ${CHART_DIR}/tests/defaultvaluetest "${EXPECTED_TEMPLATES_DIR}/default.yaml" +# Cleanup is handled by the cleanup function + +# Check if TEST_CASES_DIR is not empty +if [ "$(ls -A ${TEST_CASES_DIR})" ]; then + # Iterate over each test case + for values_file in ${TEST_CASES_DIR}/*; do + case_name=$(basename "${values_file}") + output_dir="${CHART_DIR}/tests/${case_name}_output" + expected_file="${EXPECTED_TEMPLATES_DIR}/${case_name}" + + # Check if expected template exists for the current test case + if [ -f "${expected_file}" ]; then + # Create output directory for the current test case + mkdir -p "${output_dir}" + # Render templates for the current test case + render_templates "${values_file}" "${output_dir}" + # Compare rendered templates with expected templates for the current test case + compare_templates "${values_file}" "${output_dir}" "${expected_file}" + # Cleanup is handled by the cleanup function + else + echo "No expected template found for ${values_file}. Skipping template testing." + fi + done +else + echo "No test cases found in ${TEST_CASES_DIR}. Skipping template testing." +fi + +# Check failing test cases +if [ -d "${FAILING_TEST_CASES_DIR}" ] && [ "$(ls -A ${FAILING_TEST_CASES_DIR})" ]; then + for values_file in ${FAILING_TEST_CASES_DIR}/*; do + render_templates_expect_failure "${values_file}" + done +fi + +echo "Template testing completed." diff --git a/charts/headlamp/tests/test_cases/azure-oidc-with-validators.yaml b/charts/headlamp/tests/test_cases/azure-oidc-with-validators.yaml new file mode 100644 index 0000000000..700e4dda40 --- /dev/null +++ b/charts/headlamp/tests/test_cases/azure-oidc-with-validators.yaml @@ -0,0 +1,12 @@ +# Test case: Azure OIDC provider WITH validator variables +# Should include validator arguments in the deployment +config: + oidc: + secret: + create: false + clientID: "azure-client-id" + clientSecret: "azure-client-secret" + issuerURL: "https://login.microsoftonline.com/tenant-id/v2.0" + scopes: "openid email profile" + validatorClientID: "azure-validator-client-id" + validatorIssuerURL: "https://login.microsoftonline.com/tenant-id/v2.0" diff --git a/charts/headlamp/tests/test_cases/cluster-inventory-plugins.yaml b/charts/headlamp/tests/test_cases/cluster-inventory-plugins.yaml new file mode 100644 index 0000000000..f0e8c9fcad --- /dev/null +++ b/charts/headlamp/tests/test_cases/cluster-inventory-plugins.yaml @@ -0,0 +1,24 @@ +config: + clusterInventory: + enabled: true + accessProvidersConfig: + providers: + - name: secretreader + execConfig: + apiVersion: client.authentication.k8s.io/v1 + command: /access-plugins/secretreader/bin/secretreader-plugin + interactiveMode: Never + provideClusterInfo: true + - name: kubeconfig-secretreader + execConfig: + apiVersion: client.authentication.k8s.io/v1 + command: /access-plugins/kubeconfig-secretreader/bin/kubeconfig-secretreader-plugin + interactiveMode: Never + provideClusterInfo: true + plugins: + - name: secretreader + image: registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3@sha256:ec3090dc166aa2b42fb35d714d161c417d8b27bbc463404c8f615f5f4c610a1d + mountPath: /access-plugins/secretreader/ + - name: kubeconfig-secretreader + image: registry.k8s.io/cluster-inventory-api/kubeconfig-secretreader:v0.1.3@sha256:b92966cc6e4ac78002a63862921022a71d54956826f6e4febcb7247495eb98c0 + mountPath: /access-plugins/kubeconfig-secretreader/ diff --git a/charts/headlamp/tests/test_cases/cluster-inventory.yaml b/charts/headlamp/tests/test_cases/cluster-inventory.yaml new file mode 100644 index 0000000000..5aad61196c --- /dev/null +++ b/charts/headlamp/tests/test_cases/cluster-inventory.yaml @@ -0,0 +1,8 @@ +config: + clusterInventory: + enabled: true + accessProvidersConfig: + providers: + - name: static-provider + rootReconcileInterval: 10s + noCRDCacheTTL: 30s diff --git a/charts/headlamp/tests/test_cases/disable-static-plugins.yaml b/charts/headlamp/tests/test_cases/disable-static-plugins.yaml new file mode 100644 index 0000000000..abbf2abd91 --- /dev/null +++ b/charts/headlamp/tests/test_cases/disable-static-plugins.yaml @@ -0,0 +1,7 @@ +# Test case for disabling the bundled static plugins (e.g. the Prometheus +# "Show Prometheus metrics" plugin). With staticPlugins.enabled set to false the +# deployment should set HEADLAMP_STATIC_PLUGINS_DIR to an empty string so the +# backend skips serving them. +config: + staticPlugins: + enabled: false diff --git a/charts/headlamp/tests/test_cases/extra-args.yaml b/charts/headlamp/tests/test_cases/extra-args.yaml new file mode 100644 index 0000000000..51e69e4df3 --- /dev/null +++ b/charts/headlamp/tests/test_cases/extra-args.yaml @@ -0,0 +1,5 @@ +# This is a test case for extraArgs in the Headlamp deployment. +# Each test case is a dictionary with the following keys: +config: + extraArgs: + - -insecure-ssl diff --git a/charts/headlamp/tests/test_cases/extra-manifests.yaml b/charts/headlamp/tests/test_cases/extra-manifests.yaml new file mode 100644 index 0000000000..40b7387a67 --- /dev/null +++ b/charts/headlamp/tests/test_cases/extra-manifests.yaml @@ -0,0 +1,16 @@ +extraManifests: + - | + apiVersion: v1 + kind: ConfigMap + metadata: + name: dummy-configmap + data: + key1: value1 + key2: value2 + - | + apiVersion: v1 + kind: ConfigMap + metadata: + name: configmap-with-templated-data + data: + injectedKey: {{ .Release.Name }} diff --git a/charts/headlamp/tests/test_cases/host-aliases.yaml b/charts/headlamp/tests/test_cases/host-aliases.yaml new file mode 100644 index 0000000000..463dc237d4 --- /dev/null +++ b/charts/headlamp/tests/test_cases/host-aliases.yaml @@ -0,0 +1,8 @@ +hostAliases: + - ip: 10.0.0.5 + hostnames: + - keycloak.test + - sso.local + - ip: 192.168.1.10 + hostnames: + - registry.internal diff --git a/charts/headlamp/tests/test_cases/host-users-override.yaml b/charts/headlamp/tests/test_cases/host-users-override.yaml new file mode 100644 index 0000000000..b859e671e8 --- /dev/null +++ b/charts/headlamp/tests/test_cases/host-users-override.yaml @@ -0,0 +1 @@ +hostUsers: false diff --git a/charts/headlamp/tests/test_cases/httproute-enabled.yaml b/charts/headlamp/tests/test_cases/httproute-enabled.yaml new file mode 100644 index 0000000000..64f137fdf7 --- /dev/null +++ b/charts/headlamp/tests/test_cases/httproute-enabled.yaml @@ -0,0 +1,12 @@ +# Test case for HTTPRoute Gateway API configuration +httpRoute: + enabled: true + annotations: + gateway.example.com/annotation: "test" + labels: + app.kubernetes.io/component: ingress + parentRefs: + - name: my-gateway + namespace: gateway-namespace + hostnames: + - headlamp.example.com diff --git a/charts/headlamp/tests/test_cases/ingress-multi-backend.yaml b/charts/headlamp/tests/test_cases/ingress-multi-backend.yaml new file mode 100644 index 0000000000..63db7496a7 --- /dev/null +++ b/charts/headlamp/tests/test_cases/ingress-multi-backend.yaml @@ -0,0 +1,32 @@ +service: + extraServicePorts: + - name: extra + port: 9090 + +ingress: + enabled: true + ingressClassName: nginx + hosts: + - host: chart-example.local + paths: + - path: / + type: Prefix + - path: /extra-by-name + type: Prefix + backend: + service: + port: + name: extra + - path: /extra-by-number + type: Prefix + backend: + service: + port: + number: 9090 + - path: /custom-svc + type: Prefix + backend: + service: + name: "{{ .Release.Name }}-other" + port: + number: 8080 diff --git a/charts/headlamp/tests/test_cases/me-user-info-url-directly.yaml b/charts/headlamp/tests/test_cases/me-user-info-url-directly.yaml new file mode 100644 index 0000000000..149928b481 --- /dev/null +++ b/charts/headlamp/tests/test_cases/me-user-info-url-directly.yaml @@ -0,0 +1,3 @@ +env: + - name: ME_USER_INFO_URL + value: /oauth2/userinfocustom1 diff --git a/charts/headlamp/tests/test_cases/me-user-info-url.yaml b/charts/headlamp/tests/test_cases/me-user-info-url.yaml new file mode 100644 index 0000000000..76c4f64050 --- /dev/null +++ b/charts/headlamp/tests/test_cases/me-user-info-url.yaml @@ -0,0 +1,4 @@ +# -- Headlamp OIDC me user info URL test case +config: + oidc: + meUserInfoURL: /oauth2/userinfocustom2 diff --git a/charts/headlamp/tests/test_cases/namespace-override-oidc-create-secret.yaml b/charts/headlamp/tests/test_cases/namespace-override-oidc-create-secret.yaml new file mode 100644 index 0000000000..12faa2a523 --- /dev/null +++ b/charts/headlamp/tests/test_cases/namespace-override-oidc-create-secret.yaml @@ -0,0 +1,18 @@ +namespaceOverride: "mynamespace2" + +# This is a test case for the oidc.secret.create field in the Headlamp deployment. +# The oidc.secret.create field is a boolean that determines whether to create a secret for OIDC. +# The oidc.secret.name field is a string that specifies the name of the OIDC secret. +# The oidc.clientID field is a string that specifies the client ID for OIDC. +# The oidc.clientSecret field is a string that specifies the client secret for OIDC. +# The oidc.issuerURL field is a string that specifies the issuer URL for OIDC. +# The oidc.scopes field is a string that specifies the scopes for OIDC. +config: + oidc: + secret: + create: true + name: oidc + clientID: "testClientId" + clientSecret: "testClientSecret" + issuerURL: "testIssuerURL" + scopes: "testScope" diff --git a/charts/headlamp/tests/test_cases/namespace-override.yaml b/charts/headlamp/tests/test_cases/namespace-override.yaml new file mode 100644 index 0000000000..e631110f52 --- /dev/null +++ b/charts/headlamp/tests/test_cases/namespace-override.yaml @@ -0,0 +1 @@ +namespaceOverride: "mynamespace" diff --git a/charts/headlamp/tests/test_cases/non-azure-oidc.yaml b/charts/headlamp/tests/test_cases/non-azure-oidc.yaml new file mode 100644 index 0000000000..765efcc14a --- /dev/null +++ b/charts/headlamp/tests/test_cases/non-azure-oidc.yaml @@ -0,0 +1,10 @@ +# Test case: Non-Azure OIDC provider without validator variables +# This should NOT include validator arguments in the deployment +config: + oidc: + secret: + create: false + clientID: "generic-oidc-client" + clientSecret: "generic-oidc-secret" + issuerURL: "https://auth.example.com" + scopes: "openid email profile" diff --git a/charts/headlamp/tests/test_cases/oidc-create-secret.yaml b/charts/headlamp/tests/test_cases/oidc-create-secret.yaml new file mode 100644 index 0000000000..7a5f652a6a --- /dev/null +++ b/charts/headlamp/tests/test_cases/oidc-create-secret.yaml @@ -0,0 +1,16 @@ +# This is a test case for the oidc.secret.create field in the Headlamp deployment. +# The oidc.secret.create field is a boolean that determines whether to create a secret for OIDC. +# The oidc.secret.name field is a string that specifies the name of the OIDC secret. +# The oidc.clientID field is a string that specifies the client ID for OIDC. +# The oidc.clientSecret field is a string that specifies the client secret for OIDC. +# The oidc.issuerURL field is a string that specifies the issuer URL for OIDC. +# The oidc.scopes field is a string that specifies the scopes for OIDC. +config: + oidc: + secret: + create: true + name: oidc + clientID: "testClientId" + clientSecret: "testClientSecret" + issuerURL: "testIssuerURL" + scopes: "testScope" diff --git a/charts/headlamp/tests/test_cases/oidc-directly-env.yaml b/charts/headlamp/tests/test_cases/oidc-directly-env.yaml new file mode 100644 index 0000000000..53f9fe3b09 --- /dev/null +++ b/charts/headlamp/tests/test_cases/oidc-directly-env.yaml @@ -0,0 +1,10 @@ +# This is a test case where user can set env values directly for OIDC configuration. +env: + - name: OIDC_CLIENT_ID + value: testClientId + - name: OIDC_CLIENT_SECRET + value: testClientSecret + - name: OIDC_ISSUER_URL + value: testIssuerURL + - name: OIDC_SCOPES + value: testScope diff --git a/charts/headlamp/tests/test_cases/oidc-directly.yaml b/charts/headlamp/tests/test_cases/oidc-directly.yaml new file mode 100644 index 0000000000..4a318c60b4 --- /dev/null +++ b/charts/headlamp/tests/test_cases/oidc-directly.yaml @@ -0,0 +1,14 @@ +# This is a test case for the direct OIDC configuration in the Headlamp deployment. +# The oidc.secret.create field is false to avoid creating a secret for OIDC. +# The oidc.clientID field is a string that specifies the client ID for OIDC. +# The oidc.clientSecret field is a string that specifies the client secret for OIDC. +# The oidc.issuerURL field is a string that specifies the issuer URL for OIDC. +# The oidc.scopes field is a string that specifies the scopes for OIDC. +config: + oidc: + secret: + create: false + clientID: "testClientId" + clientSecret: "testClientSecret" + issuerURL: "testIssuerURL" + scopes: "testScope" diff --git a/charts/headlamp/tests/test_cases/oidc-external-secret-with-scopes.yaml b/charts/headlamp/tests/test_cases/oidc-external-secret-with-scopes.yaml new file mode 100644 index 0000000000..3bc0f3dc4b --- /dev/null +++ b/charts/headlamp/tests/test_cases/oidc-external-secret-with-scopes.yaml @@ -0,0 +1,11 @@ +# This is a test case for OIDC external secret with hasScopes enabled. +# When hasScopes is true, the -oidc-scopes argument is included so that +# the OIDC_SCOPES key from the external secret is passed to Headlamp. +config: + oidc: + secret: + create: false + externalSecret: + enabled: true + name: oidc + hasScopes: true diff --git a/charts/headlamp/tests/test_cases/oidc-external-secret.yaml b/charts/headlamp/tests/test_cases/oidc-external-secret.yaml new file mode 100644 index 0000000000..6773732812 --- /dev/null +++ b/charts/headlamp/tests/test_cases/oidc-external-secret.yaml @@ -0,0 +1,10 @@ +# This is a test case for OIDC external secret. +# The oidc.externalSecret.enabled field is a boolean that determines whether to use an external secret for OIDC. +# The oidc.externalSecret.name field is a string that specifies the name of the external OIDC secret. +config: + oidc: + secret: + create: false + externalSecret: + enabled: true + name: oidc diff --git a/charts/headlamp/tests/test_cases/oidc-pkce.yaml b/charts/headlamp/tests/test_cases/oidc-pkce.yaml new file mode 100644 index 0000000000..f8a5ed5ec4 --- /dev/null +++ b/charts/headlamp/tests/test_cases/oidc-pkce.yaml @@ -0,0 +1,15 @@ +# This is a test case for the direct OIDC configuration in the Headlamp deployment. +# The oidc.secret.create field is false to avoid creating a secret for OIDC. +# The oidc.clientID field is a string that specifies the client ID for OIDC. +# The oidc.clientSecret field is a string that specifies the client secret for OIDC. +# The oidc.issuerURL field is a string that specifies the issuer URL for OIDC. +# The oidc.scopes field is a string that specifies the scopes for OIDC. +config: + oidc: + secret: + create: false + clientID: "testClientId" + clientSecret: "testClientSecret" + issuerURL: "testIssuerURL" + scopes: "testScope" + usePKCE: true diff --git a/charts/headlamp/tests/test_cases/oidc-validator-overrides.yaml b/charts/headlamp/tests/test_cases/oidc-validator-overrides.yaml new file mode 100644 index 0000000000..cac2c43549 --- /dev/null +++ b/charts/headlamp/tests/test_cases/oidc-validator-overrides.yaml @@ -0,0 +1,20 @@ +# This is a test case for OIDC using the validator overrides and access_token. +# The oidc.secret.create field is false to avoid creating a secret for OIDC. +# The oidc.clientID field is a string that specifies the client ID for OIDC. +# The oidc.clientSecret field is a string that specifies the client secret for OIDC. +# The oidc.issuerURL field is a string that specifies the issuer URL for OIDC. +# The oidc.scopes field is a string that specifies the scopes for OIDC. +# The oidc.validatorClientID field is a string that specifies the ClientID used in validation for OIDC. +# The oidc.validatorIssuerURL field is a string that specifies the IssuerURL used in validation for OIDC. +# The oidc.useAccessToken field is a bool that determines if 'access_token' or 'id_token' is used for OIDC. +config: + oidc: + secret: + create: false + clientID: "testClientId" + clientSecret: "testClientSecret" + issuerURL: "testIssuerURL" + scopes: "testScope" + validatorClientID: "overriddenClientID" + validatorIssuerURL: "overriddenIssuerURL" + useAccessToken: true diff --git a/charts/headlamp/tests/test_cases/pod-disruption.yaml b/charts/headlamp/tests/test_cases/pod-disruption.yaml new file mode 100644 index 0000000000..382f0d3fd4 --- /dev/null +++ b/charts/headlamp/tests/test_cases/pod-disruption.yaml @@ -0,0 +1,5 @@ +podDisruptionBudget: + enabled: true + maxUnavailable: 1 + unhealthyPodEvictionPolicy: IfHealthyBudget + minAvailable: null diff --git a/charts/headlamp/tests/test_cases/readonly-root-filesystem-custom-tmp-volume.yaml b/charts/headlamp/tests/test_cases/readonly-root-filesystem-custom-tmp-volume.yaml new file mode 100644 index 0000000000..3d01bb5490 --- /dev/null +++ b/charts/headlamp/tests/test_cases/readonly-root-filesystem-custom-tmp-volume.yaml @@ -0,0 +1,7 @@ +securityContext: + readOnlyRootFilesystem: true + +volumes: + - name: headlamp-tmp + emptyDir: + sizeLimit: 100Mi diff --git a/charts/headlamp/tests/test_cases/readonly-root-filesystem-custom-tmp.yaml b/charts/headlamp/tests/test_cases/readonly-root-filesystem-custom-tmp.yaml new file mode 100644 index 0000000000..144e3fb338 --- /dev/null +++ b/charts/headlamp/tests/test_cases/readonly-root-filesystem-custom-tmp.yaml @@ -0,0 +1,10 @@ +securityContext: + readOnlyRootFilesystem: true + +volumeMounts: + - name: my-tmp + mountPath: /tmp + +volumes: + - name: my-tmp + emptyDir: {} diff --git a/charts/headlamp/tests/test_cases/readonly-root-filesystem-plugins-inherit.yaml b/charts/headlamp/tests/test_cases/readonly-root-filesystem-plugins-inherit.yaml new file mode 100644 index 0000000000..0f400292ee --- /dev/null +++ b/charts/headlamp/tests/test_cases/readonly-root-filesystem-plugins-inherit.yaml @@ -0,0 +1,5 @@ +securityContext: + readOnlyRootFilesystem: true + +pluginsManager: + enabled: true diff --git a/charts/headlamp/tests/test_cases/readonly-root-filesystem-plugins-only.yaml b/charts/headlamp/tests/test_cases/readonly-root-filesystem-plugins-only.yaml new file mode 100644 index 0000000000..1395568f5c --- /dev/null +++ b/charts/headlamp/tests/test_cases/readonly-root-filesystem-plugins-only.yaml @@ -0,0 +1,7 @@ +securityContext: + readOnlyRootFilesystem: false + +pluginsManager: + enabled: true + securityContext: + readOnlyRootFilesystem: true diff --git a/charts/headlamp/tests/test_cases/readonly-root-filesystem.yaml b/charts/headlamp/tests/test_cases/readonly-root-filesystem.yaml new file mode 100644 index 0000000000..5ab0a7a636 --- /dev/null +++ b/charts/headlamp/tests/test_cases/readonly-root-filesystem.yaml @@ -0,0 +1,2 @@ +securityContext: + readOnlyRootFilesystem: true diff --git a/charts/headlamp/tests/test_cases/security-context.yaml b/charts/headlamp/tests/test_cases/security-context.yaml new file mode 100644 index 0000000000..3abc1b80c6 --- /dev/null +++ b/charts/headlamp/tests/test_cases/security-context.yaml @@ -0,0 +1,31 @@ +podSecurityContext: + runAsUser: 1000 + runAsGroup: 3000 + fsGroup: 2000 + seccompProfile: + type: RuntimeDefault + +# This config will be rendered into the container's securityContext. +securityContext: + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + +# Example override for the optional pluginsManager container securityContext +pluginsManager: + enabled: true + baseImage: "node:18-alpine" + version: "1.0.0" + securityContext: + runAsUser: 1001 + runAsNonRoot: true + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL \ No newline at end of file diff --git a/charts/headlamp/tests/test_cases/service-account-token-custom-token.yaml b/charts/headlamp/tests/test_cases/service-account-token-custom-token.yaml new file mode 100644 index 0000000000..04e3694ceb --- /dev/null +++ b/charts/headlamp/tests/test_cases/service-account-token-custom-token.yaml @@ -0,0 +1,5 @@ +# Enables unsafe service account token auth with a custom token path. +config: + inCluster: true + unsafeUseServiceAccountToken: true + serviceAccountTokenPath: "/custom/token" diff --git a/charts/headlamp/tests/test_cases/service-account-token-out-of-cluster.yaml b/charts/headlamp/tests/test_cases/service-account-token-out-of-cluster.yaml new file mode 100644 index 0000000000..33110748de --- /dev/null +++ b/charts/headlamp/tests/test_cases/service-account-token-out-of-cluster.yaml @@ -0,0 +1,5 @@ +# Service account token flags must not be rendered when running out of cluster. +config: + inCluster: false + unsafeUseServiceAccountToken: true + serviceAccountTokenPath: "/custom/token" diff --git a/charts/headlamp/tests/test_cases/service-account-token.yaml b/charts/headlamp/tests/test_cases/service-account-token.yaml new file mode 100644 index 0000000000..f55f50ef66 --- /dev/null +++ b/charts/headlamp/tests/test_cases/service-account-token.yaml @@ -0,0 +1,4 @@ +# Enables unsafe service account token auth for in-cluster mode. +config: + inCluster: true + unsafeUseServiceAccountToken: true diff --git a/charts/headlamp/tests/test_cases/service-appprotocol.yaml b/charts/headlamp/tests/test_cases/service-appprotocol.yaml new file mode 100644 index 0000000000..853f1fbf39 --- /dev/null +++ b/charts/headlamp/tests/test_cases/service-appprotocol.yaml @@ -0,0 +1,2 @@ +service: + appProtocol: kubernetes.io/ws diff --git a/charts/headlamp/tests/test_cases/service-extra-ports.yaml b/charts/headlamp/tests/test_cases/service-extra-ports.yaml new file mode 100644 index 0000000000..b3ba274538 --- /dev/null +++ b/charts/headlamp/tests/test_cases/service-extra-ports.yaml @@ -0,0 +1,10 @@ +service: + type: NodePort + extraServicePorts: + - name: metrics + port: 9090 + targetPort: metrics + - name: grpc + port: 9091 + protocol: TCP + nodePort: 31091 diff --git a/charts/headlamp/tests/test_cases/tls-added.yaml b/charts/headlamp/tests/test_cases/tls-added.yaml new file mode 100644 index 0000000000..3b9ba9b5c0 --- /dev/null +++ b/charts/headlamp/tests/test_cases/tls-added.yaml @@ -0,0 +1,20 @@ +config: + tlsCertPath: "/headlamp-cert/headlamp-ca.crt" + tlsKeyPath: "/headlamp-cert/headlamp-tls.key" + +probes: + scheme: HTTPS + +volumes: + - name: "headlamp-cert" + secret: + secretName: "headlamp-tls" + items: + - key: "tls.crt" + path: "headlamp-ca.crt" + - key: "tls.key" + path: "headlamp-tls.key" + +volumeMounts: + - name: "headlamp-cert" + mountPath: "/headlamp-cert" diff --git a/charts/headlamp/tests/test_cases/topology-spread-constraints-custom-selector.yaml b/charts/headlamp/tests/test_cases/topology-spread-constraints-custom-selector.yaml new file mode 100644 index 0000000000..1a8360af9f --- /dev/null +++ b/charts/headlamp/tests/test_cases/topology-spread-constraints-custom-selector.yaml @@ -0,0 +1,11 @@ +# Test case for topologySpreadConstraints with custom labelSelector +topologySpreadConstraints: + - maxSkew: 2 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: headlamp + custom-label: custom-value + matchLabelKeys: + - pod-template-hash diff --git a/charts/headlamp/tests/test_cases/topology-spread-constraints.yaml b/charts/headlamp/tests/test_cases/topology-spread-constraints.yaml new file mode 100644 index 0000000000..ad1ea0032d --- /dev/null +++ b/charts/headlamp/tests/test_cases/topology-spread-constraints.yaml @@ -0,0 +1,12 @@ +# Test case for topologySpreadConstraints with automatic labelSelector population +topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + matchLabelKeys: + - pod-template-hash + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + matchLabelKeys: + - pod-template-hash diff --git a/charts/headlamp/tests/test_cases/volumes-added.yaml b/charts/headlamp/tests/test_cases/volumes-added.yaml new file mode 100644 index 0000000000..b84a646d3d --- /dev/null +++ b/charts/headlamp/tests/test_cases/volumes-added.yaml @@ -0,0 +1,5 @@ +# This is a test case for volumes in the Headlamp deployment. +# The volumes field is a list of dictionaries that specify the volumes to add to the Headlamp deployment. +volumes: + - name: plugins + emptyDir: {} diff --git a/charts/headlamp/tests/update-version.sh b/charts/headlamp/tests/update-version.sh new file mode 100644 index 0000000000..d44f91beab --- /dev/null +++ b/charts/headlamp/tests/update-version.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +# Enable strict mode +set -euo pipefail + +# Set up variables +CHART_DIR="./charts/headlamp" +TEST_CASES_DIR="${CHART_DIR}/tests/test_cases" +EXPECTED_TEMPLATES_DIR="${CHART_DIR}/tests/expected_templates" + +# Get the current chart, app and image version +CURRENT_CHART_VERSION=$(grep '^version:' ${CHART_DIR}/Chart.yaml | awk '{print $2}') +CURRENT_APP_VERSION=$(grep '^appVersion:' ${CHART_DIR}/Chart.yaml | awk '{print $2}') +CURRENT_IMAGE_VERSION=$(grep '^appVersion:' ${CHART_DIR}/Chart.yaml | awk '{print $2}') + +echo "Checking and updating template versions..." +echo "Using chart version: ${CURRENT_CHART_VERSION}, app version: ${CURRENT_APP_VERSION}, image version: ${CURRENT_IMAGE_VERSION}" + +# Function to render templates for a specific values file +render_templates() { + values_file="$1" + output_dir="$2" + # Render templates + helm template headlamp ${CHART_DIR} --values ${values_file} > "${output_dir}/rendered_templates.yaml" + if [ ! -s "${output_dir}/rendered_templates.yaml" ]; then + echo "ERROR: Failed to render templates for ${values_file}" + exit 1 + fi +} + +# Check if versions need updating and update the expected template if needed +check_and_update_template() { + case_name="$1" + values_file="$2" + + expected_file="${EXPECTED_TEMPLATES_DIR}/${case_name}" + needs_update=false + + # Check if versions need updating + if [ -f "${expected_file}" ]; then + # Check chart version + if ! grep -q "helm.sh/chart: headlamp-${CURRENT_CHART_VERSION}" "${expected_file}"; then + needs_update=true + fi + + # Check app version + if ! grep -q "app.kubernetes.io/version: \"${CURRENT_APP_VERSION}\"" "${expected_file}"; then + needs_update=true + fi + + # Check image version + if ! grep -q "ghcr.io/headlamp-k8s/headlamp:v${CURRENT_IMAGE_VERSION}" "${expected_file}"; then + needs_update=true + fi + else + # File doesn't exist, so it needs to be created + needs_update=true + fi + + if [ "$needs_update" = true ]; then + echo "${case_name}: Updating to version ${CURRENT_CHART_VERSION}..." + + # Create temporary output directory + output_dir="${CHART_DIR}/tests/update_${case_name}" + mkdir -p "${output_dir}" + + # Render the template + render_templates "${values_file}" "${output_dir}" + + # Update the expected template + cp "${output_dir}/rendered_templates.yaml" "${expected_file}" + + # Clean up + rm -rf "${output_dir}" + else + echo "${case_name}: Version ${CURRENT_CHART_VERSION} already up to date" + fi +} + +# Check and update default template +check_and_update_template "default.yaml" "${CHART_DIR}/values.yaml" + +# Check and update templates for each test case +if [ "$(ls -A ${TEST_CASES_DIR})" ]; then + for values_file in ${TEST_CASES_DIR}/*; do + case_name=$(basename "${values_file}") + check_and_update_template "${case_name}" "${values_file}" + done +else + echo "No test cases found in ${TEST_CASES_DIR}." +fi + +echo "Version check complete. Please review any changes before committing." diff --git a/charts/headlamp/values.schema.json b/charts/headlamp/values.schema.json new file mode 100644 index 0000000000..a85baeaa00 --- /dev/null +++ b/charts/headlamp/values.schema.json @@ -0,0 +1,989 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "replicaCount": { + "type": "integer", + "description": "Number of replicas to deploy", + "minimum": 1 + }, + "image": { + "type": "object", + "title": "Image", + "description": "Image to deploy", + "properties": { + "registry": { + "type": "string", + "description": "Registry of the image" + }, + "repository": { + "type": "string", + "description": "Repository of the image" + }, + "pullPolicy": { + "type": "string", + "description": "Pull policy of the image", + "enum": ["Always", "IfNotPresent", "Never"] + }, + "tag": { + "type": "string", + "description": "Tag of the image" + } + } + }, + "imagePullSecrets": { + "type": "array", + "description": "ImagePullSecrets is an optional list of references to secrets in the same namespace to use for pulling any of the images used by this PodSpec. If specified, these secrets will be passed to individual puller implementations for them to use.", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the image pull secret" + } + } + } + }, + "nameOverride": { + "type": "string", + "description": "Override the name of the chart" + }, + "fullnameOverride": { + "type": "string", + "description": "Override the full name of the chart" + }, + "namespaceOverride": { + "type": "string", + "description": "Override the deployment namespace; defaults to .Release.Namespace" + }, + "initContainers": { + "type": "array", + "description": "Init containers", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the init container" + }, + "image": { + "type": "string", + "description": "Image of the init container" + }, + "imagePullPolicy": { + "type": "string", + "description": "Pull policy of the init container", + "enum": ["Always", "IfNotPresent", "Never"] + }, + "command": { + "type": "array", + "description": "Command of the init container", + "items": { + "type": "string" + } + }, + "args": { + "type": "array", + "description": "Arguments of the init container", + "items": { + "type": "string" + } + }, + "resources": { + "type": "object", + "description": "Resources of the init container", + "properties": { + "limits": { + "type": "object", + "description": "Limits of the init container", + "properties": { + "cpu": { + "type": "string", + "description": "CPU limit" + }, + "memory": { + "type": "string", + "description": "Memory limit" + } + } + }, + "requests": { + "type": "object", + "description": "Requests of the init container", + "properties": { + "cpu": { + "type": "string", + "description": "CPU request" + }, + "memory": { + "type": "string", + "description": "Memory request" + } + } + } + } + }, + "env": { + "type": "array", + "description": "Environment variables of the init container", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the environment variable" + }, + "value": { + "type": "string", + "description": "Value of the environment variable" + } + } + } + }, + "volumeMounts": { + "type": "array", + "description": "Volume mounts of the init container", + "items": + { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the volume mount" + }, + "mountPath": { + "type": "string", + "description": "Mount path of the volume mount" + }, + "readOnly": { + "type": "boolean", + "description": "Read only of the volume mount" + } + } + } + } + } + } + }, + "config": { + "type": "object", + "description": "Headlamp deployment configuration", + "properties": { + "baseURL": { + "type": "string", + "description": "Base URL of the application" + }, + "sessionTTL": { + "type": "integer", + "description": "The time in seconds for the session to be valid", + "default": 86400, + "minimum": 1, + "maximum": 31536000 + }, + "podDebugImage": { + "type": "string", + "description": "Default image to use when creating pod debug containers" + }, + "nodeShellImage": { + "type": "string", + "description": "Default image to use when creating node shell pods" + }, + "nodeShellNamespace": { + "type": "string", + "description": "Default namespace to use when creating node shell pods" + }, + "unsafeUseServiceAccountToken": { + "type": "boolean", + "description": "UNSAFE: authenticate every user as the pod's service account in-cluster mode" + }, + "serviceAccountTokenPath": { + "type": "string", + "description": "Path to the service account token file when unsafeUseServiceAccountToken is enabled" + }, + "oidc": { + "type": "object", + "description": "OIDC configuration", + "properties": { + "secret": { + "type": "object", + "description": "Secret created by Headlamp to authenticate with the OIDC provider", + "properties": { + "name": { + "type": "string", + "description": "Name of the secret" + }, + "create": { + "type": "boolean", + "description": "Create the secret" + } + } + }, + "clientID": { + "type": "string", + "description": "Issuer of the OIDC provider" + }, + "clientSecret": { + "type": "string", + "description": "Client ID of the OIDC provider" + }, + "issuerURL": { + "type": "string", + "description": "Client secret of the OIDC provider" + }, + "scopes": { + "type": "string", + "description": "Scopes of the OIDC provider" + }, + "usePKCE": { + "type": "boolean", + "description": "Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow" + }, + "externalSecret": { + "type": "object", + "description": "External secret to use for OIDC configuration", + "properties": { + "name": { + "type": "string", + "description": "Name of the external secret" + }, + "enabled": { + "type": "boolean", + "description": "Enable the external secret" + }, + "hasScopes": { + "type": "boolean", + "description": "Set to true if the external secret contains an OIDC_SCOPES key" + } + } + } + } + }, + "pluginsDir": { + "type": "string", + "description": "Directory to load plugins from" + }, + "staticPlugins": { + "type": "object", + "description": "Bundled (static) plugins shipped in the Headlamp image, such as the Prometheus plugin", + "properties": { + "enabled": { + "type": "boolean", + "description": "Serve the bundled static plugins. Set to false to disable them (e.g. the \"Show Prometheus metrics\" button)", + "default": true + } + }, + "additionalProperties": false + }, + "tlsCertPath": { + "type": "string", + "description": "Path of certificate file for TLS" + }, + "tlsKeyPath": { + "type": "string", + "description": "Path of private key file for TLS" + }, + "clusterInventory": { + "type": "object", + "description": "Experimental/alpha Cluster Inventory configuration", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable experimental/alpha Cluster Inventory discovery", + "default": false + }, + "accessProvidersConfig": { + "type": "object", + "description": "Experimental/alpha Cluster Inventory access providers config", + "default": {} + }, + "plugins": { + "type": "array", + "description": "Kubernetes image volumes that provide experimental/alpha Cluster Inventory access provider binaries", + "items": { + "type": "object", + "required": ["name", "image", "mountPath"], + "properties": { + "name": { + "type": "string", + "maxLength": 63, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "description": "DNS label name of the plugin image volume" + }, + "image": { + "type": "string", + "description": "Image reference for the plugin image volume" + }, + "mountPath": { + "type": "string", + "pattern": "^/", + "description": "Absolute read-only mount path for the plugin image volume" + } + }, + "additionalProperties": false + }, + "default": [] + }, + "labelSelector": { + "type": "string", + "description": "Kubernetes label selector used to filter experimental/alpha ClusterProfile resources", + "default": "!headlamp.dev/ignore" + }, + "rootReconcileInterval": { + "type": "string", + "description": "Override the experimental/alpha Cluster Inventory root reconcile interval. Empty uses the Headlamp default", + "default": "" + }, + "noCRDCacheTTL": { + "type": "string", + "description": "Override the experimental/alpha Cluster Inventory no-CRD cache TTL. Empty uses the Headlamp default", + "default": "" + } + } + }, + "extraArgs": { + "type": "array", + "description": "Extra arguments to pass to the application", + "items": { + "type": "string" + } + } + } + }, + "env": { + "type": "array", + "description": "Environment variables to pass to the deployment", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the environment variable" + }, + "value": { + "type": "string", + "description": "Value of the environment variable" + }, + "valueFrom": { + "type": "object", + "description": "Source for the environment variable's value", + "properties": { + "secretKeyRef": { + "type": "object", + "description": "Selects a key of a Secret", + "properties": { + "name": { + "type": "string", + "description": "Name of the Secret" + }, + "key": { + "type": "string", + "description": "Key of the Secret to select from" + }, + "optional": { + "type": "boolean", + "description": "Specify whether the Secret or its key must be defined" + } + }, + "required": ["key"], + "additionalProperties": false + }, + "configMapKeyRef": { + "type": "object", + "description": "Selects a key of a ConfigMap", + "properties": { + "name": { + "type": "string", + "description": "Name of the ConfigMap" + }, + "key": { + "type": "string", + "description": "Key of the ConfigMap to select from" + }, + "optional": { + "type": "boolean", + "description": "Specify whether the ConfigMap or its key must be defined" + } + }, + "required": ["key"], + "additionalProperties": false + }, + "fieldRef": { + "type": "object", + "description": "Selects a field of the pod", + "properties": { + "apiVersion": { + "type": "string", + "description": "API version of the schema the fieldPath is written in" + }, + "fieldPath": { + "type": "string", + "description": "Path of the field to select in the specified API version" + } + }, + "required": ["fieldPath"], + "additionalProperties": false + }, + "resourceFieldRef": { + "type": "object", + "description": "Selects a resource of the container", + "properties": { + "containerName": { + "type": "string", + "description": "Container name to select resources from" + }, + "resource": { + "type": "string", + "description": "Resource to select" + }, + "divisor": { + "type": "string", + "description": "Output format of the exposed resource" + } + }, + "required": ["resource"], + "additionalProperties": false + } + }, + "oneOf": [ + { + "required": ["secretKeyRef"], + "not": { "anyOf": [{ "required": ["configMapKeyRef"] }, { "required": ["fieldRef"] }, { "required": ["resourceFieldRef"] }] } + }, + { + "required": ["configMapKeyRef"], + "not": { "anyOf": [{ "required": ["secretKeyRef"] }, { "required": ["fieldRef"] }, { "required": ["resourceFieldRef"] }] } + }, + { + "required": ["fieldRef"], + "not": { "anyOf": [{ "required": ["secretKeyRef"] }, { "required": ["configMapKeyRef"] }, { "required": ["resourceFieldRef"] }] } + }, + { + "required": ["resourceFieldRef"], + "not": { "anyOf": [{ "required": ["secretKeyRef"] }, { "required": ["configMapKeyRef"] }, { "required": ["fieldRef"] }] } + } + ], + "additionalProperties": false + } + }, + "required": ["name"], + "oneOf": [ + { + "required": ["value"], + "not": { + "required": ["valueFrom"] + } + }, + { + "required": ["valueFrom"], + "not": { + "required": ["value"] + } + } + ], + "additionalProperties": false + } + }, + "automountServiceAccountToken": { + "type": "boolean", + "description": "Mount Service Account token in pod" + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Specifies whether a service account should be created" + }, + "annotations": { + "type": "object", + "description": "Annotations to add to the service account" + }, + "name": { + "type": "string", + "description": "The name of the service account to use" + } + } + }, + "clusterRoleBinding": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Specifies whether a cluster role binding should be created" + }, + "clusterRoleName": { + "type": "string", + "description": "The name of the ClusterRole to create in the cluster" + }, + "annotations": { + "type": "object", + "description": "Annotations to add to the cluster role binding" + } + } + }, + "service": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Kubernetes Service type", + "enum": ["ClusterIP", "NodePort", "LoadBalancer", "ExternalName"] + }, + "port": { + "type": "integer", + "description": "Kubernetes Service port" + }, + "appProtocol": { + "type": ["string", "null"], + "description": "Kubernetes Service port appProtocol (for the main http port)" + }, + "clusterIP": { + "type": "string", + "description": "Kubernetes Service clusterIP" + }, + "loadBalancerIP": { + "type": "string", + "description": "Kubernetes Service loadBalancerIP" + }, + "loadBalancerSourceRanges": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Kubernetes Service loadBalancerSourceRanges" + }, + "nodePort": { + "type": ["integer", "null"], + "description": "Kubernetes Service Nodeport" + }, + "extraServicePorts": { + "type": "array", + "description": "Additional ports to expose on the Service in addition to the default http port", + "items": { + "type": "object", + "required": ["name", "port"], + "properties": { + "name": { + "type": "string", + "description": "Port name (must be unique within the Service)" + }, + "port": { + "type": "integer", + "description": "Service port number" + }, + "targetPort": { + "type": ["integer", "string"], + "description": "Pod-side target port (number or named port). Defaults to `port` when omitted" + }, + "protocol": { + "type": "string", + "enum": ["TCP", "UDP", "SCTP"], + "description": "Protocol (TCP/UDP/SCTP). Defaults to TCP" + }, + "nodePort": { + "type": ["integer", "null"], + "description": "Node port (only honored when service.type is NodePort or LoadBalancer)" + } + } + } + } + } + }, + "persistentVolumeClaim": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable Persistent Volume Claim" + }, + "annotations": { + "type": "object", + "description": "Annotations to add to the persistent volume claim (if enabled)" + }, + "accessModes": { + "type": "array", + "items": { + "type": "string" + } + }, + "size": { + "type": "string" + }, + "storageClassName": { + "type": "string" + }, + "selector": { + "type": "object", + "properties": { + "matchLabels": { + "type": "object" + }, + "matchExpressions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "operator": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "volumeMode": { + "type": "string" + } + } + }, + "ingress": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable ingress controller resource" + }, + "annotations": { + "type": "object", + "description": "Annotations for Ingress resource" + }, + "ingressClassName": { + "type": "string", + "description": "Ingress class name" + }, + "hosts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "paths": { + "type": "array", + "items": { + "type": "object", + "required": [ + "path", + "type" + ], + "properties": { + "path": { + "type": "string" + }, + "type": { + "type": "string" + }, + "backend": { + "type": "object", + "description": "Optional override of the backend Service for this path", + "properties": { + "service": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Service name (supports tpl). Defaults to the Headlamp Service when omitted" + }, + "port": { + "type": "object", + "properties": { + "number": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "oneOf": [ + { + "required": [ + "number" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "number" + ] + } + } + ] + } + } + } + } + } + } + } + } + } + } + }, + "tls": { + "type": "array", + "items": { + "type": "object", + "properties": { + "secretName": { + "type": "string" + }, + "hosts": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "httpRoute": { + "type": "object", + "description": "HTTPRoute configuration for Gateway API", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable HTTPRoute resource for Gateway API" + }, + "annotations": { + "type": "object", + "description": "Annotations for HTTPRoute resource" + }, + "labels": { + "type": "object", + "description": "Additional labels for HTTPRoute resource" + }, + "parentRefs": { + "type": "array", + "description": "Parent references (REQUIRED when enabled - HTTPRoute will not work without this)", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the parent gateway" + }, + "namespace": { + "type": "string", + "description": "Namespace of the parent gateway" + }, + "sectionName": { + "type": "string", + "description": "Section name of the parent gateway listener" + } + }, + "required": ["name"] + } + }, + "hostnames": { + "type": "array", + "description": "Hostnames for the HTTPRoute", + "items": { + "type": "string" + } + }, + "rules": { + "type": "array", + "description": "Custom routing rules (optional, defaults to path prefix /)", + "items": { + "type": "object" + } + } + } + }, + "podDisruptionBudget": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "description": "Enable PodDisruptionBudget. See: https://kubernetes.io/docs/concepts/workloads/pods/disruptions/" + }, + "minAvailable": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "pattern": "^([0-9]+%?|[0-9]*\\.[0-9]+%)$" + } + ], + "default": 0, + "description": "Minimum number/percentage of pods that should remain scheduled. When it's set, maxUnavailable must be disabled by `maxUnavailable: null`" + }, + "maxUnavailable": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "pattern": "^([0-9]+%?|[0-9]*\\.[0-9]+%)$" + } + ], + "default": null, + "description": "Maximum number/percentage of pods that may be made unavailable" + }, + "unhealthyPodEvictionPolicy": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "enum": [ + "IfHealthyBudget", + "AlwaysAllow" + ] + } + ], + "default": null, + "description": "How are unhealthy, but running, pods counted for eviction" + } + } + }, + "extraManifests": { + "type": "array", + "description": "Extra manifests to apply to the deployment", + "items": { + "type": "string" + } + }, + "probes": { + "type": "object", + "description": "Probe configuration for liveness and readiness checks", + "properties": { + "scheme": { + "type": "string", + "description": "Scheme for probes (HTTP or HTTPS). Set to HTTPS when TLS is enabled at the backend server.", + "enum": ["HTTP", "HTTPS"], + "default": "HTTP" + }, + "livenessProbe": { + "type": "object", + "description": "Liveness probe settings", + "properties": { + "initialDelaySeconds": { + "type": "integer", + "description": "Initial delay in seconds before starting liveness probe", + "minimum": 0, + "default": 0 + }, + "periodSeconds": { + "type": "integer", + "description": "Period in seconds between liveness probe checks", + "minimum": 1, + "default": 10 + }, + "timeoutSeconds": { + "type": "integer", + "description": "Timeout in seconds for liveness probe", + "minimum": 1, + "default": 1 + }, + "successThreshold": { + "type": "integer", + "description": "Minimum consecutive successes for the probe to be considered successful (must be 1 for liveness probes per Kubernetes API)", + "minimum": 1, + "maximum": 1, + "const": 1, + "default": 1 + }, + "failureThreshold": { + "type": "integer", + "description": "Minimum consecutive failures for the probe to be considered failed", + "minimum": 1, + "default": 3 + } + } + }, + "readinessProbe": { + "type": "object", + "description": "Readiness probe settings", + "properties": { + "initialDelaySeconds": { + "type": "integer", + "description": "Initial delay in seconds before starting readiness probe", + "minimum": 0, + "default": 0 + }, + "periodSeconds": { + "type": "integer", + "description": "Period in seconds between readiness probe checks", + "minimum": 1, + "default": 10 + }, + "timeoutSeconds": { + "type": "integer", + "description": "Timeout in seconds for readiness probe", + "minimum": 1, + "default": 1 + }, + "successThreshold": { + "type": "integer", + "description": "Minimum consecutive successes for the probe to be considered successful", + "minimum": 1, + "default": 1 + }, + "failureThreshold": { + "type": "integer", + "description": "Minimum consecutive failures for the probe to be considered failed", + "minimum": 1, + "default": 3 + } + } + } + } + }, + "hostAliases": { + "type": "array", + "description": "Host aliases to add to the pod's /etc/hosts file", + "items": { + "type": "object", + "properties": { + "ip": { + "type": "string", + "description": "IP address the hostnames resolve to" + }, + "hostnames": { + "type": "array", + "description": "Hostnames that resolve to the given IP", + "items": { + "type": "string" + } + } + }, + "required": ["ip", "hostnames"], + "additionalProperties": false + } + } + } +} diff --git a/charts/headlamp/values.yaml b/charts/headlamp/values.yaml new file mode 100644 index 0000000000..726fe29ec2 --- /dev/null +++ b/charts/headlamp/values.yaml @@ -0,0 +1,533 @@ +# Default values for headlamp. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# -- Number of desired pods +replicaCount: 1 + +image: + # -- Container image registry + registry: ghcr.io + # -- Container image name + repository: headlamp-k8s/headlamp + # -- Image pull policy. One of Always, Never, IfNotPresent + pullPolicy: IfNotPresent + # -- Container image tag, If "" uses appVersion in Chart.yaml + tag: "" + +# -- An optional list of references to secrets in the same namespace to use for pulling any of the images used +imagePullSecrets: [] +# -- Overrides the name of the chart +nameOverride: "" +# -- Overrides the full name of the chart +fullnameOverride: "" + +# -- Override the deployment namespace; defaults to .Release.Namespace +namespaceOverride: "" + +# -- An optional list of init containers to be run before the main containers. +initContainers: [] + +# -- An optional list of extra containers to be run along side the main containers. +extraContainers: [] + +config: + inCluster: true + inClusterContextName: "main" + # -- base url path at which headlamp should run + baseURL: "" + # -- session token TTL in seconds (default is 24 hours) + sessionTTL: 86400 + + # -- UNSAFE: authenticate every Headlamp user as the pod's service account when running in-cluster. + # This disables per-user authentication and is only safe behind an auth proxy (e.g. OIDC proxy). + unsafeUseServiceAccountToken: false + # -- path to the service account token file. + # Only used when unsafeUseServiceAccountToken is true. + # Defaults to /var/run/secrets/kubernetes.io/serviceaccount/token when empty. + serviceAccountTokenPath: "" + oidc: + # Option 1: + # @param config.oidc.secret - OIDC secret configuration + # If you want to use an existing secret, set create to false and provide the name of the secret. + # If you want to create a new secret, set create to true and provide the name of the secret. + # Also provide the values for clientID, clientSecret, issuerURL, and scopes. + # Example: + # config: + # oidc: + # secret: + # create: true + # name: oidc + secret: + # -- Generate OIDC secret. If true, will generate a secret using .config.oidc. + create: true + # -- Name of the OIDC secret. + name: oidc + + # Option 2: + # @param config.oidc - OIDC env configuration + # If you want to set the OIDC configuration directly, set the following values. + # Example: + # config: + # oidc: + # clientID: "clientID" + # clientSecret: "clientSecret" + # issuerURL: "issuerURL" + # scopes: "scopes" + + # -- OIDC client ID + clientID: "" + # -- OIDC client secret + clientSecret: "" + # -- OIDC issuer URL + issuerURL: "" + # -- OIDC scopes to be used + scopes: "" + # -- OIDC callback URL + callbackURL: "" + + # -- OIDC client to be used during token validation + validatorClientID: "" + # -- OIDC Issuer URL to be used during token validation + validatorIssuerURL: "" + # -- Use 'access_token' instead of 'id_token' when authenticating using OIDC + useAccessToken: false + # -- Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow + usePKCE: false + # -- Enable using OIDC cookie for authentication outside of cluster + useCookie: false + + # Option 3: + # @param config.oidc - External OIDC secret configuration + # If you want to use an external secret for OIDC configuration, enable this option. + # Provide the name of the secret to use. + # Example: + # config: + # oidc: + # secret: + # create: false + # externalSecret: + # enabled: true + # name: oidc + externalSecret: + enabled: false + name: "" + # -- Set to true if your external secret contains an OIDC_SCOPES key. + # When false (default), the -oidc-scopes argument is omitted so that + # a missing key does not produce an empty or unresolved argument. + hasScopes: false + + # -- URL to fetch additional user info for the /me endpoint. + # For oauth2proxy /oauth2/userinfo can be used. Empty and it will not be used. + meUserInfoURL: "" + + # -- directory to look for plugins + pluginsDir: "/headlamp/plugins" + # Bundled (static) plugins that ship inside the Headlamp image, such as the + # Prometheus plugin (the "Show Prometheus metrics" UI). + staticPlugins: + # -- Serve the bundled static plugins. Set to false to disable them (e.g. to + # hide the "Show Prometheus metrics" button) without relying on internal + # image paths; this sets HEADLAMP_STATIC_PLUGINS_DIR to an empty string so the backend skips them. + enabled: true + enableHelm: false + watchPlugins: false + # -- Default image to use when creating pod debug containers. If empty, Headlamp uses its built-in default. + podDebugImage: "" + # -- Default image to use when creating node shell pods. If empty, Headlamp uses its built-in default. + nodeShellImage: "" + # -- Default namespace to use when creating node shell pods. If empty, Headlamp uses its built-in default. + nodeShellNamespace: "" + # tlsCertPath: "/headlamp-cert/headlamp-ca.crt" + # tlsKeyPath: "/headlamp-cert/headlamp-tls.key" + clusterInventory: + # -- Enable experimental/alpha Cluster Inventory discovery. + enabled: false + # -- Experimental/alpha Cluster Inventory access providers config. Required when enabled. + accessProvidersConfig: {} + # accessProvidersConfig: + # providers: + # - name: secretreader + # execConfig: + # apiVersion: client.authentication.k8s.io/v1 + # command: /access-plugins/secretreader/bin/secretreader-plugin + # interactiveMode: Never + # provideClusterInfo: true + # - name: kubeconfig-secretreader + # execConfig: + # apiVersion: client.authentication.k8s.io/v1 + # command: /access-plugins/kubeconfig-secretreader/bin/kubeconfig-secretreader-plugin + # interactiveMode: Never + # provideClusterInfo: true + # plugins[] uses the Kubernetes "image" volume type to mount experimental/alpha access provider binaries. + plugins: [] + # plugins: + # - name: secretreader + # image: registry.k8s.io/cluster-inventory-api/secretreader:v0.1.3@sha256:ec3090dc166aa2b42fb35d714d161c417d8b27bbc463404c8f615f5f4c610a1d + # mountPath: /access-plugins/secretreader + # - name: kubeconfig-secretreader + # image: registry.k8s.io/cluster-inventory-api/kubeconfig-secretreader:v0.1.3@sha256:b92966cc6e4ac78002a63862921022a71d54956826f6e4febcb7247495eb98c0 + # mountPath: /access-plugins/kubeconfig-secretreader + # -- Kubernetes label selector used to filter experimental/alpha ClusterProfile resources. + labelSelector: "!headlamp.dev/ignore" + # -- Override the experimental/alpha Cluster Inventory root reconcile interval. Empty uses the Headlamp default. + rootReconcileInterval: "" + # -- Override the experimental/alpha Cluster Inventory no-CRD cache TTL. Empty uses the Headlamp default. + noCRDCacheTTL: "" + # Extra arguments that can be given to the container. See charts/headlamp/README.md for more information. + extraArgs: [] + +# -- An optional list of environment variables +# env: +# - name: KUBERNETES_SERVICE_HOST +# value: "localhost" +# - name: KUBERNETES_SERVICE_PORT +# value: "6443" + +# -- Mount Service Account token in pod +automountServiceAccountToken: true + +serviceAccount: + # -- Specifies whether a service account should be created + create: true + # -- Annotations to add to the service account + annotations: {} + # -- The name of the service account to use.(If not set and create is true, a name is generated using the fullname template) + name: "" + +clusterRoleBinding: + # -- Specified whether a cluster role binding should be created + create: true + # -- Set name of the Cluster Role with limited permissions from you cluster + # for example - clusterRoleName: user-ro + clusterRoleName: cluster-admin + # -- Annotations to add to the cluster role binding + annotations: {} + +# -- Annotations to add to the deployment +deploymentAnnotations: {} + +# -- Annotations to add to the pod +podAnnotations: {} + +# -- Labels to add to the pod +podLabels: {} + +# -- Controls user namespace isolation for the Headlamp pod. +# When true (default), the pod shares the host user namespace (user namespaces are DISABLED). +# When false, the pod uses a separate user namespace (user namespaces are ENABLED) for stronger isolation, +# if supported by the cluster. Set this to false if your cluster supports user namespaces and you want +# additional isolation; leave as true if user namespaces are not available. +# See: https://kubernetes.io/docs/concepts/workloads/pods/user-namespaces/ +hostUsers: true + +# -- Headlamp pod's Security Context +podSecurityContext: + {} + # fsGroup: 2000 + +# -- Headlamp containers Security Context +# When readOnlyRootFilesystem: true is set, the chart automatically adds a +# writable emptyDir volume named "headlamp-tmp" mounted at /tmp so the +# application can write temporary files. +# You can override this behavior in two ways: +# 1. Supply your own volumeMount with mountPath: /tmp — the chart will skip +# adding both the automatic mount and the automatic volume. +# 2. Supply your own volume named "headlamp-tmp" (e.g. to set sizeLimit) — +# the chart will skip creating the volume but will still add the /tmp mount +# pointing to your volume. +securityContext: + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + runAsNonRoot: true + privileged: false + runAsUser: 100 + runAsGroup: 101 +# Uses these defaults if this is empty. +# allowPrivilegeEscalation: false +# runAsNonRoot: true +# seccompProfile: +# type: RuntimeDefault +# capabilities: +# drop: +# - ALL + + +service: + # -- Annotations to add to the service + annotations: {} + # -- Kubernetes Service type + type: ClusterIP + # -- Kubernetes Service port + port: 80 + # -- Kubernetes Service port appProtocol (for the main http port) + appProtocol: null + # -- Kubernetes Service clusterIP + clusterIP: "" + # -- Kubernetes Service loadBalancerIP + loadBalancerIP: "" + # -- Kubernetes Service loadBalancerSourceRanges + loadBalancerSourceRanges: [] + # -- Kubernetes Service Nodeport + nodePort: null + # -- Additional ports to expose on the Service in addition to the default + # http port. Each entry must have a unique `name` and a `port`. `targetPort` + # defaults to `port` when omitted. `nodePort` is only honored when + # `service.type` is `NodePort` or `LoadBalancer`. A matching containerPort + # must be provided by the user (e.g. via a sidecar) for traffic to actually + # be routed. + extraServicePorts: [] + # - name: extra + # port: 9090 + # targetPort: extra + # protocol: TCP + # nodePort: null + +# -- Headlamp containers volume mounts +volumeMounts: [] + +# -- Headlamp pod's volumes +volumes: [] + +persistentVolumeClaim: + # -- Enable Persistent Volume Claim + enabled: false + # -- Annotations to add to the persistent volume claim (if enabled) + annotations: + {} + # -- accessModes for the persistent volume claim, eg: ReadWriteOnce, ReadOnlyMany, ReadWriteMany etc. + accessModes: [] + # -- size of the persistent volume claim, eg: 10Gi. Required if enabled is true. + size: "" + # -- storageClassName for the persistent volume claim. + storageClassName: "" + # -- selector for the persistent volume claim. + selector: {} + # -- volumeMode for the persistent volume claim, eg: Filesystem, Block. + volumeMode: "" + +ingress: + # -- Enable ingress controller resource + enabled: false + # -- Annotations for Ingress resource + annotations: + {} + # kubernetes.io/tls-acme: "true" + + # -- Additional labels to add to the Ingress resource + labels: {} + # app.kubernetes.io/part-of: traefik + # environment: prod + + # -- Ingress class name. replacement for the deprecated "kubernetes.io/ingress.class" annotation + ingressClassName: "" + + # -- Hostname(s) for the Ingress resource + # Please refer to https://kubernetes.io/docs/reference/kubernetes-api/service-resources/ingress-v1/#IngressSpec for more information. + # Each path may optionally specify `backend.service.{name,port}` to override + # the default Headlamp Service / `service.port`. `name` supports `tpl` so + # values like `{{ .Release.Name }}-extra` can be used. `port` accepts either + # `number` or `name` (matching `service.extraServicePorts[].name`). + hosts: + [] + # - host: chart-example.local + # paths: + # - path: / + # type: ImplementationSpecific + # - path: /extra + # type: ImplementationSpecific + # backend: + # service: + # port: + # name: extra + # -- Ingress TLS configuration + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# HTTPRoute configuration for Gateway API +# Please refer to https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.HTTPRoute +httpRoute: + # -- Enable HTTPRoute resource for Gateway API + enabled: false + # -- Annotations for HTTPRoute resource + annotations: {} + # -- Additional labels for HTTPRoute resource + labels: {} + # -- Parent references (REQUIRED when enabled - HTTPRoute will not work without this) + # Example: + # parentRefs: + # - name: my-gateway + # namespace: gateway-namespace + parentRefs: [] + # -- Hostnames for the HTTPRoute + # Example: + # hostnames: + # - headlamp.example.com + hostnames: [] + # -- Custom routing rules (optional, defaults to path prefix /) + # If not specified, a default rule routing all traffic to the service is used + rules: [] + # Example custom rules: + # rules: + # - matches: + # - path: + # type: PathPrefix + # value: /headlamp + # backendRefs: + # - name: "{{ .Release.Name }}-headlamp" + # port: 80 + +# -- CPU/Memory resource requests/limits +resources: + {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# -- Node labels for pod assignment +nodeSelector: {} + +# -- Toleration labels for pod assignment +tolerations: [] + +# -- Affinity settings for pod assignment +affinity: {} + +# -- Host aliases to add to the pod's /etc/hosts file +hostAliases: [] + +# -- Topology Spread Constraints for pod assignment +topologySpreadConstraints: [] +# - maxSkew: 1 +# topologyKey: topology.kubernetes.io/zone +# whenUnsatisfiable: ScheduleAnyway +# matchLabelKeys: +# - pod-template-hash +# - maxSkew: 1 +# topologyKey: kubernetes.io/hostname +# whenUnsatisfiable: DoNotSchedule +# matchLabelKeys: +# - pod-template-hash + +# -- Pod priority class +priorityClassName: "" + +# Probe configuration for liveness and readiness checks +probes: + # -- Scheme to use for liveness and readiness probes (HTTP or HTTPS). + # Set to HTTPS when TLS is enabled at the backend server. + scheme: HTTP + livenessProbe: + # -- Initial delay in seconds before starting liveness probe + initialDelaySeconds: 0 + # -- Period in seconds between liveness probe checks + periodSeconds: 10 + # -- Timeout in seconds for liveness probe + timeoutSeconds: 1 + # -- Minimum consecutive successes for the probe to be considered successful (must be 1 for liveness probes per Kubernetes API) + successThreshold: 1 + # -- Minimum consecutive failures for the probe to be considered failed + failureThreshold: 3 + readinessProbe: + # -- Initial delay in seconds before starting readiness probe + initialDelaySeconds: 0 + # -- Period in seconds between readiness probe checks + periodSeconds: 10 + # -- Timeout in seconds for readiness probe + timeoutSeconds: 1 + # -- Minimum consecutive successes for the probe to be considered successful + successThreshold: 1 + # -- Minimum consecutive failures for the probe to be considered failed + failureThreshold: 3 + +# Plugin Manager Sidecar Container Configuration +pluginsManager: + # -- Enable plugin manager + enabled: false + # -- Plugin configuration file name + configFile: "plugin.yml" + # -- Plugin configuration content in YAML format. This is required if plugins.enabled is true. + configContent: "" + # -- Base node image to use + baseImage: node:lts-alpine + # -- Headlamp plugin package version to install + version: latest + # -- Plugin manager containers volume mounts + volumeMounts: [] + # -- Plugin manager env variable configuration + # env: + # - name: HTTPS_PROXY + # value: "proxy.example.com:8080" + # -- Specify resrouces + # resources: + # requests: + # cpu: "500m" + # memory: "2048Mi" + # limits: + # cpu: "1000m" + # memory: "4096Mi" + # If omitted, the plugin manager will inherit the global securityContext. + # When readOnlyRootFilesystem: true is active (set here or inherited), the + # chart automatically adds a writable emptyDir volume named + # "headlamp-plugins-tmp" mounted at /tmp in the plugin manager container. + # Override behavior mirrors the main container: supply your own /tmp + # volumeMount to skip both, or supply your own "headlamp-plugins-tmp" volume + # (e.g. to set sizeLimit) to have the chart only add the mount. + securityContext: + {} + # runAsUser: 1001 + # runAsNonRoot: true + # allowPrivilegeEscalation: false + # readOnlyRootFilesystem: true + # capabilities: + # drop: + # - ALL + +podDisruptionBudget: + # -- enable PodDisruptionBudget + # ref: https://kubernetes.io/docs/concepts/workloads/pods/disruptions/ + enabled: false + # @schema + # type: [null, integer, string] + # @schema + # -- Minimum number/percentage of pods that should remain scheduled. + # When it's set, maxUnavailable must be disabled by `maxUnavailable: null` + minAvailable: 0 + # @schema + # type: [null, integer, string] + # @schema + # -- Maximum number/percentage of pods that may be made unavailable + maxUnavailable: null + # @schema + # type: [null, string] + # @schema + # -- How are unhealthy, but running, pods counted for eviction + unhealthyPodEvictionPolicy: null + +# -- Additional Kubernetes manifests to be deployed. Include the manifest as nested YAML. +extraManifests: [] +# - | +# apiVersion: v1 +# kind: ConfigMap +# metadata: +# name: my-config +# data: +# key: value +# - | +# apiVersion: v1 +# kind: ConfigMap +# metadata: +# name: my-config-too +# data: +# key: value diff --git a/core.yaml b/core.yaml index 2cb473d426..d595cd8883 100644 --- a/core.yaml +++ b/core.yaml @@ -27,6 +27,8 @@ k8s: disableIstioInjection: true - name: grafana app: grafana + - name: headlamp + app: headlamp - name: istio-system disableIstioInjection: true - name: keycloak @@ -105,6 +107,9 @@ adminApps: tags: [ingress, security, tls] - name: cnpg tags: [database] + - name: headlamp + tags: [custom] + ownHost: true - name: tty tags: [tty] isShared: true diff --git a/helmfile.d/helmfile-92.headlamp.yaml.gotmpl b/helmfile.d/helmfile-92.headlamp.yaml.gotmpl new file mode 100644 index 0000000000..0fde4f3849 --- /dev/null +++ b/helmfile.d/helmfile-92.headlamp.yaml.gotmpl @@ -0,0 +1,32 @@ +bases: + - snippets/defaults.yaml +--- +bases: + - snippets/defaults.gotmpl +--- +bases: + - snippets/env.gotmpl +--- +bases: + - snippets/derived.gotmpl +--- +{{ readFile "snippets/templates.gotmpl" }} +{{- $v := .Values }} +{{- $a := $v.apps }} + +releases: + - name: headlamp-artifacts + installed: {{ $a | get "headlamp.enabled" }} + namespace: headlamp + labels: + pkg: headlamp + app: core + <<: *raw + - name: headlamp + installed: {{ $a | get "headlamp.enabled" }} + namespace: headlamp + labels: + pkg: headlamp + app: core + <<: *default + diff --git a/helmfile.d/snippets/defaults.yaml b/helmfile.d/snippets/defaults.yaml index 7d9d569960..23c06a9570 100644 --- a/helmfile.d/snippets/defaults.yaml +++ b/helmfile.d/snippets/defaults.yaml @@ -264,6 +264,9 @@ environments: memory: 512Mi cpu: 200m _rawValues: {} + headlamp: + enabled: false + _rawValues: {} istio: autoscaling: egressgateway: @@ -1166,3 +1169,4 @@ environments: users: [] versions: specVersion: 71 + diff --git a/tests/fixtures/env/apps/headlamp.yaml b/tests/fixtures/env/apps/headlamp.yaml new file mode 100644 index 0000000000..22f92ec0a3 --- /dev/null +++ b/tests/fixtures/env/apps/headlamp.yaml @@ -0,0 +1,6 @@ +kind: AplApp +metadata: + name: headlamp +spec: + _rawValues: {} + enabled: true diff --git a/tests/integration/full.yaml b/tests/integration/full.yaml index 544158028f..f46eb060b3 100644 --- a/tests/integration/full.yaml +++ b/tests/integration/full.yaml @@ -7,6 +7,8 @@ otomi: version: 'OTOMI_VERSION_PLACEHOLDER' adminPassword: welcomeotomi apps: + headlamp: + enabled: false otomi-api: _rawValues: image: @@ -118,3 +120,4 @@ teamConfig: - HELLO name: otomi-welcome type: generic + diff --git a/tests/integration/minimal-with-team.yaml b/tests/integration/minimal-with-team.yaml index 47feaf197f..9b62ffe39c 100644 --- a/tests/integration/minimal-with-team.yaml +++ b/tests/integration/minimal-with-team.yaml @@ -6,6 +6,8 @@ otomi: version: 'OTOMI_VERSION_PLACEHOLDER' adminPassword: welcomeotomi apps: + headlamp: + enabled: false otomi-api: _rawValues: image: @@ -174,3 +176,4 @@ files: tag: v1.5.1 env/teams/admin/workloadValues/nodejs-helloworld.managed.yaml: | {} + diff --git a/tests/integration/minimal.yaml b/tests/integration/minimal.yaml index 2f0fddab27..879287b7f9 100644 --- a/tests/integration/minimal.yaml +++ b/tests/integration/minimal.yaml @@ -5,6 +5,8 @@ cluster: otomi: version: 'OTOMI_VERSION_PLACEHOLDER' apps: + headlamp: + enabled: false otomi-api: _rawValues: image: @@ -44,3 +46,4 @@ apps: image: repository: docker.io/linode/apl-core imagePullPolicy: Always + diff --git a/tests/integration/monitoring-with-team.yaml b/tests/integration/monitoring-with-team.yaml index 4336349e60..ab59a546ff 100644 --- a/tests/integration/monitoring-with-team.yaml +++ b/tests/integration/monitoring-with-team.yaml @@ -6,6 +6,8 @@ otomi: version: 'OTOMI_VERSION_PLACEHOLDER' adminPassword: welcomeotomi apps: + headlamp: + enabled: false otomi-api: _rawValues: image: @@ -189,3 +191,4 @@ files: tag: v1.5.1 env/teams/admin/workloadValues/nodejs-helloworld.managed.yaml: | {} + diff --git a/values/headlamp/headlamp-raw.gotmpl b/values/headlamp/headlamp-raw.gotmpl new file mode 100644 index 0000000000..0c32f2a875 --- /dev/null +++ b/values/headlamp/headlamp-raw.gotmpl @@ -0,0 +1,69 @@ +{{- $v := .Values }} +{{- $httpRoute := tpl (readFile "../../helmfile.d/snippets/routes.gotmpl") $v | fromYaml }} +resources: + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: headlamp-auth-redirects + {{ with $httpRoute.annotations }} + annotations: + {{ . | toYaml | nindent 8 }} + {{- end }} + spec: + parentRefs: + {{- $httpRoute.parentRefs | toYaml | nindent 8 }} + hostnames: + - {{ printf "headlamp.%s" $v.cluster.domainSuffix }} + rules: + {{- $httpRoute.authRules | toYaml | nindent 8 }} + - apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: headlamp + {{ with $httpRoute.annotations }} + annotations: + {{ . | toYaml | nindent 8 }} + {{- end }} + spec: + parentRefs: + {{- $httpRoute.parentRefs | toYaml | nindent 8 }} + hostnames: + - {{ printf "headlamp.%s" $v.cluster.domainSuffix }} + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - kind: Service + name: headlamp + port: 80 + - apiVersion: security.istio.io/v1 + kind: RequestAuthentication + metadata: + name: headlamp-auth + namespace: headlamp + spec: + selector: + matchLabels: + app.kubernetes.io/name: headlamp + jwtRules: + - issuer: {{ $v._derived.oidcBaseUrl }} + jwksUri: {{ $v._derived.oidcBaseUrlBackchannel }}/protocol/openid-connect/certs + fromHeaders: + - name: Authorization + prefix: "Bearer " + - apiVersion: security.istio.io/v1 + kind: AuthorizationPolicy + metadata: + name: headlamp-authz + namespace: headlamp + spec: + action: ALLOW + selector: + matchLabels: + app.kubernetes.io/name: headlamp + rules: + - from: + - source: + requestPrincipals: ["*"] diff --git a/values/headlamp/headlamp.gotmpl b/values/headlamp/headlamp.gotmpl new file mode 100644 index 0000000000..2d278d084a --- /dev/null +++ b/values/headlamp/headlamp.gotmpl @@ -0,0 +1,3 @@ +podLabels: + otomi.io/auth: platform + otomi.io/auth-policy: platform