Compare commits

..

1 Commits

Author SHA1 Message Date
Timofei Larkin
15ce380f14 [backups][WIP] Draft Job strategy implementation
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2025-12-12 16:12:57 +03:00
1679 changed files with 89448 additions and 180844 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @kvaps @lllamnyp @lexfrei @androndo @IvanHunters @sircthulhu
* @kvaps @lllamnyp @nbykov0

View File

@@ -1,188 +0,0 @@
name: Auto Patch Release
on:
schedule:
# Run daily at 2:00 AM CET (1:00 UTC in winter, 0:00 UTC in summer)
# Using 1:00 UTC to approximate 2:00 AM CET
- cron: '0 1 * * *'
workflow_dispatch: # Allow manual trigger
concurrency:
group: auto-release-${{ github.workflow }}
cancel-in-progress: false
jobs:
auto-release:
name: Auto Patch Release
runs-on: [self-hosted]
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Configure git
env:
GH_PAT: ${{ secrets.GH_PAT }}
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
git config --unset-all http.https://github.com/.extraheader || true
- name: Process release branches
uses: actions/github-script@v7
env:
GH_PAT: ${{ secrets.GH_PAT }}
with:
github-token: ${{ secrets.GH_PAT }}
script: |
const { execSync } = require('child_process');
// Configure git to use PAT for authentication
execSync('git config user.name "cozystack-bot"', { encoding: 'utf8' });
execSync('git config user.email "217169706+cozystack-bot@users.noreply.github.com"', { encoding: 'utf8' });
execSync(`git remote set-url origin https://cozystack-bot:${process.env.GH_PAT}@github.com/${process.env.GITHUB_REPOSITORY}`, { encoding: 'utf8' });
// Remove GITHUB_TOKEN extraheader to ensure PAT is used (needed to trigger other workflows)
execSync('git config --unset-all http.https://github.com/.extraheader || true', { encoding: 'utf8' });
// Get all release-X.Y branches
const branches = execSync('git branch -r | grep -E "origin/release-[0-9]+\\.[0-9]+$" | sed "s|origin/||" | tr -d " "', { encoding: 'utf8' })
.split('\n')
.filter(b => b.trim())
.filter(b => /^release-\d+\.\d+$/.test(b));
console.log(`Found ${branches.length} release branches: ${branches.join(', ')}`);
// Get all published releases (not draft)
const allReleases = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100
});
// Filter to only published releases (not draft) with tags matching vX.Y.Z (no suffixes)
const publishedReleases = allReleases.data
.filter(r => !r.draft)
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name));
console.log(`Found ${publishedReleases.length} published releases without suffixes`);
for (const branch of branches) {
console.log(`\n=== Processing branch: ${branch} ===`);
// Extract X.Y from branch name (release-X.Y)
const match = branch.match(/^release-(\d+\.\d+)$/);
if (!match) {
console.log(` ⚠️ Branch ${branch} doesn't match pattern, skipping`);
continue;
}
const [major, minor] = match[1].split('.');
const versionPrefix = `v${major}.${minor}.`;
console.log(` Looking for releases with prefix: ${versionPrefix}`);
// Find the latest published release for this branch (vX.Y.Z without suffixes)
const branchReleases = publishedReleases
.filter(r => r.tag_name.startsWith(versionPrefix))
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name)); // Ensure no suffixes
if (branchReleases.length === 0) {
console.log(` ⚠️ No published releases found for ${branch}, skipping`);
continue;
}
// Sort by version (descending) to get the latest
branchReleases.sort((a, b) => {
const aVersion = a.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
const bVersion = b.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
if (!aVersion || !bVersion) return 0;
const aNum = parseInt(aVersion[1]) * 10000 + parseInt(aVersion[2]) * 100 + parseInt(aVersion[3]);
const bNum = parseInt(bVersion[1]) * 10000 + parseInt(bVersion[2]) * 100 + parseInt(bVersion[3]);
return bNum - aNum;
});
const latestRelease = branchReleases[0];
console.log(` ✅ Latest published release: ${latestRelease.tag_name}`);
// Get the commit SHA for this release tag
let releaseCommitSha;
try {
releaseCommitSha = execSync(`git rev-list -n 1 ${latestRelease.tag_name}`, { encoding: 'utf8' }).trim();
console.log(` Release commit SHA: ${releaseCommitSha}`);
} catch (error) {
console.log(` ⚠️ Could not find commit for tag ${latestRelease.tag_name}, skipping`);
continue;
}
// Checkout the branch
execSync(`git fetch origin ${branch}:${branch}`, { encoding: 'utf8' });
execSync(`git checkout ${branch}`, { encoding: 'utf8' });
// Get the latest commit on the branch
const latestBranchCommit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
console.log(` Latest branch commit: ${latestBranchCommit}`);
// Check if there are new commits after the release
const commitsAfterRelease = execSync(
`git rev-list ${releaseCommitSha}..HEAD --oneline`,
{ encoding: 'utf8' }
).trim();
if (!commitsAfterRelease) {
console.log(` No new commits after ${latestRelease.tag_name}, skipping`);
continue;
}
console.log(` ✅ Found new commits after release:`);
console.log(commitsAfterRelease);
// Calculate next version (Z+1)
const versionMatch = latestRelease.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
if (!versionMatch) {
console.log(` ❌ Could not parse version from ${latestRelease.tag_name}, skipping`);
continue;
}
const nextPatch = parseInt(versionMatch[3]) + 1;
const nextTag = `v${versionMatch[1]}.${versionMatch[2]}.${nextPatch}`;
console.log(` 🏷️ Creating new tag: ${nextTag} on commit ${latestBranchCommit}`);
// Create and push the tag with base_ref for workflow triggering
try {
// Delete local tag if exists to force update
try {
execSync(`git tag -d ${nextTag}`, { encoding: 'utf8' });
} catch (e) {
// Tag doesn't exist locally, that's fine
}
// Delete remote tag if exists
try {
execSync(`git push origin :refs/tags/${nextTag}`, { encoding: 'utf8' });
} catch (e) {
// Tag doesn't exist remotely, that's fine
}
// Create tag locally
execSync(`git tag ${nextTag} ${latestBranchCommit}`, { encoding: 'utf8' });
// Push tag with HEAD reference to preserve base_ref
execSync(`git push origin HEAD:refs/tags/${nextTag}`, { encoding: 'utf8' });
console.log(` ✅ Successfully created and pushed tag ${nextTag}`);
} catch (error) {
console.log(` ❌ Error creating/pushing tag ${nextTag}: ${error.message}`);
core.setFailed(`Failed to create tag ${nextTag} for branch ${branch}`);
}
}
console.log(`\n✅ Finished processing all release branches`);

View File

@@ -46,12 +46,7 @@ jobs:
fetch-depth: 0
- name: Create tag on merge commit
env:
GH_PAT: ${{ secrets.GH_PAT }}
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
git tag -f ${{ steps.get_tag.outputs.tag }} ${{ github.sha }}
git push -f origin ${{ steps.get_tag.outputs.tag }}
@@ -110,95 +105,67 @@ jobs:
}
}
# Publish draft release and ensure correct latest flag
- name: Publish draft release
# Get the latest published release
- name: Get the latest published release
id: latest_release
uses: actions/github-script@v7
with:
script: |
const tag = '${{ steps.get_tag.outputs.tag }}';
try {
const rel = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo
});
core.setOutput('tag', rel.data.tag_name);
} catch (_) {
core.setOutput('tag', '');
}
# Compare current tag vs latest using semver-utils
- name: Semver compare
id: semver
uses: madhead/semver-utils@v4.3.0
with:
version: ${{ steps.get_tag.outputs.tag }}
compare-to: ${{ steps.latest_release.outputs.tag }}
# Derive flags: prerelease? make_latest?
- name: Calculate publish flags
id: flags
uses: actions/github-script@v7
with:
script: |
const tag = '${{ steps.get_tag.outputs.tag }}'; // v0.31.5-rc.1
const m = tag.match(/^v(\d+\.\d+\.\d+)(-(?:alpha|beta|rc)\.\d+)?$/);
if (!m) {
core.setFailed(`❌ tag '${tag}' must match 'vX.Y.Z' or 'vX.Y.Z-(alpha|beta|rc).N'`);
return;
}
const isRc = Boolean(m[2]);
const version = m[1] + (m[2] ?? ''); // 0.31.5-rc.1
const isRc = Boolean(m[2]);
core.setOutput('is_rc', isRc);
const outdated = '${{ steps.semver.outputs.comparison-result }}' === '<';
core.setOutput('make_latest', isRc || outdated ? 'false' : 'legacy');
// Parse semver string to comparable numbers
function parseSemver(v) {
const match = v.replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/);
if (!match) return null;
return {
major: parseInt(match[1]),
minor: parseInt(match[2]),
patch: parseInt(match[3])
};
}
// Compare two semver objects
function compareSemver(a, b) {
if (a.major !== b.major) return a.major - b.major;
if (a.minor !== b.minor) return a.minor - b.minor;
return a.patch - b.patch;
}
const currentSemver = parseSemver(tag);
// Get all releases
# Publish draft release with correct flags
- name: Publish draft release
uses: actions/github-script@v7
with:
script: |
const tag = '${{ steps.get_tag.outputs.tag }}';
const releases = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100
repo: context.repo.repo
});
// Find draft release to publish
const draft = releases.data.find(r => r.tag_name === tag && r.draft);
if (!draft) throw new Error(`Draft release for ${tag} not found`);
// Find max semver among published releases (excluding current draft)
const publishedReleases = releases.data
.filter(r => !r.draft && !r.prerelease)
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name))
.map(r => ({ id: r.id, tag: r.tag_name, semver: parseSemver(r.tag_name) }))
.filter(r => r.semver !== null);
let maxRelease = null;
for (const rel of publishedReleases) {
if (!maxRelease || compareSemver(rel.semver, maxRelease.semver) > 0) {
maxRelease = rel;
}
}
// Determine if this release should be latest
const isOutdated = maxRelease && compareSemver(currentSemver, maxRelease.semver) < 0;
const makeLatest = (isRc || isOutdated) ? 'false' : 'true';
if (isRc) {
console.log(`🏷️ ${tag} is a prerelease, make_latest: false`);
} else if (isOutdated) {
console.log(`🏷️ ${tag} < ${maxRelease.tag} (max semver), make_latest: false`);
} else {
console.log(`🏷️ ${tag} is the highest version, make_latest: true`);
}
// Publish the release
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: draft.id,
draft: false,
prerelease: isRc,
make_latest: makeLatest
owner: context.repo.owner,
repo: context.repo.repo,
release_id: draft.id,
draft: false,
prerelease: ${{ steps.flags.outputs.is_rc }},
make_latest: '${{ steps.flags.outputs.make_latest }}'
});
console.log(`🚀 Published release ${tag}`);
// If this is a backport/outdated release, ensure the correct release is marked as latest
if (isOutdated && maxRelease) {
console.log(`🔧 Ensuring ${maxRelease.tag} remains the latest release...`);
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: maxRelease.id,
make_latest: 'true'
});
console.log(`✅ Restored ${maxRelease.tag} as latest release`);
}
console.log(`🚀 Published release for ${tag}`);

View File

@@ -6,6 +6,8 @@ env:
on:
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- 'docs/**/*'
# Cancel inflight runs for the same PR when a new push arrives.
concurrency:
@@ -13,19 +15,6 @@ concurrency:
cancel-in-progress: true
jobs:
detect-changes:
name: Detect changes
runs-on: ubuntu-latest
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
code:
- '!docs/**'
build:
name: Build
runs-on: [self-hosted]
@@ -33,11 +22,9 @@ jobs:
contents: read
packages: write
needs: ["detect-changes"]
# Never run when the PR carries the "release" label or only docs changed.
# Never run when the PR carries the "release" label.
if: |
needs.detect-changes.outputs.code == 'true'
&& !contains(github.event.pull_request.labels.*.name, 'release')
!contains(github.event.pull_request.labels.*.name, 'release')
steps:
- name: Checkout code
@@ -71,7 +58,7 @@ jobs:
DOCKER_CONFIG: ${{ runner.temp }}/.docker
- name: Build Talos image
run: make -C packages/core/talos talos-nocloud
run: make -C packages/core/installer talos-nocloud
- name: Save git diff as patch
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
@@ -84,6 +71,12 @@ jobs:
name: pr-patch
path: _out/assets/pr.patch
- name: Upload installer
uses: actions/upload-artifact@v4
with:
name: cozystack-installer
path: _out/assets/cozystack-installer.yaml
- name: Upload Talos image
uses: actions/upload-artifact@v4
with:
@@ -95,7 +88,8 @@ jobs:
runs-on: ubuntu-latest
if: contains(github.event.pull_request.labels.*.name, 'release')
outputs:
disk_id: ${{ steps.fetch_assets.outputs.disk_id }}
installer_id: ${{ steps.fetch_assets.outputs.installer_id }}
disk_id: ${{ steps.fetch_assets.outputs.disk_id }}
steps:
- name: Checkout code
@@ -138,18 +132,19 @@ jobs:
return;
}
const find = (n) => draft.assets.find(a => a.name === n)?.id;
const diskId = find('nocloud-amd64.raw.xz');
if (!diskId) {
const installerId = find('cozystack-installer.yaml');
const diskId = find('nocloud-amd64.raw.xz');
if (!installerId || !diskId) {
core.setFailed('Required assets missing in draft release');
return;
}
core.setOutput('disk_id', diskId);
core.setOutput('installer_id', installerId);
core.setOutput('disk_id', diskId);
e2e:
name: "E2E Tests"
runs-on: ${{ contains(github.event.pull_request.labels.*.name, 'debug') && 'self-hosted' || 'oracle-vm-24cpu-96gb-x86-64' }}
#runs-on: [oracle-vm-32cpu-128gb-x86-64]
prepare_env:
name: "Prepare environment"
runs-on: [self-hosted]
permissions:
contents: read
packages: read
@@ -195,7 +190,7 @@ jobs:
- name: Set sandbox ID
run: echo "SANDBOX_NAME=cozy-e2e-sandbox-$(echo "${GITHUB_REPOSITORY}:${GITHUB_WORKFLOW}:${GITHUB_REF}" | sha256sum | cut -c1-10)" >> $GITHUB_ENV
# ▸ Prepare environment
# ▸ Start actual job steps
- name: Prepare workspace
run: |
rm -rf /tmp/$SANDBOX_NAME
@@ -215,7 +210,47 @@ jobs:
done
echo "✅ The task completed successfully after $attempt attempts"
# ▸ Install Cozystack
install_cozystack:
name: "Install Cozystack"
runs-on: [self-hosted]
permissions:
contents: read
packages: read
needs: ["prepare_env", "resolve_assets"]
if: ${{ always() && needs.prepare_env.result == 'success' }}
steps:
- name: Prepare _out/assets directory
run: mkdir -p _out/assets
# ▸ Regular PR path download artefacts produced by the *build* job
- name: "Download installer (regular PR)"
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
uses: actions/download-artifact@v4
with:
name: cozystack-installer
path: _out/assets
# ▸ Release PR path fetch artefacts from the corresponding draft release
- name: Download assets from draft release (release PR)
if: contains(github.event.pull_request.labels.*.name, 'release')
run: |
mkdir -p _out/assets
curl -sSL -H "Authorization: token ${GH_PAT}" -H "Accept: application/octet-stream" \
-o _out/assets/cozystack-installer.yaml \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${{ needs.resolve_assets.outputs.installer_id }}"
env:
GH_PAT: ${{ secrets.GH_PAT }}
# ▸ Start actual job steps
- name: Set sandbox ID
run: echo "SANDBOX_NAME=cozy-e2e-sandbox-$(echo "${GITHUB_REPOSITORY}:${GITHUB_WORKFLOW}:${GITHUB_REF}" | sha256sum | cut -c1-10)" >> $GITHUB_ENV
- name: Sync _out/assets directory
run: |
mkdir -p /tmp/$SANDBOX_NAME/_out/assets
mv _out/assets/* /tmp/$SANDBOX_NAME/_out/assets/
- name: Install Cozystack into sandbox
run: |
cd /tmp/$SANDBOX_NAME
@@ -228,77 +263,107 @@ jobs:
fi
echo "❌ Attempt $attempt failed, retrying..."
done
echo "✅ The task completed successfully after $attempt attempts"
echo "✅ The task completed successfully after $attempt attempts."
- name: Run OpenAPI tests
run: |
cd /tmp/$SANDBOX_NAME
make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME test-openapi
# ▸ Run E2E tests
- name: Run E2E tests
id: e2e_tests
run: |
cd /tmp/$SANDBOX_NAME
failed_tests=""
for app in $(ls hack/e2e-apps/*.bats | xargs -n1 basename | cut -d. -f1); do
echo "::group::Testing $app"
attempt=0
success=false
until [ $attempt -ge 3 ]; do
if make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME test-apps-$app; then
success=true
break
fi
attempt=$((attempt + 1))
echo "❌ Attempt $attempt failed, retrying..."
done
if [ "$success" = true ]; then
echo "✅ Test $app completed successfully"
else
echo "❌ Test $app failed after $attempt attempts"
failed_tests="$failed_tests $app"
fi
echo "::endgroup::"
done
if [ -n "$failed_tests" ]; then
echo "❌ Failed tests:$failed_tests"
exit 1
fi
echo "✅ All E2E tests passed"
detect_test_matrix:
name: "Detect e2e test matrix"
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set.outputs.matrix }}
# ▸ Collect debug information (always runs)
- name: Collect report
if: always()
steps:
- uses: actions/checkout@v4
- id: set
run: |
apps=$(ls hack/e2e-apps/*.bats | cut -f3 -d/ | cut -f1 -d. | jq -R | jq -cs)
echo "matrix={\"app\":$apps}" >> "$GITHUB_OUTPUT"
test_apps:
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.detect_test_matrix.outputs.matrix) }}
name: Test ${{ matrix.app }}
runs-on: [self-hosted]
needs: [install_cozystack,detect_test_matrix]
if: ${{ always() && (needs.install_cozystack.result == 'success' && needs.detect_test_matrix.result == 'success') }}
steps:
- name: Set sandbox ID
run: echo "SANDBOX_NAME=cozy-e2e-sandbox-$(echo "${GITHUB_REPOSITORY}:${GITHUB_WORKFLOW}:${GITHUB_REF}" | sha256sum | cut -c1-10)" >> $GITHUB_ENV
- name: E2E Apps
run: |
cd /tmp/$SANDBOX_NAME
make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME collect-report || true
attempt=0
until make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME test-apps-${{ matrix.app }}; do
attempt=$((attempt + 1))
if [ $attempt -ge 3 ]; then
echo "❌ Attempt $attempt failed, exiting..."
exit 1
fi
echo "❌ Attempt $attempt failed, retrying..."
done
echo "✅ The task completed successfully after $attempt attempts"
collect_debug_information:
name: Collect debug information
runs-on: [self-hosted]
needs: [test_apps]
if: ${{ always() }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set sandbox ID
run: echo "SANDBOX_NAME=cozy-e2e-sandbox-$(echo "${GITHUB_REPOSITORY}:${GITHUB_WORKFLOW}:${GITHUB_REF}" | sha256sum | cut -c1-10)" >> $GITHUB_ENV
- name: Collect report
run: |
cd /tmp/$SANDBOX_NAME
make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME collect-report
- name: Upload cozyreport.tgz
if: always()
uses: actions/upload-artifact@v4
with:
name: cozyreport
path: /tmp/${{ env.SANDBOX_NAME }}/_out/cozyreport.tgz
- name: Collect images list
if: always()
run: |
cd /tmp/$SANDBOX_NAME
make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME collect-images || true
make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME collect-images
- name: Upload image list
if: always()
uses: actions/upload-artifact@v4
with:
name: image-list
path: /tmp/${{ env.SANDBOX_NAME }}/_out/images.txt
# ▸ Tear down environment (always runs)
cleanup:
name: Tear down environment
runs-on: [self-hosted]
needs: [collect_debug_information]
if: ${{ always() && needs.test_apps.result == 'success' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Set sandbox ID
run: echo "SANDBOX_NAME=cozy-e2e-sandbox-$(echo "${GITHUB_REPOSITORY}:${GITHUB_WORKFLOW}:${GITHUB_REF}" | sha256sum | cut -c1-10)" >> $GITHUB_ENV
- name: Tear down sandbox
if: always()
run: make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME delete || true
run: make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME delete
- name: Remove workspace
if: always()
run: rm -rf /tmp/$SANDBOX_NAME

View File

@@ -1,78 +0,0 @@
name: Retest
on:
issue_comment:
types: [created]
jobs:
retest:
name: Retest PR
runs-on: ubuntu-latest
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '/retest')
permissions:
actions: write
pull-requests: read
steps:
- name: Rerun from Prepare environment
uses: actions/github-script@v7
with:
script: |
const prNumber = context.issue.number;
// Get the PR to find the head SHA
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
// Find the latest workflow run for this PR
const runs = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'pull-requests.yaml',
head_sha: pr.data.head.sha
});
if (runs.data.workflow_runs.length === 0) {
core.setFailed('No workflow runs found for this PR');
return;
}
const latestRun = runs.data.workflow_runs[0];
console.log(`Found workflow run: ${latestRun.id} (${latestRun.status})`);
// Check if workflow is waiting for approval (fork PRs)
if (latestRun.conclusion === 'action_required') {
core.setFailed('Workflow is waiting for approval. A maintainer must approve the workflow first.');
return;
}
// Get jobs for this run
const jobs = await github.rest.actions.listJobsForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: latestRun.id
});
// Find "Prepare environment" job
const prepareJob = jobs.data.jobs.find(j => j.name === 'Prepare environment');
if (!prepareJob) {
core.setFailed('Could not find "Prepare environment" job');
return;
}
console.log(`Found job: ${prepareJob.name} (id: ${prepareJob.id}, status: ${prepareJob.status})`);
// Rerun the job
await github.rest.actions.reRunJobForWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
job_id: prepareJob.id
});
console.log(`✅ Triggered rerun of job "${prepareJob.name}" (${prepareJob.id})`);

View File

@@ -123,19 +123,31 @@ jobs:
git commit -m "Prepare release ${GITHUB_REF#refs/tags/}" -s || echo "No changes to commit"
git push origin HEAD || true
# Tag the api/apps/v1alpha1 submodule for pkg.go.dev
- name: Tag API submodule
# Get `latest_version` from latest published release
- name: Get latest published release
if: steps.check_release.outputs.skip == 'false'
env:
GH_PAT: ${{ secrets.GH_PAT }}
run: |
VTAG="${{ steps.tag.outputs.tag }}"
SUBTAG="api/apps/v1alpha1/${VTAG}"
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
git tag "${SUBTAG}" "${VTAG}^{}" 2>/dev/null || git tag "${SUBTAG}" HEAD
git push origin "${SUBTAG}" || true
id: latest_release
uses: actions/github-script@v7
with:
script: |
try {
const rel = await github.rest.repos.getLatestRelease({
owner: context.repo.owner,
repo: context.repo.repo
});
core.setOutput('tag', rel.data.tag_name);
} catch (_) {
core.setOutput('tag', '');
}
# Compare tag (A) with latest (B)
- name: Semver compare
if: steps.check_release.outputs.skip == 'false'
id: semver
uses: madhead/semver-utils@v4.3.0
with:
version: ${{ steps.tag.outputs.tag }} # A
compare-to: ${{ steps.latest_release.outputs.tag }} # B
# Create or reuse draft release
- name: Create / reuse draft release
@@ -227,231 +239,3 @@ jobs:
} else {
console.log(`PR already exists from ${head} to ${base}`);
}
generate-changelog:
name: Generate Changelog
runs-on: [self-hosted]
needs: [prepare-release]
permissions:
contents: write
pull-requests: write
if: needs.prepare-release.result == 'success'
steps:
- name: Parse tag
id: tag
uses: actions/github-script@v7
with:
script: |
const ref = context.ref.replace('refs/tags/', '');
const m = ref.match(/^v(\d+\.\d+\.\d+)(-(?:alpha|beta|rc)\.\d+)?$/);
if (!m) {
core.setFailed(`❌ tag '${ref}' must match 'vX.Y.Z' or 'vX.Y.Z-(alpha|beta|rc).N'`);
return;
}
const version = m[1] + (m[2] ?? '');
core.setOutput('version', version);
core.setOutput('tag', ref);
- name: Checkout main branch
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
fetch-tags: true
token: ${{ secrets.GH_PAT }}
- name: Check if changelog already exists
id: check_changelog
run: |
CHANGELOG_FILE="docs/changelogs/v${{ steps.tag.outputs.version }}.md"
if [ -f "$CHANGELOG_FILE" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Changelog file $CHANGELOG_FILE already exists"
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Changelog file $CHANGELOG_FILE does not exist"
fi
- name: Setup Node.js
if: steps.check_changelog.outputs.exists == 'false'
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install GitHub Copilot CLI
if: steps.check_changelog.outputs.exists == 'false'
run: npm i -g @github/copilot
- name: Generate changelog using AI
if: steps.check_changelog.outputs.exists == 'false'
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GH_PAT }}
run: |
copilot --prompt "prepare changelog file for tagged release v${{ steps.tag.outputs.version }}, use @docs/agents/changelog.md for it. Create the changelog file at docs/changelogs/v${{ steps.tag.outputs.version }}.md" \
--allow-all-tools --allow-all-paths < /dev/null
- name: Create changelog branch and commit
if: steps.check_changelog.outputs.exists == 'false'
env:
GH_PAT: ${{ secrets.GH_PAT }}
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
CHANGELOG_FILE="docs/changelogs/v${{ steps.tag.outputs.version }}.md"
CHANGELOG_BRANCH="changelog-v${{ steps.tag.outputs.version }}"
if [ -f "$CHANGELOG_FILE" ]; then
# Fetch latest main branch
git fetch origin main
# Delete local branch if it exists
git branch -D "$CHANGELOG_BRANCH" 2>/dev/null || true
# Create and checkout new branch from main
git checkout -b "$CHANGELOG_BRANCH" origin/main
# Add and commit changelog
git add "$CHANGELOG_FILE"
if git diff --staged --quiet; then
echo "⚠️ No changes to commit (file may already be committed)"
else
git commit -m "docs: add changelog for v${{ steps.tag.outputs.version }}" -s
echo "✅ Changelog committed to branch $CHANGELOG_BRANCH"
fi
# Push the branch (force push to update if it exists)
git push -f origin "$CHANGELOG_BRANCH"
else
echo "⚠️ Changelog file was not generated"
exit 1
fi
- name: Create PR for changelog
if: steps.check_changelog.outputs.exists == 'false'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_PAT }}
script: |
const version = '${{ steps.tag.outputs.version }}';
const changelogBranch = `changelog-v${version}`;
const baseBranch = 'main';
// Check if PR already exists
const prs = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.repo.owner}:${changelogBranch}`,
base: baseBranch,
state: 'open'
});
if (prs.data.length > 0) {
const pr = prs.data[0];
console.log(`PR #${pr.number} already exists for changelog branch ${changelogBranch}`);
// Update PR body with latest info
const body = `This PR adds the changelog for release \`v${version}\`.\n\n✅ Changelog has been automatically generated in \`docs/changelogs/v${version}.md\`.`;
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
body: body
});
console.log(`Updated existing PR #${pr.number}`);
} else {
// Create new PR
const pr = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
head: changelogBranch,
base: baseBranch,
title: `docs: add changelog for v${version}`,
body: `This PR adds the changelog for release \`v${version}\`.\n\n✅ Changelog has been automatically generated in \`docs/changelogs/v${version}.md\`.`,
draft: false
});
// Add label if needed
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.data.number,
labels: ['documentation', 'automated']
});
console.log(`Created PR #${pr.data.number} for changelog`);
}
update-website-docs:
name: Update Website Docs
runs-on: [self-hosted]
needs: [generate-changelog]
if: needs.generate-changelog.result == 'success'
permissions:
contents: read
steps:
- name: Parse tag
id: tag
uses: actions/github-script@v7
with:
script: |
const ref = context.ref.replace('refs/tags/', '');
const m = ref.match(/^v(\d+\.\d+\.\d+)(-(?:alpha|beta|rc)\.\d+)?$/);
if (!m) {
core.setFailed(`❌ tag '${ref}' must match 'vX.Y.Z' or 'vX.Y.Z-(alpha|beta|rc).N'`);
return;
}
const version = m[1] + (m[2] ?? '');
core.setOutput('tag', ref); // v0.22.0
core.setOutput('version', version); // 0.22.0
- name: Checkout website repo
uses: actions/checkout@v4
with:
repository: cozystack/website
token: ${{ secrets.GH_PAT }}
ref: main
- name: Update docs from release branch
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
run: make update-all BRANCH=release-${{ steps.tag.outputs.version }} RELEASE_TAG=${{ steps.tag.outputs.tag }}
- name: Commit and push
id: commit
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git add content
if git diff --cached --quiet; then
echo "No changes to commit"
echo "changed=false" >> $GITHUB_OUTPUT
exit 0
fi
BRANCH="update-docs-v${{ steps.tag.outputs.version }}"
git branch -D "$BRANCH" 2>/dev/null || true
git checkout -b "$BRANCH"
git commit --signoff -m "[docs] Update managed apps reference for v${{ steps.tag.outputs.version }}"
git push --force --set-upstream origin "$BRANCH"
echo "changed=true" >> $GITHUB_OUTPUT
- name: Open pull request
if: steps.commit.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
run: |
BRANCH="update-docs-v${{ steps.tag.outputs.version }}"
pr_state=$(gh pr view "$BRANCH" --repo cozystack/website --json state --jq .state 2>/dev/null || echo "")
if [[ "$pr_state" == "OPEN" ]]; then
echo "PR already open, skipping creation."
else
gh pr create \
--repo cozystack/website \
--title "[docs] Update managed apps reference for v${{ steps.tag.outputs.version }}" \
--body "Automated docs update for release \`v${{ steps.tag.outputs.version }}\`." \
--head "update-docs-v${{ steps.tag.outputs.version }}" \
--base main
fi

View File

@@ -1,92 +0,0 @@
name: Update Release Notes
on:
push:
branches:
- main
concurrency:
group: update-releasenotes-${{ github.workflow }}
cancel-in-progress: false
jobs:
update-releasenotes:
name: Update Release Notes
runs-on: [self-hosted]
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Update release notes from changelogs
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const changelogDir = 'docs/changelogs';
// Get releases from first page
const releases = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 30
});
console.log(`Found ${releases.data.length} releases (first page only)`);
// Process each release
for (const release of releases.data) {
const tag = release.tag_name;
const changelogFile = `${tag}.md`;
const changelogPath = path.join(changelogDir, changelogFile);
console.log(`\nProcessing release: ${tag}`);
// Check if changelog file exists
if (!fs.existsSync(changelogPath)) {
console.log(` ⚠️ Changelog file ${changelogFile} does not exist, skipping...`);
continue;
}
// Read changelog file content
let changelogContent;
try {
changelogContent = fs.readFileSync(changelogPath, 'utf8');
} catch (error) {
console.log(` ❌ Error reading file ${changelogPath}: ${error.message}`);
continue;
}
if (!changelogContent.trim()) {
console.log(` ⚠️ Changelog file ${changelogFile} is empty, skipping...`);
continue;
}
// Check if content is already up to date
const currentBody = release.body || '';
if (currentBody.trim() === changelogContent.trim()) {
console.log(` ✓ Content is already up to date, skipping...`);
continue;
}
// Update release notes
try {
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.id,
body: changelogContent
});
console.log(` ✅ Successfully updated release notes for ${tag}`);
} catch (error) {
console.log(` ❌ Error updating release ${tag}: ${error.message}`);
core.setFailed(`Failed to update release notes for ${tag}`);
}
}

View File

@@ -32,4 +32,4 @@ This list is sorted in chronological order, based on the submission date.
| [Urmanac](https://urmanac.com) | @kingdonb | 2024-12-04 | Urmanac is the future home of a hosting platform for the knowledge base of a community of personal server enthusiasts. We use Cozystack to provide support services for web sites hosted using both conventional deployments and on SpinKube, with WASM. |
| [Hidora](https://hikube.cloud) | @matthieu-robin | 2025-09-17 | Hidora is a Swiss cloud provider delivering managed services and infrastructure solutions through datacenters located in Switzerland, ensuring data sovereignty and reliability. Its sovereign cloud platform, Hikube, is designed to run workloads with high availability across multiple datacenters, providing enterprises with a secure and scalable foundation for their applications based on Cozystack. |
| [QOSI](https://qosi.kz) | @tabu-a | 2025-10-04 | QOSI is a non-profit organization driving open-source adoption and digital sovereignty across Kazakhstan and Central Asia. We use Cozystack as a platform for deploying sovereign, GPU-enabled clouds and educational environments under the National AI Program. Our goal is to accelerate the regions transition toward open, self-hosted cloud-native technologies |
| [Cloupard](https://cloupard.kz/) | @serjiott | 2025-12-18 | Cloupard is a public cloud provider offering IaaS and PaaS services via datacenters in Kazakhstan and Uzbekistan. Uses CozyStack on bare metal to extend its managed PaaS offerings. |
|

View File

@@ -3,38 +3,14 @@
This file provides structured guidance for AI coding assistants and agents
working with the **Cozystack** project.
## Activation
## Agent Documentation
**CRITICAL**: When the user asks you to do something that matches the scope of a documented process, you MUST read the corresponding documentation file and follow the instructions exactly as written.
- **Commits, PRs, git operations** (e.g., "create a commit", "make a PR", "fix review comments", "rebase", "cherry-pick")
- Read: [`contributing.md`](./docs/agents/contributing.md)
- Action: Read the entire file and follow ALL instructions step-by-step
- **Changelog generation** (e.g., "generate changelog", "create changelog", "prepare changelog for version X")
- Read: [`changelog.md`](./docs/agents/changelog.md)
- Action: Read the entire file and follow ALL steps in the checklist. Do NOT skip any mandatory steps
- **Release creation** (e.g., "create release", "prepare release", "tag release", "make a release")
- Read: [`releasing.md`](./docs/agents/releasing.md)
- Action: Read the file and follow the referenced release process in `docs/release.md`
- **Project structure, conventions, code layout** (e.g., "where should I put X", "what's the convention for Y", "how is the project organized")
- Read: [`overview.md`](./docs/agents/overview.md)
- Action: Read relevant sections to understand project structure and conventions
- **General questions about contributing**
- Read: [`contributing.md`](./docs/agents/contributing.md)
- Action: Read the file to understand git workflow, commit format, PR process
**Important rules:**
-**ONLY read the file if the task matches the documented process scope** - do not read files for tasks that don't match their purpose
-**ALWAYS read the file FIRST** before starting the task (when applicable)
-**Follow instructions EXACTLY** as written in the documentation
-**Do NOT skip mandatory steps** (especially in changelog.md)
-**Do NOT assume** you know the process - always check the documentation when the task matches
-**Do NOT read files** for tasks that are outside their documented scope
- 📖 **Note**: [`overview.md`](./docs/agents/overview.md) can be useful as a reference to understand project structure and conventions, even when not explicitly required by the task
| Agent | Purpose |
|-------|---------|
| [overview.md](./docs/agents/overview.md) | Project structure and conventions |
| [contributing.md](./docs/agents/contributing.md) | Commits, pull requests, and git workflow |
| [changelog.md](./docs/agents/changelog.md) | Changelog generation instructions |
| [releasing.md](./docs/agents/releasing.md) | Release process and workflow |
## Project Overview

View File

@@ -1,6 +1,4 @@
.PHONY: manifests assets unit-tests helm-unit-tests
include hack/common-envs.mk
.PHONY: manifests repos assets unit-tests helm-unit-tests
build-deps:
@command -V find docker skopeo jq gh helm > /dev/null
@@ -11,17 +9,15 @@ build-deps:
build: build-deps
make -C packages/apps/http-cache image
make -C packages/apps/mariadb image
make -C packages/apps/mysql image
make -C packages/apps/clickhouse image
make -C packages/apps/kubernetes image
make -C packages/system/monitoring image
make -C packages/extra/monitoring image
make -C packages/system/cozystack-api image
make -C packages/system/cozystack-controller image
make -C packages/system/backup-controller image
make -C packages/system/backupstrategy-controller image
make -C packages/system/lineage-controller-webhook image
make -C packages/system/cilium image
make -C packages/system/linstor image
make -C packages/system/kubeovn-webhook image
make -C packages/system/kubeovn-plunger image
make -C packages/system/dashboard image
@@ -29,53 +25,22 @@ build: build-deps
make -C packages/system/kamaji image
make -C packages/system/bucket image
make -C packages/system/objectstorage-controller image
make -C packages/system/grafana-operator image
make -C packages/core/testing image
make -C packages/core/talos image
make -C packages/core/platform image
make -C packages/core/installer image
make manifests
repos:
rm -rf _out
make -C packages/system repo
make -C packages/apps repo
make -C packages/extra repo
manifests:
mkdir -p _out/assets
cat internal/crdinstall/manifests/*.yaml > _out/assets/cozystack-crds.yaml
# Talos variant (default)
helm template installer packages/core/installer -n cozy-system \
--show-only templates/cozystack-operator.yaml \
> _out/assets/cozystack-operator-talos.yaml
# Generic Kubernetes variant (k3s, kubeadm, RKE2)
helm template installer packages/core/installer -n cozy-system \
--set cozystackOperator.variant=generic \
--set cozystack.apiServerHost=REPLACE_ME \
--show-only templates/cozystack-operator.yaml \
> _out/assets/cozystack-operator-generic.yaml
# Hosted variant (managed Kubernetes)
helm template installer packages/core/installer -n cozy-system \
--set cozystackOperator.variant=hosted \
--show-only templates/cozystack-operator.yaml \
> _out/assets/cozystack-operator-hosted.yaml
(cd packages/core/installer/; helm template -n cozy-installer installer .) > _out/assets/cozystack-installer.yaml
cozypkg:
go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozypkg/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozypkg ./cmd/cozypkg
assets: assets-talos assets-cozypkg openapi-json
openapi-json:
mkdir -p _out/assets
VERSION=$(shell git describe --tags --always 2>/dev/null || echo dev) go run ./tools/openapi-gen/ > _out/assets/openapi.json
assets-talos:
make -C packages/core/talos assets
assets-cozypkg: assets-cozypkg-linux-amd64 assets-cozypkg-linux-arm64 assets-cozypkg-darwin-amd64 assets-cozypkg-darwin-arm64 assets-cozypkg-windows-amd64 assets-cozypkg-windows-arm64
(cd _out/assets/ && sha256sum cozypkg-*.tar.gz) > _out/assets/cozypkg-checksums.txt
assets-cozypkg-%:
$(eval EXT := $(if $(filter windows,$(firstword $(subst -, ,$*))),.exe,))
mkdir -p _out/assets
GOOS=$(firstword $(subst -, ,$*)) GOARCH=$(lastword $(subst -, ,$*)) go build -ldflags "-X github.com/cozystack/cozystack/cmd/cozypkg/cmd.Version=v$(COZYSTACK_VERSION)" -o _out/bin/cozypkg-$*/cozypkg$(EXT) ./cmd/cozypkg
cp LICENSE _out/bin/cozypkg-$*/LICENSE
tar -C _out/bin/cozypkg-$* -czf _out/assets/cozypkg-$*.tar.gz LICENSE cozypkg$(EXT)
assets:
make -C packages/core/installer assets
test:
make -C packages/core/testing apply

View File

@@ -1,35 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package bucket
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Provisions bucket from the `-lock` BucketClass (with object lock enabled).
// +kubebuilder:default:=false
Locking bool `json:"locking"`
// Selects a specific BucketClass by storage pool name.
// +kubebuilder:default:=""
StoragePool string `json:"storagePool,omitempty"`
// Users configuration map.
// +kubebuilder:default:={}
Users map[string]User `json:"users,omitempty"`
}
type User struct {
// Whether the user has read-only access.
Readonly bool `json:"readonly,omitempty"`
}

View File

@@ -1,114 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package clickhouse
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Backup configuration.
// +kubebuilder:default:={}
Backup Backup `json:"backup"`
// ClickHouse Keeper configuration.
// +kubebuilder:default:={}
ClickhouseKeeper ClickHouseKeeper `json:"clickhouseKeeper"`
// Size of Persistent Volume for logs.
// +kubebuilder:default:="2Gi"
LogStorageSize resource.Quantity `json:"logStorageSize"`
// TTL (expiration time) for `query_log` and `query_thread_log`.
// +kubebuilder:default:=15
LogTTL int `json:"logTTL"`
// Number of ClickHouse replicas.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration for each ClickHouse replica. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="small"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Number of ClickHouse shards.
// +kubebuilder:default:=1
Shards int `json:"shards"`
// Persistent Volume Claim size available for application data.
// +kubebuilder:default:="10Gi"
Size resource.Quantity `json:"size"`
// StorageClass used to store the data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
// Users configuration map.
// +kubebuilder:default:={}
Users map[string]User `json:"users,omitempty"`
}
type ClickHouseKeeper struct {
// Deploy ClickHouse Keeper for cluster coordination.
// +kubebuilder:default:=true
Enabled bool `json:"enabled,omitempty"`
// Number of Keeper replicas.
// +kubebuilder:default:=3
Replicas int `json:"replicas,omitempty"`
// Default sizing preset.
// +kubebuilder:default:="micro"
ResourcesPreset ResourcesPreset `json:"resourcesPreset,omitempty"`
// Persistent Volume Claim size available for application data.
// +kubebuilder:default:="1Gi"
Size resource.Quantity `json:"size,omitempty"`
}
type User struct {
// Password for the user.
Password string `json:"password,omitempty"`
// User is readonly (default: false).
Readonly bool `json:"readonly,omitempty"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
type Backup struct {
// Retention strategy for cleaning up old backups.
// +kubebuilder:default:="--keep-last=3 --keep-daily=3 --keep-within-weekly=1m"
CleanupStrategy string `json:"cleanupStrategy"`
// Enable regular backups (default: false).
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Password for Restic backup encryption.
// +kubebuilder:default:="<password>"
ResticPassword string `json:"resticPassword"`
// Access key for S3 authentication.
// +kubebuilder:default:="<your-access-key>"
S3AccessKey string `json:"s3AccessKey"`
// S3 bucket used for storing backups.
// +kubebuilder:default:="s3.example.org/clickhouse-backups"
S3Bucket string `json:"s3Bucket"`
// AWS S3 region where backups are stored.
// +kubebuilder:default:="us-east-1"
S3Region string `json:"s3Region"`
// Secret key for S3 authentication.
// +kubebuilder:default:="<your-secret-key>"
S3SecretKey string `json:"s3SecretKey"`
// Cron schedule for automated backups.
// +kubebuilder:default:="0 2 * * *"
Schedule string `json:"schedule"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string

View File

@@ -1,164 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package foundationdb
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Enable automatic pod replacements.
// +kubebuilder:default:=true
AutomaticReplacements bool `json:"automaticReplacements"`
// Backup configuration.
// +kubebuilder:default:={}
Backup Backup `json:"backup"`
// Cluster configuration.
// +kubebuilder:default:={}
Cluster Cluster `json:"cluster"`
// Custom parameters to pass to FoundationDB.
// +kubebuilder:default:={}
CustomParameters []string `json:"customParameters,omitempty"`
// Container image deployment type.
// +kubebuilder:default:="unified"
ImageType ImageType `json:"imageType"`
// Monitoring configuration.
// +kubebuilder:default:={}
Monitoring Monitoring `json:"monitoring"`
// Explicit CPU and memory configuration for each FoundationDB instance. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="medium"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Security context for containers.
// +kubebuilder:default:={}
SecurityContext SecurityContext `json:"securityContext"`
// Storage configuration.
// +kubebuilder:default:={}
Storage Storage `json:"storage"`
}
type Cluster struct {
// Fault domain configuration.
// +kubebuilder:default:={}
FaultDomain ClusterFaultDomain `json:"faultDomain"`
// Process counts for different roles.
// +kubebuilder:default:={}
ProcessCounts ClusterProcessCounts `json:"processCounts"`
// Database redundancy mode (single, double, triple, three_datacenter, three_datacenter_fallback).
// +kubebuilder:default:="double"
RedundancyMode string `json:"redundancyMode"`
// Storage engine (ssd-2, ssd-redwood-v1, ssd-rocksdb-v1, memory).
// +kubebuilder:default:="ssd-2"
StorageEngine string `json:"storageEngine"`
// Version of FoundationDB to use.
// +kubebuilder:default:="7.3.63"
Version string `json:"version"`
}
type BackupS3Credentials struct {
// S3 access key ID.
// +kubebuilder:default:=""
AccessKeyId string `json:"accessKeyId"`
// S3 secret access key.
// +kubebuilder:default:=""
SecretAccessKey string `json:"secretAccessKey"`
}
type BackupS3 struct {
// S3 bucket name.
// +kubebuilder:default:=""
Bucket string `json:"bucket"`
// S3 credentials.
// +kubebuilder:default:={}
Credentials BackupS3Credentials `json:"credentials"`
// S3 endpoint URL.
// +kubebuilder:default:=""
Endpoint string `json:"endpoint"`
// S3 region.
// +kubebuilder:default:="us-east-1"
Region string `json:"region"`
}
type SecurityContext struct {
// Group ID to run the container.
// +kubebuilder:default:=4059
RunAsGroup int `json:"runAsGroup"`
// User ID to run the container.
// +kubebuilder:default:=4059
RunAsUser int `json:"runAsUser"`
}
type ClusterFaultDomain struct {
// Fault domain key.
// +kubebuilder:default:="kubernetes.io/hostname"
Key string `json:"key"`
// Fault domain value source.
// +kubebuilder:default:="spec.nodeName"
ValueFrom string `json:"valueFrom"`
}
type Storage struct {
// Size of persistent volumes for each instance.
// +kubebuilder:default:="16Gi"
Size resource.Quantity `json:"size"`
// Storage class (if not set, uses cluster default).
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
}
type Resources struct {
// CPU available to each instance.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each instance.
Memory resource.Quantity `json:"memory,omitempty"`
}
type Backup struct {
// Enable backups.
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Retention policy for backups.
// +kubebuilder:default:="7d"
RetentionPolicy string `json:"retentionPolicy"`
// S3 configuration for backups.
// +kubebuilder:default:={}
S3 BackupS3 `json:"s3"`
}
type ClusterProcessCounts struct {
// Number of cluster controller processes.
// +kubebuilder:default:=1
ClusterController int `json:"cluster_controller"`
// Number of stateless processes (-1 for automatic).
// +kubebuilder:default:=-1
Stateless int `json:"stateless"`
// Number of storage processes (determines cluster size).
// +kubebuilder:default:=3
Storage int `json:"storage"`
}
type Monitoring struct {
// Enable WorkloadMonitor integration.
// +kubebuilder:default:=true
Enabled bool `json:"enabled"`
}
// +kubebuilder:validation:Enum="unified";"split"
type ImageType string
// +kubebuilder:validation:Enum="small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string

View File

@@ -1,24 +0,0 @@
module github.com/cozystack/cozystack/api/apps/v1alpha1
go 1.25.0
require k8s.io/apimachinery v0.35.2
require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/text v0.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
)

View File

@@ -1,56 +0,0 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=
k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View File

@@ -1,116 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package harbor
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Core API server configuration.
// +kubebuilder:default:={}
Core Core `json:"core"`
// PostgreSQL database configuration.
// +kubebuilder:default:={}
Database Database `json:"database"`
// Hostname for external access to Harbor (defaults to 'harbor' subdomain for the tenant host).
// +kubebuilder:default:=""
Host string `json:"host,omitempty"`
// Background job service configuration.
// +kubebuilder:default:={}
Jobservice Jobservice `json:"jobservice"`
// Redis cache configuration.
// +kubebuilder:default:={}
Redis Redis `json:"redis"`
// Container image registry configuration.
// +kubebuilder:default:={}
Registry Registry `json:"registry"`
// StorageClass used to store the data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
// Trivy vulnerability scanner configuration.
// +kubebuilder:default:={}
Trivy Trivy `json:"trivy"`
}
type Trivy struct {
// Enable or disable the vulnerability scanner.
// +kubebuilder:default:=true
Enabled bool `json:"enabled"`
// Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="nano"
ResourcesPreset ResourcesPreset `json:"resourcesPreset,omitempty"`
// Persistent Volume size for vulnerability database cache.
// +kubebuilder:default:="5Gi"
Size resource.Quantity `json:"size"`
}
type Jobservice struct {
// Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="nano"
ResourcesPreset ResourcesPreset `json:"resourcesPreset,omitempty"`
}
type Resources struct {
// Number of CPU cores allocated.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Amount of memory allocated.
Memory resource.Quantity `json:"memory,omitempty"`
}
type Database struct {
// Number of database instances.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Persistent Volume size for database storage.
// +kubebuilder:default:="5Gi"
Size resource.Quantity `json:"size"`
}
type Redis struct {
// Number of Redis replicas.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Persistent Volume size for cache storage.
// +kubebuilder:default:="1Gi"
Size resource.Quantity `json:"size"`
}
type Core struct {
// Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="small"
ResourcesPreset ResourcesPreset `json:"resourcesPreset,omitempty"`
}
type Registry struct {
// Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="small"
ResourcesPreset ResourcesPreset `json:"resourcesPreset,omitempty"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string

View File

@@ -1,74 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package httpcache
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Endpoints configuration, as a list of <ip:port>.
// +kubebuilder:default:={}
Endpoints []string `json:"endpoints,omitempty"`
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// HAProxy configuration.
// +kubebuilder:default:={}
Haproxy HAProxy `json:"haproxy"`
// Nginx configuration.
// +kubebuilder:default:={}
Nginx Nginx `json:"nginx"`
// Persistent Volume Claim size available for application data.
// +kubebuilder:default:="10Gi"
Size resource.Quantity `json:"size"`
// StorageClass used to store the data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
type HAProxy struct {
// Number of HAProxy replicas.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="nano"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
}
type Nginx struct {
// Number of Nginx replicas.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="nano"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string

View File

@@ -1,92 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package kafka
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
k8sRuntime "k8s.io/apimachinery/pkg/runtime"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// Kafka configuration.
// +kubebuilder:default:={}
Kafka Kafka `json:"kafka"`
// Topics configuration.
// +kubebuilder:default:={}
Topics []Topic `json:"topics,omitempty"`
// ZooKeeper configuration.
// +kubebuilder:default:={}
Zookeeper ZooKeeper `json:"zookeeper"`
}
type ZooKeeper struct {
// Number of ZooKeeper replicas.
// +kubebuilder:default:=3
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="small"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Persistent Volume size for ZooKeeper.
// +kubebuilder:default:="5Gi"
Size resource.Quantity `json:"size"`
// StorageClass used to store the ZooKeeper data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
}
type Topic struct {
// Topic configuration.
Config k8sRuntime.RawExtension `json:"config"`
// Topic name.
Name string `json:"name"`
// Number of partitions.
Partitions int `json:"partitions"`
// Number of replicas.
Replicas int `json:"replicas"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
type Kafka struct {
// Number of Kafka replicas.
// +kubebuilder:default:=3
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="small"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Persistent Volume size for Kafka.
// +kubebuilder:default:="10Gi"
Size resource.Quantity `json:"size"`
// StorageClass used to store the Kafka data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string

View File

@@ -1,260 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package kubernetes
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
k8sRuntime "k8s.io/apimachinery/pkg/runtime"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Cluster addons configuration.
// +kubebuilder:default:={}
Addons Addons `json:"addons"`
// Kubernetes control-plane configuration.
// +kubebuilder:default:={}
ControlPlane ControlPlane `json:"controlPlane"`
// External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty.
// +kubebuilder:default:=""
Host string `json:"host"`
// Worker nodes configuration map.
// +kubebuilder:default:={"md0":{"ephemeralStorage":"20Gi","gpus":{},"instanceType":"u1.medium","maxReplicas":10,"minReplicas":0,"resources":{},"roles":{"ingress-nginx"}}}
NodeGroups map[string]NodeGroup `json:"nodeGroups,omitempty"`
// StorageClass used to store the data.
// +kubebuilder:default:="replicated"
StorageClass string `json:"storageClass"`
// Kubernetes major.minor version to deploy
// +kubebuilder:default:="v1.35"
Version Version `json:"version"`
}
type VeleroAddon struct {
// Enable Velero.
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Custom Helm values overrides.
// +kubebuilder:default:={}
ValuesOverride k8sRuntime.RawExtension `json:"valuesOverride"`
}
type KonnectivityServer struct {
// CPU and memory resources for Konnectivity.
// +kubebuilder:default:={}
Resources Resources `json:"resources"`
// Preset if `resources` omitted.
// +kubebuilder:default:="micro"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
}
type IngressNginxAddon struct {
// Enable the controller (requires nodes labeled `ingress-nginx`).
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Method to expose the controller. Allowed values: `Proxied`, `LoadBalancer`.
// +kubebuilder:default:="Proxied"
ExposeMethod IngressNginxExposeMethod `json:"exposeMethod"`
// Domains routed to this tenant cluster when `exposeMethod` is `Proxied`.
// +kubebuilder:default:={}
Hosts []string `json:"hosts,omitempty"`
// Custom Helm values overrides.
// +kubebuilder:default:={}
ValuesOverride k8sRuntime.RawExtension `json:"valuesOverride"`
}
type FluxCDAddon struct {
// Enable FluxCD.
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Custom Helm values overrides.
// +kubebuilder:default:={}
ValuesOverride k8sRuntime.RawExtension `json:"valuesOverride"`
}
type APIServer struct {
// CPU and memory resources for API Server.
// +kubebuilder:default:={}
Resources Resources `json:"resources"`
// Preset if `resources` omitted.
// +kubebuilder:default:="large"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
}
type Resources struct {
// CPU available.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available.
Memory resource.Quantity `json:"memory,omitempty"`
}
type CoreDNSAddon struct {
// Custom Helm values overrides.
// +kubebuilder:default:={}
ValuesOverride k8sRuntime.RawExtension `json:"valuesOverride"`
}
type Konnectivity struct {
// Konnectivity Server configuration.
// +kubebuilder:default:={}
Server KonnectivityServer `json:"server"`
}
type ControlPlane struct {
// API Server configuration.
// +kubebuilder:default:={}
ApiServer APIServer `json:"apiServer"`
// Controller Manager configuration.
// +kubebuilder:default:={}
ControllerManager ControllerManager `json:"controllerManager"`
// Konnectivity configuration.
// +kubebuilder:default:={}
Konnectivity Konnectivity `json:"konnectivity"`
// Number of control-plane replicas.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Scheduler configuration.
// +kubebuilder:default:={}
Scheduler Scheduler `json:"scheduler"`
}
type GPUOperatorAddon struct {
// Enable GPU Operator.
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Custom Helm values overrides.
// +kubebuilder:default:={}
ValuesOverride k8sRuntime.RawExtension `json:"valuesOverride"`
}
type Addons struct {
// Cert-manager addon.
// +kubebuilder:default:={}
CertManager CertManagerAddon `json:"certManager"`
// Cilium CNI plugin.
// +kubebuilder:default:={}
Cilium CiliumAddon `json:"cilium"`
// CoreDNS addon.
// +kubebuilder:default:={}
Coredns CoreDNSAddon `json:"coredns"`
// FluxCD GitOps operator.
// +kubebuilder:default:={}
Fluxcd FluxCDAddon `json:"fluxcd"`
// Gateway API addon.
// +kubebuilder:default:={}
GatewayAPI GatewayAPIAddon `json:"gatewayAPI"`
// NVIDIA GPU Operator.
// +kubebuilder:default:={}
GpuOperator GPUOperatorAddon `json:"gpuOperator"`
// Ingress-NGINX controller.
// +kubebuilder:default:={}
IngressNginx IngressNginxAddon `json:"ingressNginx"`
// Monitoring agents.
// +kubebuilder:default:={}
MonitoringAgents MonitoringAgentsAddon `json:"monitoringAgents"`
// Velero backup/restore addon.
// +kubebuilder:default:={}
Velero VeleroAddon `json:"velero"`
// Vertical Pod Autoscaler.
// +kubebuilder:default:={}
VerticalPodAutoscaler VerticalPodAutoscalerAddon `json:"verticalPodAutoscaler"`
}
type Scheduler struct {
// CPU and memory resources for Scheduler.
// +kubebuilder:default:={}
Resources Resources `json:"resources"`
// Preset if `resources` omitted.
// +kubebuilder:default:="micro"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
}
type CertManagerAddon struct {
// Enable cert-manager.
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Custom Helm values overrides.
// +kubebuilder:default:={}
ValuesOverride k8sRuntime.RawExtension `json:"valuesOverride"`
}
type MonitoringAgentsAddon struct {
// Enable monitoring agents.
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Custom Helm values overrides.
// +kubebuilder:default:={}
ValuesOverride k8sRuntime.RawExtension `json:"valuesOverride"`
}
type GPU struct {
// Name of GPU, such as "nvidia.com/AD102GL_L40S".
Name string `json:"name"`
}
type GatewayAPIAddon struct {
// Enable Gateway API.
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
}
type VerticalPodAutoscalerAddon struct {
// Custom Helm values overrides.
// +kubebuilder:default:={}
ValuesOverride k8sRuntime.RawExtension `json:"valuesOverride"`
}
type ControllerManager struct {
// CPU and memory resources for Controller Manager.
// +kubebuilder:default:={}
Resources Resources `json:"resources"`
// Preset if `resources` omitted.
// +kubebuilder:default:="micro"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
}
type NodeGroup struct {
// Ephemeral storage size.
// +kubebuilder:default:="20Gi"
EphemeralStorage resource.Quantity `json:"ephemeralStorage"`
// List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM).
Gpus []GPU `json:"gpus,omitempty"`
// Virtual machine instance type.
// +kubebuilder:default:="u1.medium"
InstanceType string `json:"instanceType"`
// Maximum number of replicas.
// +kubebuilder:default:=10
MaxReplicas int `json:"maxReplicas"`
// Minimum number of replicas.
// +kubebuilder:default:=0
MinReplicas int `json:"minReplicas"`
// CPU and memory resources for each worker node.
Resources Resources `json:"resources"`
// List of node roles.
Roles []string `json:"roles,omitempty"`
}
type CiliumAddon struct {
// Custom Helm values overrides.
// +kubebuilder:default:={}
ValuesOverride k8sRuntime.RawExtension `json:"valuesOverride"`
}
// +kubebuilder:validation:Enum="v1.35";"v1.34";"v1.33";"v1.32";"v1.31";"v1.30"
type Version string
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string
// +kubebuilder:validation:Enum="Proxied";"LoadBalancer"
type IngressNginxExposeMethod string

View File

@@ -1,111 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package mariadb
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Backup configuration.
// +kubebuilder:default:={}
Backup Backup `json:"backup"`
// Databases configuration map.
// +kubebuilder:default:={}
Databases map[string]Database `json:"databases,omitempty"`
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// Number of MariaDB replicas.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration for each MariaDB replica. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="nano"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Persistent Volume Claim size available for application data.
// +kubebuilder:default:="10Gi"
Size resource.Quantity `json:"size"`
// StorageClass used to store the data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
// Users configuration map.
// +kubebuilder:default:={}
Users map[string]User `json:"users,omitempty"`
// MariaDB major.minor version to deploy
// +kubebuilder:default:="v11.8"
Version Version `json:"version"`
}
type Backup struct {
// Retention strategy for cleaning up old backups.
// +kubebuilder:default:="--keep-last=3 --keep-daily=3 --keep-within-weekly=1m"
CleanupStrategy string `json:"cleanupStrategy"`
// Enable regular backups (default: false).
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Password for Restic backup encryption.
// +kubebuilder:default:="<password>"
ResticPassword string `json:"resticPassword"`
// Access key for S3 authentication.
// +kubebuilder:default:="<your-access-key>"
S3AccessKey string `json:"s3AccessKey"`
// S3 bucket used for storing backups.
// +kubebuilder:default:="s3.example.org/mariadb-backups"
S3Bucket string `json:"s3Bucket"`
// AWS S3 region where backups are stored.
// +kubebuilder:default:="us-east-1"
S3Region string `json:"s3Region"`
// Secret key for S3 authentication.
// +kubebuilder:default:="<your-secret-key>"
S3SecretKey string `json:"s3SecretKey"`
// Cron schedule for automated backups.
// +kubebuilder:default:="0 2 * * *"
Schedule string `json:"schedule"`
}
type Database struct {
// Roles assigned to users.
Roles DatabaseRoles `json:"roles,omitempty"`
}
type DatabaseRoles struct {
// List of users with admin privileges.
Admin []string `json:"admin,omitempty"`
// List of users with read-only privileges.
Readonly []string `json:"readonly,omitempty"`
}
type User struct {
// Maximum number of connections.
MaxUserConnections int `json:"maxUserConnections"`
// Password for the user.
Password string `json:"password"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string
// +kubebuilder:validation:Enum="v11.8";"v11.4";"v10.11";"v10.6"
type Version string

View File

@@ -1,151 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package mongodb
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Backup configuration.
// +kubebuilder:default:={}
Backup Backup `json:"backup"`
// Bootstrap configuration.
// +kubebuilder:default:={}
Bootstrap Bootstrap `json:"bootstrap"`
// Databases configuration map.
// +kubebuilder:default:={}
Databases map[string]Database `json:"databases,omitempty"`
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// Number of MongoDB replicas in replica set.
// +kubebuilder:default:=3
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration for each MongoDB replica. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="small"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Enable sharded cluster mode. When disabled, deploys a replica set.
// +kubebuilder:default:=false
Sharding bool `json:"sharding"`
// Configuration for sharded cluster mode.
// +kubebuilder:default:={}
ShardingConfig ShardingConfig `json:"shardingConfig"`
// Persistent Volume Claim size available for application data.
// +kubebuilder:default:="10Gi"
Size resource.Quantity `json:"size"`
// StorageClass used to store the data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
// Users configuration map.
// +kubebuilder:default:={}
Users map[string]User `json:"users,omitempty"`
// MongoDB major version to deploy.
// +kubebuilder:default:="v8"
Version Version `json:"version"`
}
type Shard struct {
// Shard name.
Name string `json:"name"`
// Number of replicas in this shard.
Replicas int `json:"replicas"`
// PVC size for this shard.
Size resource.Quantity `json:"size"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
type DatabaseRoles struct {
// List of users with admin privileges (readWrite + dbAdmin).
Admin []string `json:"admin,omitempty"`
// List of users with read-only privileges.
Readonly []string `json:"readonly,omitempty"`
}
type Backup struct {
// Destination path for backups (e.g. s3://bucket/path/).
// +kubebuilder:default:="s3://bucket/path/to/folder/"
DestinationPath string `json:"destinationPath,omitempty"`
// Enable regular backups.
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// S3 endpoint URL for uploads.
// +kubebuilder:default:="http://minio-gateway-service:9000"
EndpointURL string `json:"endpointURL,omitempty"`
// Retention policy (e.g. "30d").
// +kubebuilder:default:="30d"
RetentionPolicy string `json:"retentionPolicy,omitempty"`
// Access key for S3 authentication.
// +kubebuilder:default:=""
S3AccessKey string `json:"s3AccessKey,omitempty"`
// Secret key for S3 authentication.
// +kubebuilder:default:=""
S3SecretKey string `json:"s3SecretKey,omitempty"`
// Cron schedule for automated backups.
// +kubebuilder:default:="0 2 * * *"
Schedule string `json:"schedule,omitempty"`
}
type ShardingConfig struct {
// PVC size for config servers.
// +kubebuilder:default:="3Gi"
ConfigServerSize resource.Quantity `json:"configServerSize"`
// Number of config server replicas.
// +kubebuilder:default:=3
ConfigServers int `json:"configServers"`
// Number of mongos router replicas.
// +kubebuilder:default:=2
Mongos int `json:"mongos"`
// List of shard configurations.
// +kubebuilder:default:={{"name":"rs0","replicas":3,"size":"10Gi"}}
Shards []Shard `json:"shards,omitempty"`
}
type Bootstrap struct {
// Name of backup to restore from.
// +kubebuilder:default:=""
BackupName string `json:"backupName"`
// Whether to restore from a backup.
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Timestamp for point-in-time recovery; empty means latest.
// +kubebuilder:default:=""
RecoveryTime string `json:"recoveryTime,omitempty"`
}
type User struct {
// Password for the user (auto-generated if omitted).
Password string `json:"password,omitempty"`
}
type Database struct {
// Roles assigned to users.
Roles DatabaseRoles `json:"roles,omitempty"`
}
// +kubebuilder:validation:Enum="v8";"v7";"v6"
type Version string
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string

View File

@@ -1,80 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package nats
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
k8sRuntime "k8s.io/apimachinery/pkg/runtime"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// NATS configuration.
// +kubebuilder:default:={}
Config ValuesConfig `json:"config"`
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// Jetstream configuration.
// +kubebuilder:default:={}
Jetstream Jetstream `json:"jetstream"`
// Number of replicas.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration for each NATS replica. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="nano"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// StorageClass used to store the data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
// Users configuration map.
// +kubebuilder:default:={}
Users map[string]User `json:"users,omitempty"`
}
type User struct {
// Password for the user.
Password string `json:"password,omitempty"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
type Jetstream struct {
// Enable or disable Jetstream for persistent messaging in NATS.
// +kubebuilder:default:=true
Enabled bool `json:"enabled"`
// Jetstream persistent storage size.
// +kubebuilder:default:="10Gi"
Size resource.Quantity `json:"size"`
}
type ValuesConfig struct {
// Additional configuration to merge into NATS config.
// +kubebuilder:default:={}
Merge *k8sRuntime.RawExtension `json:"merge,omitempty"`
// Additional resolver configuration to merge into NATS config.
// +kubebuilder:default:={}
Resolver *k8sRuntime.RawExtension `json:"resolver,omitempty"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string

View File

@@ -1,53 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package openbao
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// Number of OpenBAO replicas. HA with Raft is automatically enabled when replicas > 1. Switching between standalone (file storage) and HA (Raft storage) modes requires data migration.
// +kubebuilder:default:=1
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration for each OpenBAO replica. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="small"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Persistent Volume Claim size for data storage.
// +kubebuilder:default:="10Gi"
Size resource.Quantity `json:"size"`
// StorageClass used to store the data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
// Enable the OpenBAO web UI.
// +kubebuilder:default:=true
Ui bool `json:"ui"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string

View File

@@ -1,152 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package postgresql
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Backup configuration.
// +kubebuilder:default:={}
Backup Backup `json:"backup"`
// Bootstrap configuration.
// +kubebuilder:default:={}
Bootstrap Bootstrap `json:"bootstrap"`
// Databases configuration map.
// +kubebuilder:default:={}
Databases map[string]Database `json:"databases,omitempty"`
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// PostgreSQL server configuration.
// +kubebuilder:default:={}
Postgresql PostgreSQL `json:"postgresql"`
// Quorum configuration for synchronous replication.
// +kubebuilder:default:={}
Quorum Quorum `json:"quorum"`
// Number of Postgres replicas.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration for each PostgreSQL replica. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="micro"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Persistent Volume Claim size available for application data.
// +kubebuilder:default:="10Gi"
Size resource.Quantity `json:"size"`
// StorageClass used to store the data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
// Users configuration map.
// +kubebuilder:default:={}
Users map[string]User `json:"users,omitempty"`
// PostgreSQL major version to deploy
// +kubebuilder:default:="v18"
Version Version `json:"version"`
}
type User struct {
// Password for the user.
Password string `json:"password,omitempty"`
// Whether the user has replication privileges.
Replication bool `json:"replication,omitempty"`
}
type Bootstrap struct {
// Whether to restore from a backup.
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Previous cluster name before deletion.
// +kubebuilder:default:=""
OldName string `json:"oldName"`
// Timestamp (RFC3339) for point-in-time recovery; empty means latest.
// +kubebuilder:default:=""
RecoveryTime string `json:"recoveryTime,omitempty"`
}
type Quorum struct {
// Maximum number of synchronous replicas allowed (must be less than total replicas).
// +kubebuilder:default:=0
MaxSyncReplicas int `json:"maxSyncReplicas"`
// Minimum number of synchronous replicas required for commit.
// +kubebuilder:default:=0
MinSyncReplicas int `json:"minSyncReplicas"`
}
type DatabaseRoles struct {
// List of users with admin privileges.
Admin []string `json:"admin,omitempty"`
// List of users with read-only privileges.
Readonly []string `json:"readonly,omitempty"`
}
type Database struct {
// List of enabled PostgreSQL extensions.
Extensions []string `json:"extensions,omitempty"`
// Roles assigned to users.
Roles DatabaseRoles `json:"roles,omitempty"`
}
type Backup struct {
// Destination path for backups (e.g. s3://bucket/path/).
// +kubebuilder:default:="s3://bucket/path/to/folder/"
DestinationPath string `json:"destinationPath,omitempty"`
// Enable regular backups.
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// S3 endpoint URL for uploads.
// +kubebuilder:default:="http://minio-gateway-service:9000"
EndpointURL string `json:"endpointURL,omitempty"`
// Retention policy (e.g. "30d").
// +kubebuilder:default:="30d"
RetentionPolicy string `json:"retentionPolicy,omitempty"`
// Access key for S3 authentication.
// +kubebuilder:default:="<your-access-key>"
S3AccessKey string `json:"s3AccessKey,omitempty"`
// Secret key for S3 authentication.
// +kubebuilder:default:="<your-secret-key>"
S3SecretKey string `json:"s3SecretKey,omitempty"`
// Cron schedule for automated backups.
// +kubebuilder:default:="0 2 * * * *"
Schedule string `json:"schedule,omitempty"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
type PostgreSQLParameters struct {
// Maximum number of concurrent connections to the database server.
// +kubebuilder:default:=100
MaxConnections int `json:"max_connections,omitempty"`
}
type PostgreSQL struct {
// PostgreSQL server parameters.
// +kubebuilder:default:={}
Parameters PostgreSQLParameters `json:"parameters,omitempty"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string
// +kubebuilder:validation:Enum="v18";"v17";"v16";"v15";"v14";"v13"
type Version string

View File

@@ -1,50 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package qdrant
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// Number of Qdrant replicas. Cluster mode is automatically enabled when replicas > 1.
// +kubebuilder:default:=1
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration for each Qdrant replica. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="small"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Persistent Volume Claim size available for vector data storage.
// +kubebuilder:default:="10Gi"
Size resource.Quantity `json:"size"`
// StorageClass used to store the data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string

View File

@@ -1,79 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package rabbitmq
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// Number of RabbitMQ replicas.
// +kubebuilder:default:=3
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration for each RabbitMQ replica. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="nano"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Persistent Volume Claim size available for application data.
// +kubebuilder:default:="10Gi"
Size resource.Quantity `json:"size"`
// StorageClass used to store the data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
// Users configuration map.
// +kubebuilder:default:={}
Users map[string]User `json:"users,omitempty"`
// RabbitMQ major.minor version to deploy
// +kubebuilder:default:="v4.2"
Version Version `json:"version"`
// Virtual hosts configuration map.
// +kubebuilder:default:={}
Vhosts map[string]Vhost `json:"vhosts,omitempty"`
}
type Vhost struct {
// Virtual host roles list.
Roles Roles `json:"roles"`
}
type User struct {
// Password for the user.
Password string `json:"password,omitempty"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
type Roles struct {
// List of admin users.
Admin []string `json:"admin,omitempty"`
// List of readonly users.
Readonly []string `json:"readonly,omitempty"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string
// +kubebuilder:validation:Enum="v4.2";"v4.1";"v4.0";"v3.13"
type Version string

View File

@@ -1,59 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package redis
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Enable password generation.
// +kubebuilder:default:=true
AuthEnabled bool `json:"authEnabled"`
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// Number of Redis replicas.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration for each Redis replica. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="nano"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Persistent Volume Claim size available for application data.
// +kubebuilder:default:="1Gi"
Size resource.Quantity `json:"size"`
// StorageClass used to store the data.
// +kubebuilder:default:=""
StorageClass string `json:"storageClass"`
// Redis major version to deploy
// +kubebuilder:default:="v8"
Version Version `json:"version"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string
// +kubebuilder:validation:Enum="v8";"v7"
type Version string

View File

@@ -1,77 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package tcpbalancer
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// HTTP and HTTPS configuration.
// +kubebuilder:default:={}
HttpAndHttps HttpAndHttps `json:"httpAndHttps"`
// Number of HAProxy replicas.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration for each TCP Balancer replica. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="nano"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// List of allowed client networks.
// +kubebuilder:default:={}
Whitelist []string `json:"whitelist,omitempty"`
// Secure HTTP by whitelisting client networks (default: false).
// +kubebuilder:default:=false
WhitelistHTTP bool `json:"whitelistHTTP"`
}
type TargetPorts struct {
// HTTP port number.
// +kubebuilder:default:=80
Http int `json:"http"`
// HTTPS port number.
// +kubebuilder:default:=443
Https int `json:"https"`
}
type HttpAndHttps struct {
// Endpoint addresses list.
// +kubebuilder:default:={}
Endpoints []string `json:"endpoints,omitempty"`
// Mode for balancer.
// +kubebuilder:default:="tcp"
Mode Mode `json:"mode"`
// Target ports configuration.
// +kubebuilder:default:={}
TargetPorts TargetPorts `json:"targetPorts"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string
// +kubebuilder:validation:Enum="tcp";"tcp-with-proxy"
type Mode string

View File

@@ -1,40 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package tenant
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Deploy own Etcd cluster.
// +kubebuilder:default:=false
Etcd bool `json:"etcd"`
// The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host).
// +kubebuilder:default:=""
Host string `json:"host,omitempty"`
// Deploy own Ingress Controller.
// +kubebuilder:default:=false
Ingress bool `json:"ingress"`
// Deploy own Monitoring Stack.
// +kubebuilder:default:=false
Monitoring bool `json:"monitoring"`
// Define resource quotas for the tenant.
// +kubebuilder:default:={}
ResourceQuotas map[string]resource.Quantity `json:"resourceQuotas,omitempty"`
// Deploy own SeaweedFS.
// +kubebuilder:default:=false
Seaweedfs bool `json:"seaweedfs"`
}

View File

@@ -1,53 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package vmdisk
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Defines if disk should be considered optical.
// +kubebuilder:default:=false
Optical bool `json:"optical"`
// The source image location used to create a disk.
// +kubebuilder:default:={}
Source Source `json:"source"`
// The size of the disk allocated for the virtual machine.
// +kubebuilder:default:="5Gi"
Storage resource.Quantity `json:"storage"`
// StorageClass used to store the data.
// +kubebuilder:default:="replicated"
StorageClass string `json:"storageClass"`
}
type SourceImage struct {
// Name of the image to use (uploaded as "golden image" or from the list: `ubuntu`, `fedora`, `cirros`, `alpine`, `talos`).
Name string `json:"name"`
}
type SourceHTTP struct {
// URL to download the image.
Url string `json:"url"`
}
type Source struct {
// Download image from an HTTP source.
Http *SourceHTTP `json:"http,omitempty"`
// Use image by name.
Image *SourceImage `json:"image,omitempty"`
// Upload local image.
Upload *SourceUpload `json:"upload,omitempty"`
}

View File

@@ -1,96 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package vminstance
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Cloud-init user data.
// +kubebuilder:default:=""
CloudInit string `json:"cloudInit"`
// Seed string to generate SMBIOS UUID for the VM.
// +kubebuilder:default:=""
CloudInitSeed string `json:"cloudInitSeed"`
// Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map
// +kubebuilder:default:=""
CpuModel string `json:"cpuModel"`
// List of disks to attach.
// +kubebuilder:default:={}
Disks []Disk `json:"disks,omitempty"`
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// Method to pass through traffic to the VM.
// +kubebuilder:default:="PortList"
ExternalMethod ExternalMethod `json:"externalMethod"`
// Ports to forward from outside the cluster.
// +kubebuilder:default:={22}
ExternalPorts []int `json:"externalPorts,omitempty"`
// List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM).
// +kubebuilder:default:={}
Gpus []GPU `json:"gpus,omitempty"`
// Virtual Machine preferences profile.
// +kubebuilder:default:="ubuntu"
InstanceProfile string `json:"instanceProfile"`
// Virtual Machine instance type.
// +kubebuilder:default:="u1.medium"
InstanceType string `json:"instanceType"`
// Resource configuration for the virtual machine.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Requested running state of the VirtualMachineInstance
// +kubebuilder:default:="Always"
RunStrategy RunStrategy `json:"runStrategy"`
// List of SSH public keys for authentication.
// +kubebuilder:default:={}
SshKeys []string `json:"sshKeys,omitempty"`
// Additional subnets
// +kubebuilder:default:={}
Subnets []Subnet `json:"subnets,omitempty"`
}
type Disk struct {
// Disk bus type (e.g. "sata").
Bus string `json:"bus,omitempty"`
// Disk name.
Name string `json:"name"`
}
type Subnet struct {
// Subnet name
Name string `json:"name,omitempty"`
}
type GPU struct {
// The name of the GPU resource to attach.
Name string `json:"name"`
}
type Resources struct {
// Number of CPU cores allocated.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Amount of memory allocated.
Memory resource.Quantity `json:"memory,omitempty"`
// Number of CPU sockets (vCPU topology).
Sockets resource.Quantity `json:"sockets,omitempty"`
}
// +kubebuilder:validation:Enum="Always";"Halted";"Manual";"RerunOnFailure";"Once"
type RunStrategy string
// +kubebuilder:validation:Enum="PortList";"WholeIP"
type ExternalMethod string

View File

@@ -1,31 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package vpc
import (
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Subnets of a VPC
// +kubebuilder:default:={}
Subnets []Subnet `json:"subnets,omitempty"`
}
type Subnet struct {
// IP address range
Cidr string `json:"cidr,omitempty"`
// Subnet name
Name string `json:"name"`
}

View File

@@ -1,58 +0,0 @@
// +kubebuilder:object:generate=true
// +kubebuilder:object:root=true
// +groupName=values.helm.io
// +versionName=v1alpha1
// Code generated by values-gen. DO NOT EDIT.
package vpn
import (
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Config struct {
v1.TypeMeta `json:",inline"`
v1.ObjectMeta `json:"metadata,omitempty"`
Spec ConfigSpec `json:"spec,omitempty"`
}
type ConfigSpec struct {
// Enable external access from outside the cluster.
// +kubebuilder:default:=false
External bool `json:"external"`
// List of externalIPs for service. Optional. If not specified, will use LoadBalancer service by default.
// +kubebuilder:default:={}
ExternalIPs []string `json:"externalIPs,omitempty"`
// Host used to substitute into generated URLs.
// +kubebuilder:default:=""
Host string `json:"host"`
// Number of VPN server replicas.
// +kubebuilder:default:=2
Replicas int `json:"replicas"`
// Explicit CPU and memory configuration for each VPN server replica. When omitted, the preset defined in `resourcesPreset` is applied.
// +kubebuilder:default:={}
Resources Resources `json:"resources,omitempty"`
// Default sizing preset used when `resources` is omitted.
// +kubebuilder:default:="nano"
ResourcesPreset ResourcesPreset `json:"resourcesPreset"`
// Users configuration map.
// +kubebuilder:default:={}
Users map[string]User `json:"users,omitempty"`
}
type Resources struct {
// CPU available to each replica.
Cpu resource.Quantity `json:"cpu,omitempty"`
// Memory (RAM) available to each replica.
Memory resource.Quantity `json:"memory,omitempty"`
}
type User struct {
// Password for the user (autogenerated if not provided).
Password string `json:"password,omitempty"`
}
// +kubebuilder:validation:Enum="nano";"micro";"small";"medium";"large";"xlarge";"2xlarge"
type ResourcesPreset string

View File

@@ -1,65 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Package v1alpha1 defines strategy.backups.cozystack.io API types.
//
// Group: strategy.backups.cozystack.io
// Version: v1alpha1
package v1alpha1
import (
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&Velero{},
&VeleroList{},
)
return nil
})
}
const (
VeleroStrategyKind = "Velero"
)
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// Velero defines a backup strategy using Velero as the driver.
type Velero struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec VeleroSpec `json:"spec,omitempty"`
Status VeleroStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// VeleroList contains a list of Velero backup strategies.
type VeleroList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Velero `json:"items"`
}
// VeleroSpec specifies the desired strategy for backing up with Velero.
type VeleroSpec struct {
Template VeleroTemplate `json:"template"`
}
// VeleroTemplate describes the data a backup.velero.io should have when
// templated from a Velero backup strategy.
type VeleroTemplate struct {
Spec velerov1.BackupSpec `json:"spec"`
// +optional
RestoreSpec *velerov1.RestoreSpec `json:"restoreSpec,omitempty"`
}
type VeleroStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

View File

@@ -21,7 +21,6 @@ limitations under the License.
package v1alpha1
import (
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -122,121 +121,3 @@ func (in *JobStatus) DeepCopy() *JobStatus {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Velero) DeepCopyInto(out *Velero) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Velero.
func (in *Velero) DeepCopy() *Velero {
if in == nil {
return nil
}
out := new(Velero)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Velero) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VeleroList) DeepCopyInto(out *VeleroList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Velero, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroList.
func (in *VeleroList) DeepCopy() *VeleroList {
if in == nil {
return nil
}
out := new(VeleroList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *VeleroList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VeleroSpec) DeepCopyInto(out *VeleroSpec) {
*out = *in
in.Template.DeepCopyInto(&out.Template)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroSpec.
func (in *VeleroSpec) DeepCopy() *VeleroSpec {
if in == nil {
return nil
}
out := new(VeleroSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VeleroStatus) DeepCopyInto(out *VeleroStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroStatus.
func (in *VeleroStatus) DeepCopy() *VeleroStatus {
if in == nil {
return nil
}
out := new(VeleroStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VeleroTemplate) DeepCopyInto(out *VeleroTemplate) {
*out = *in
in.Spec.DeepCopyInto(&out.Spec)
if in.RestoreSpec != nil {
in, out := &in.RestoreSpec, &out.RestoreSpec
*out = new(velerov1.RestoreSpec)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroTemplate.
func (in *VeleroTemplate) DeepCopy() *VeleroTemplate {
if in == nil {
return nil
}
out := new(VeleroTemplate)
in.DeepCopyInto(out)
return out
}

View File

@@ -100,13 +100,13 @@ Describe **when**, **how**, and **where** to back up a specific managed applicat
```go
type PlanSpec struct {
// Application to back up.
// If apiGroup is not specified, it defaults to "apps.cozystack.io".
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
// BackupClassName references a BackupClass that contains strategy and other parameters (e.g. storage reference).
// The BackupClass will be resolved to determine the appropriate strategy and parameters
// based on the ApplicationRef.
BackupClassName string `json:"backupClassName"`
// Where backups should be stored.
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
// Driver-specific BackupStrategy to use.
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
// When backups should run.
Schedule PlanSchedule `json:"schedule"`
@@ -145,12 +145,12 @@ Core Plan controller:
* Create a `BackupJob` in the same namespace:
* `spec.planRef.name = plan.Name`
* `spec.applicationRef = plan.spec.applicationRef` (normalized with default apiGroup if not specified)
* `spec.backupClassName = plan.spec.backupClassName`
* `spec.applicationRef = plan.spec.applicationRef`
* `spec.storageRef = plan.spec.storageRef`
* `spec.strategyRef = plan.spec.strategyRef`
* `spec.triggeredBy = "Plan"`
* Set `ownerReferences` so the `BackupJob` is owned by the `Plan`.
**Note:** The `BackupJob` controller resolves the `BackupClass` to determine the appropriate strategy and parameters, based on the `ApplicationRef`. The strategy template is processed with a context containing the `Application` object and `Parameters` from the `BackupClass`.
The Plan controller does **not**:
* Execute backups itself.
@@ -159,64 +159,17 @@ The Plan controller does **not**:
---
### 4.2 BackupClass
### 4.2 Storage
**Group/Kind**
`backups.cozystack.io/v1alpha1, Kind=BackupClass`
**API Shape**
**Purpose**
Define a class of backup configurations that encapsulate strategy and parameters per application type. `BackupClass` is a cluster-scoped resource that allows admins to configure backup strategies and parameters in a reusable way.
TBD
**Key fields (spec)**
**Storage usage**
```go
type BackupClassSpec struct {
// Strategies is a list of backup strategies, each matching a specific application type.
Strategies []BackupClassStrategy `json:"strategies"`
}
type BackupClassStrategy struct {
// StrategyRef references the driver-specific BackupStrategy (e.g., Velero).
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
// Application specifies which application types this strategy applies to.
// If apiGroup is not specified, it defaults to "apps.cozystack.io".
Application ApplicationSelector `json:"application"`
// Parameters holds strategy-specific parameters, like storage reference.
// Common parameters include:
// - backupStorageLocationName: Name of Velero BackupStorageLocation
// +optional
Parameters map[string]string `json:"parameters,omitempty"`
}
type ApplicationSelector struct {
// APIGroup is the API group of the application.
// If not specified, defaults to "apps.cozystack.io".
// +optional
APIGroup *string `json:"apiGroup,omitempty"`
// Kind is the kind of the application (e.g., VirtualMachine, MariaDB).
Kind string `json:"kind"`
}
```
**BackupClass resolution**
* When a `BackupJob` or `Plan` references a `BackupClass` via `backupClassName`, the controller:
1. Fetches the `BackupClass` by name.
2. Matches the `ApplicationRef` against strategies in the `BackupClass`:
* Normalizes `ApplicationRef.apiGroup` (defaults to `"apps.cozystack.io"` if not specified).
* Finds a strategy where `ApplicationSelector` matches the `ApplicationRef` (apiGroup and kind).
3. Returns the matched `StrategyRef` and `Parameters`.
* Strategy templates (e.g., Velero's `backupTemplate.spec`) are processed with a context containing:
* `Application`: The application object being backed up.
* `Parameters`: The parameters from the matched `BackupClassStrategy`.
**Parameters**
* Parameters are passed via `Parameters` in the `BackupClass` (e.g., `backupStorageLocationName` for Velero).
* The driver uses these parameters to resolve the actual resources (e.g., Velero's `BackupStorageLocation` CRD).
* `Plan` and `BackupJob` reference `Storage` via `TypedLocalObjectReference`.
* Drivers read `Storage` to know how/where to store or read artifacts.
* Core treats `Storage` spec as opaque; it does not directly talk to S3 or buckets.
---
@@ -236,13 +189,16 @@ type BackupJobSpec struct {
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
// Application to back up.
// If apiGroup is not specified, it defaults to "apps.cozystack.io".
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
// BackupClassName references a BackupClass that contains strategy and related parameters
// The BackupClass will be resolved to determine the appropriate strategy and parameters
// based on the ApplicationRef.
BackupClassName string `json:"backupClassName"`
// Storage to use.
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
// Driver-specific BackupStrategy to use.
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
// Informational: what triggered this run ("Plan", "Manual", etc.).
TriggeredBy string `json:"triggeredBy,omitempty"`
}
```
@@ -267,9 +223,7 @@ type BackupJobStatus struct {
* Each driver controller:
* Watches `BackupJob`.
* Resolves the `BackupClass` referenced by `spec.backupClassName`.
* Matches the `ApplicationRef` against strategies in the `BackupClass` to find the appropriate strategy.
* Reconciles runs where the resolved strategy's `apiGroup/kind` matches its **strategy type(s)**.
* Reconciles runs where `spec.strategyRef.apiGroup/kind` matches its **strategy type(s)**.
* Driver responsibilities:
1. On first reconcile:
@@ -278,12 +232,7 @@ type BackupJobStatus struct {
* Set `status.phase = Running`.
2. Resolve inputs:
* Resolve `BackupClass` from `spec.backupClassName`.
* Match `ApplicationRef` against `BackupClass` strategies to get `StrategyRef` and `Parameters`.
* Read `Strategy` (driver-owned CRD) from `StrategyRef`.
* Read `Application` from `ApplicationRef`.
* Extract parameters from `Parameters` (e.g., `backupStorageLocationName` for Velero).
* Process strategy template with context: `Application` object and `Parameters` from `BackupClass`.
* Read `Strategy` (driver-owned CRD), `Storage`, `Application`, optionally `Plan`.
3. Execute backup logic (implementation-specific).
4. On success:
@@ -315,14 +264,13 @@ Represent a single **backup artifact** for a given application, decoupled from a
type BackupSpec struct {
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
TakenAt metav1.Time `json:"takenAt"`
DriverMetadata map[string]string `json:"driverMetadata,omitempty"`
}
```
**Note:** Parameters are not stored directly in `Backup`. Instead, they are resolved from `BackupClass` parameters when the backup was created. The storage location is managed by the driver (e.g., Velero's `BackupStorageLocation`) and referenced via parameters in the `BackupClass`.
**Key fields (status)**
```go
@@ -342,8 +290,7 @@ type BackupStatus struct {
* Creates a `Backup` in the same namespace (typically owned by the `BackupJob`).
* Populates `spec` fields with:
* The application reference.
* The strategy reference (resolved from `BackupClass` during `BackupJob` execution).
* The application, storage, strategy references.
* `takenAt`.
* Optional `driverMetadata`.
* Sets `status` with:
@@ -359,8 +306,6 @@ type BackupStatus struct {
* Anchor `RestoreJob` operations.
* Implement higher-level policies (retention) if needed.
**Note:** Parameters are resolved from `BackupClass` when the `BackupJob` is created. The driver uses these parameters to determine where to store backups. The storage location itself is managed by the driver (e.g., Velero's `BackupStorageLocation` CRD) and is not directly referenced in the `Backup` resource. When restoring, the driver resolves the storage location from the original `BackupClass` parameters or from the driver's own metadata.
---
### 4.5 RestoreJob
@@ -408,13 +353,13 @@ type RestoreJobStatus struct {
* Determines effective:
* **Strategy**: `backup.spec.strategyRef`.
* **Storage**: Resolved from driver metadata or `BackupClass` parameters (e.g., `backupStorageLocationName` stored in `driverMetadata` or resolved from the original `BackupClass`).
* **Storage**: `backup.spec.storageRef`.
* **Target application**: `spec.targetApplicationRef` or `backup.spec.applicationRef`.
* If effective strategys GVK is one of its supported strategy types → driver is responsible.
3. Behaviour:
* On first reconcile, set `status.startedAt` and `phase = Running`.
* Resolve `Backup`, storage location (from driver metadata or `BackupClass`), `Strategy`, target application.
* Resolve `Backup`, `Storage`, `Strategy`, target application.
* Execute restore logic (implementation-specific).
* On success:
@@ -469,10 +414,8 @@ The Cozystack backups core API:
* Uses a single group, `backups.cozystack.io`, for all core CRDs.
* Cleanly separates:
* **When** (Plan schedule) core-owned.
* **How & where** (BackupClass) central configuration unit that encapsulates strategy and parameters (e.g., storage reference) per application type, resolved per BackupJob/Plan.
* **Execution** (BackupJob) created by Plan when schedule fires, resolves BackupClass to get strategy and parameters, then delegates to driver.
* **When & where** (Plan + Storage) core-owned.
* **What backup artifacts exist** (Backup) driver-created but cluster-visible.
* **Restore lifecycle** (RestoreJob) shared contract boundary.
* **Execution lifecycle** (BackupJob, RestoreJob) shared contract boundary.
* Allows multiple strategy drivers to implement backup/restore logic without entangling their implementation with the core API.

View File

@@ -8,19 +8,8 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&Backup{},
&BackupList{},
)
return nil
})
}
// BackupPhase represents the lifecycle phase of a Backup.
type BackupPhase string
@@ -57,6 +46,10 @@ type BackupSpec struct {
// +optional
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
// StorageRef refers to the Storage object that describes where the backup
// artifact is stored.
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
// StrategyRef refers to the driver-specific BackupStrategy that was used
// to create this backup. This allows the driver to later perform restores.
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
@@ -88,12 +81,7 @@ type BackupStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// The field indexing on applicationRef will be needed later to display per-app backup resources.
// +kubebuilder:object:root=true
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
// Backup represents a single backup artifact for a given application.
type Backup struct {

View File

@@ -1,85 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// Package v1alpha1 defines backups.cozystack.io API types.
//
// Group: backups.cozystack.io
// Version: v1alpha1
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&BackupClass{},
&BackupClassList{},
)
return nil
})
}
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:subresource:status
// BackupClass defines a class of backup configurations that can be referenced
// by BackupJob and Plan resources. It encapsulates strategy and storage configuration
// per application type.
type BackupClass struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BackupClassSpec `json:"spec,omitempty"`
Status BackupClassStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BackupClassList contains a list of BackupClasses.
type BackupClassList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BackupClass `json:"items"`
}
// BackupClassSpec defines the desired state of a BackupClass.
type BackupClassSpec struct {
// Strategies is a list of backup strategies, each matching a specific application type.
Strategies []BackupClassStrategy `json:"strategies"`
}
// BackupClassStrategy defines a backup strategy for a specific application type.
type BackupClassStrategy struct {
// StrategyRef references the driver-specific BackupStrategy (e.g., Velero).
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
// Application specifies which application types this strategy applies to.
Application ApplicationSelector `json:"application"`
// Parameters holds strategy-specific and storage-specific parameters.
// Common parameters include:
// - backupStorageLocationName: Name of Velero BackupStorageLocation
// +optional
Parameters map[string]string `json:"parameters,omitempty"`
}
// ApplicationSelector specifies which application types a strategy applies to.
type ApplicationSelector struct {
// APIGroup is the API group of the application.
// If not specified, defaults to "apps.cozystack.io".
// +optional
APIGroup *string `json:"apiGroup,omitempty"`
// Kind is the kind of the application (e.g., VirtualMachine, MariaDB).
Kind string `json:"kind"`
}
// BackupClassStatus defines the observed state of a BackupClass.
type BackupClassStatus struct {
// Conditions represents the latest available observations of a BackupClass's state.
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

View File

@@ -8,26 +8,6 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&BackupJob{},
&BackupJobList{},
)
return nil
})
}
const (
OwningJobNameLabel = thisGroup + "/owned-by.BackupJobName"
OwningJobNamespaceLabel = thisGroup + "/owned-by.BackupJobNamespace"
// DefaultApplicationAPIGroup is the default API group for applications
// when not specified in ApplicationRef or ApplicationSelector.
DefaultApplicationAPIGroup = "apps.cozystack.io"
)
// BackupJobPhase represents the lifecycle phase of a BackupJob.
@@ -50,16 +30,15 @@ type BackupJobSpec struct {
// ApplicationRef holds a reference to the managed application whose state
// is being backed up.
// If apiGroup is not specified, it defaults to "apps.cozystack.io".
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
// BackupClassName references a BackupClass that contains strategy and storage configuration.
// The BackupClass will be resolved to determine the appropriate strategy and storage
// based on the ApplicationRef.
// This field is immutable once the BackupJob is created.
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="backupClassName is immutable"
BackupClassName string `json:"backupClassName"`
// StorageRef holds a reference to the Storage object that describes where
// the backup will be stored.
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
// StrategyRef holds a reference to the driver-specific BackupStrategy object
// that describes how the backup should be created.
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
}
// BackupJobStatus represents the observed state of a BackupJob.
@@ -92,14 +71,7 @@ type BackupJobStatus struct {
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// The field indexing on applicationRef will be needed later to display per-app backup resources.
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",priority=0
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
// BackupJob represents a single execution of a backup.
// It is typically created by a Plan controller when a schedule fires.
@@ -119,13 +91,3 @@ type BackupJobList struct {
metav1.ListMeta `json:"metadata,omitempty"`
Items []BackupJob `json:"items"`
}
// NormalizeApplicationRef sets the default apiGroup to DefaultApplicationAPIGroup if it's not specified.
// This function is exported so it can be used by other packages (e.g., controllers, factories).
func NormalizeApplicationRef(ref corev1.TypedLocalObjectReference) corev1.TypedLocalObjectReference {
if ref.APIGroup == nil || *ref.APIGroup == "" {
apiGroup := DefaultApplicationAPIGroup
ref.APIGroup = &apiGroup
}
return ref
}

View File

@@ -25,18 +25,29 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)
const (
thisGroup = "backups.cozystack.io"
thisVersion = "v1alpha1"
)
var (
GroupVersion = schema.GroupVersion{Group: thisGroup, Version: thisVersion}
SchemeBuilder = runtime.NewSchemeBuilder(addGroupVersion)
AddToScheme = SchemeBuilder.AddToScheme
// GroupVersion is group version used to register these objects.
GroupVersion = schema.GroupVersion{Group: "backups.cozystack.io", Version: "v1alpha1"}
// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
func addGroupVersion(scheme *runtime.Scheme) error {
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(
GroupVersion,
&Plan{},
&PlanList{},
&BackupJob{},
&BackupJobList{},
&Backup{},
&BackupList{},
&RestoreJob{},
&RestoreJobList{},
)
metav1.AddToGroupVersion(scheme, GroupVersion)
return nil
}

View File

@@ -8,19 +8,8 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&Plan{},
&PlanList{},
)
return nil
})
}
type PlanScheduleType string
const (
@@ -33,13 +22,8 @@ const (
PlanConditionError = "Error"
)
// The field indexing on applicationRef will be needed later to display per-app backup resources.
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
// Plan describes the schedule, method and storage location for the
// backup of a given target application.
@@ -65,13 +49,15 @@ type PlanList struct {
type PlanSpec struct {
// ApplicationRef holds a reference to the managed application,
// whose state and configuration must be backed up.
// If apiGroup is not specified, it defaults to "apps.cozystack.io".
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
// BackupClassName references a BackupClass that contains strategy and storage configuration.
// The BackupClass will be resolved to determine the appropriate strategy and storage
// based on the ApplicationRef.
BackupClassName string `json:"backupClassName"`
// StorageRef holds a reference to the Storage object that
// describes the location where the backup will be stored.
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
// StrategyRef holds a reference to the Strategy object that
// describes, how a backup copy is to be created.
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
// Schedule specifies when backup copies are created.
Schedule PlanSchedule `json:"schedule"`

View File

@@ -8,19 +8,8 @@ package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(GroupVersion,
&RestoreJob{},
&RestoreJobList{},
)
return nil
})
}
// RestoreJobPhase represents the lifecycle phase of a RestoreJob.
type RestoreJobPhase string
@@ -71,8 +60,6 @@ type RestoreJobStatus struct {
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",priority=0
// RestoreJob represents a single execution of a restore from a Backup.
type RestoreJob struct {

View File

@@ -26,26 +26,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationSelector) DeepCopyInto(out *ApplicationSelector) {
*out = *in
if in.APIGroup != nil {
in, out := &in.APIGroup, &out.APIGroup
*out = new(string)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationSelector.
func (in *ApplicationSelector) DeepCopy() *ApplicationSelector {
if in == nil {
return nil
}
out := new(ApplicationSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Backup) DeepCopyInto(out *Backup) {
*out = *in
@@ -88,133 +68,6 @@ func (in *BackupArtifact) DeepCopy() *BackupArtifact {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupClass) DeepCopyInto(out *BackupClass) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupClass.
func (in *BackupClass) DeepCopy() *BackupClass {
if in == nil {
return nil
}
out := new(BackupClass)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BackupClass) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupClassList) DeepCopyInto(out *BackupClassList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]BackupClass, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupClassList.
func (in *BackupClassList) DeepCopy() *BackupClassList {
if in == nil {
return nil
}
out := new(BackupClassList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BackupClassList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupClassSpec) DeepCopyInto(out *BackupClassSpec) {
*out = *in
if in.Strategies != nil {
in, out := &in.Strategies, &out.Strategies
*out = make([]BackupClassStrategy, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupClassSpec.
func (in *BackupClassSpec) DeepCopy() *BackupClassSpec {
if in == nil {
return nil
}
out := new(BackupClassSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupClassStatus) DeepCopyInto(out *BackupClassStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupClassStatus.
func (in *BackupClassStatus) DeepCopy() *BackupClassStatus {
if in == nil {
return nil
}
out := new(BackupClassStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupClassStrategy) DeepCopyInto(out *BackupClassStrategy) {
*out = *in
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
in.Application.DeepCopyInto(&out.Application)
if in.Parameters != nil {
in, out := &in.Parameters, &out.Parameters
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupClassStrategy.
func (in *BackupClassStrategy) DeepCopy() *BackupClassStrategy {
if in == nil {
return nil
}
out := new(BackupClassStrategy)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BackupJob) DeepCopyInto(out *BackupJob) {
*out = *in
@@ -283,6 +136,8 @@ func (in *BackupJobSpec) DeepCopyInto(out *BackupJobSpec) {
**out = **in
}
in.ApplicationRef.DeepCopyInto(&out.ApplicationRef)
in.StorageRef.DeepCopyInto(&out.StorageRef)
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJobSpec.
@@ -371,6 +226,7 @@ func (in *BackupSpec) DeepCopyInto(out *BackupSpec) {
*out = new(v1.LocalObjectReference)
**out = **in
}
in.StorageRef.DeepCopyInto(&out.StorageRef)
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
in.TakenAt.DeepCopyInto(&out.TakenAt)
if in.DriverMetadata != nil {
@@ -425,7 +281,6 @@ func (in *Plan) DeepCopyInto(out *Plan) {
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Plan.
@@ -497,6 +352,8 @@ func (in *PlanSchedule) DeepCopy() *PlanSchedule {
func (in *PlanSpec) DeepCopyInto(out *PlanSpec) {
*out = *in
in.ApplicationRef.DeepCopyInto(&out.ApplicationRef)
in.StorageRef.DeepCopyInto(&out.StorageRef)
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
out.Schedule = in.Schedule
}
@@ -510,28 +367,6 @@ func (in *PlanSpec) DeepCopy() *PlanSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PlanStatus) DeepCopyInto(out *PlanStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanStatus.
func (in *PlanStatus) DeepCopy() *PlanStatus {
if in == nil {
return nil
}
out := new(PlanStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RestoreJob) DeepCopyInto(out *RestoreJob) {
*out = *in

View File

@@ -253,25 +253,3 @@ type FactoryList struct {
metav1.ListMeta `json:"metadata,omitempty"`
Items []Factory `json:"items"`
}
// -----------------------------------------------------------------------------
// CustomFormsOverrideMapping
// -----------------------------------------------------------------------------
// +kubebuilder:object:root=true
// +kubebuilder:resource:path=cfomappings,scope=Cluster
// +kubebuilder:subresource:status
type CFOMapping struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ArbitrarySpec `json:"spec"`
Status CommonStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
type CFOMappingList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []CFOMapping `json:"items"`
}

View File

@@ -69,9 +69,6 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&Factory{},
&FactoryList{},
&CFOMapping{},
&CFOMappingList{},
)
metav1.AddToGroupVersion(scheme, GroupVersion)
return nil

View File

@@ -159,65 +159,6 @@ func (in *BreadcrumbList) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CFOMapping) DeepCopyInto(out *CFOMapping) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFOMapping.
func (in *CFOMapping) DeepCopy() *CFOMapping {
if in == nil {
return nil
}
out := new(CFOMapping)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *CFOMapping) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CFOMappingList) DeepCopyInto(out *CFOMappingList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]CFOMapping, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFOMappingList.
func (in *CFOMappingList) DeepCopy() *CFOMappingList {
if in == nil {
return nil
}
out := new(CFOMappingList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *CFOMappingList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CommonStatus) DeepCopyInto(out *CommonStatus) {
*out = *in

View File

@@ -17,52 +17,69 @@ limitations under the License.
package v1alpha1
import (
helmv2 "github.com/fluxcd/helm-controller/api/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster
// ApplicationDefinition is the Schema for the applicationdefinitions API
type ApplicationDefinition struct {
// CozystackResourceDefinition is the Schema for the cozystackresourcedefinitions API
type CozystackResourceDefinition struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ApplicationDefinitionSpec `json:"spec,omitempty"`
Spec CozystackResourceDefinitionSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// ApplicationDefinitionList contains a list of ApplicationDefinitions
type ApplicationDefinitionList struct {
// CozystackResourceDefinitionList contains a list of CozystackResourceDefinitions
type CozystackResourceDefinitionList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ApplicationDefinition `json:"items"`
Items []CozystackResourceDefinition `json:"items"`
}
func init() {
SchemeBuilder.Register(&ApplicationDefinition{}, &ApplicationDefinitionList{})
SchemeBuilder.Register(&CozystackResourceDefinition{}, &CozystackResourceDefinitionList{})
}
type ApplicationDefinitionSpec struct {
type CozystackResourceDefinitionSpec struct {
// Application configuration
Application ApplicationDefinitionApplication `json:"application"`
Application CozystackResourceDefinitionApplication `json:"application"`
// Release configuration
Release ApplicationDefinitionRelease `json:"release"`
Release CozystackResourceDefinitionRelease `json:"release"`
// Secret selectors
Secrets ApplicationDefinitionResources `json:"secrets,omitempty"`
Secrets CozystackResourceDefinitionResources `json:"secrets,omitempty"`
// Service selectors
Services ApplicationDefinitionResources `json:"services,omitempty"`
Services CozystackResourceDefinitionResources `json:"services,omitempty"`
// Ingress selectors
Ingresses ApplicationDefinitionResources `json:"ingresses,omitempty"`
Ingresses CozystackResourceDefinitionResources `json:"ingresses,omitempty"`
// Dashboard configuration for this resource
Dashboard *ApplicationDefinitionDashboard `json:"dashboard,omitempty"`
Dashboard *CozystackResourceDefinitionDashboard `json:"dashboard,omitempty"`
}
type ApplicationDefinitionApplication struct {
type CozystackResourceDefinitionChart struct {
// Name of the Helm chart
Name string `json:"name"`
// Source reference for the Helm chart
SourceRef SourceRef `json:"sourceRef"`
}
type SourceRef struct {
// Kind of the source reference
// +kubebuilder:default:="HelmRepository"
Kind string `json:"kind"`
// Name of the source reference
Name string `json:"name"`
// Namespace of the source reference
// +kubebuilder:default:="cozy-public"
Namespace string `json:"namespace"`
}
type CozystackResourceDefinitionApplication struct {
// Kind of the application, used for UI and API
Kind string `json:"kind"`
// OpenAPI schema for the application, used for API validation
@@ -73,16 +90,16 @@ type ApplicationDefinitionApplication struct {
Singular string `json:"singular"`
}
type ApplicationDefinitionRelease struct {
// Reference to the chart source
ChartRef *helmv2.CrossNamespaceSourceReference `json:"chartRef"`
type CozystackResourceDefinitionRelease struct {
// Helm chart configuration
Chart CozystackResourceDefinitionChart `json:"chart"`
// Labels for the release
Labels map[string]string `json:"labels,omitempty"`
// Prefix for the release name
Prefix string `json:"prefix"`
}
// ApplicationDefinitionResourceSelector extends metav1.LabelSelector with resourceNames support.
// CozystackResourceDefinitionResourceSelector extends metav1.LabelSelector with resourceNames support.
// A resource matches this selector only if it satisfies ALL criteria:
// - Label selector conditions (matchExpressions and matchLabels)
// - AND has a name that matches one of the names in resourceNames (if specified)
@@ -93,19 +110,18 @@ type ApplicationDefinitionRelease struct {
// - {{ .namespace }}: The namespace of the resource being processed
//
// Example YAML:
//
// secrets:
// include:
// - matchExpressions:
// - key: badlabel
// operator: DoesNotExist
// matchLabels:
// goodlabel: goodvalue
// resourceNames:
// - "{{ .name }}-secret"
// - "{{ .kind }}-{{ .name }}-tls"
// - "specificname"
type ApplicationDefinitionResourceSelector struct {
// secrets:
// include:
// - matchExpressions:
// - key: badlabel
// operator: DoesNotExist
// matchLabels:
// goodlabel: goodvalue
// resourceNames:
// - "{{ .name }}-secret"
// - "{{ .kind }}-{{ .name }}-tls"
// - "specificname"
type CozystackResourceDefinitionResourceSelector struct {
metav1.LabelSelector `json:",inline"`
// ResourceNames is a list of resource names to match
// If specified, the resource must have one of these exact names to match the selector
@@ -113,16 +129,16 @@ type ApplicationDefinitionResourceSelector struct {
ResourceNames []string `json:"resourceNames,omitempty"`
}
type ApplicationDefinitionResources struct {
type CozystackResourceDefinitionResources struct {
// Exclude contains an array of resource selectors that target resources.
// If a resource matches the selector in any of the elements in the array, it is
// hidden from the user, regardless of the matches in the include array.
Exclude []*ApplicationDefinitionResourceSelector `json:"exclude,omitempty"`
Exclude []*CozystackResourceDefinitionResourceSelector `json:"exclude,omitempty"`
// Include contains an array of resource selectors that target resources.
// If a resource matches the selector in any of the elements in the array, and
// matches none of the selectors in the exclude array that resource is marked
// as a tenant resource and is visible to users.
Include []*ApplicationDefinitionResourceSelector `json:"include,omitempty"`
Include []*CozystackResourceDefinitionResourceSelector `json:"include,omitempty"`
}
// ---- Dashboard types ----
@@ -139,8 +155,8 @@ const (
DashboardTabYAML DashboardTab = "yaml"
)
// ApplicationDefinitionDashboard describes how this resource appears in the UI.
type ApplicationDefinitionDashboard struct {
// CozystackResourceDefinitionDashboard describes how this resource appears in the UI.
type CozystackResourceDefinitionDashboard struct {
// Human-readable name shown in the UI (e.g., "Bucket")
Singular string `json:"singular"`
// Plural human-readable name (e.g., "Buckets")

View File

@@ -1,100 +0,0 @@
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster,shortName={pkg,pkgs}
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Variant",type="string",JSONPath=".spec.variant",description="Selected variant"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Ready status"
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message",description="Ready message"
// Package is the Schema for the packages API
type Package struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PackageSpec `json:"spec,omitempty"`
Status PackageStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// PackageList contains a list of Packages
type PackageList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Package `json:"items"`
}
func init() {
SchemeBuilder.Register(&Package{}, &PackageList{})
}
// PackageSpec defines the desired state of Package
type PackageSpec struct {
// Variant is the name of the variant to use from the PackageSource
// If not specified, defaults to "default"
// +optional
Variant string `json:"variant,omitempty"`
// IgnoreDependencies is a list of package source dependencies to ignore
// Dependencies listed here will not be installed even if they are specified in the PackageSource
// +optional
IgnoreDependencies []string `json:"ignoreDependencies,omitempty"`
// Components is a map of release name to component overrides
// Allows overriding values and enabling/disabling specific components from the PackageSource
// +optional
Components map[string]PackageComponent `json:"components,omitempty"`
}
// PackageComponent defines overrides for a specific component
type PackageComponent struct {
// Enabled indicates whether this component should be installed
// If false, the component will be disabled even if it's defined in the PackageSource
// +optional
Enabled *bool `json:"enabled,omitempty"`
// Values contains Helm chart values as a JSON object
// These values will be merged with the default values from the PackageSource
// +optional
Values *apiextensionsv1.JSON `json:"values,omitempty"`
}
// PackageStatus defines the observed state of Package
type PackageStatus struct {
// Conditions represents the latest available observations of a Package's state
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
// Dependencies tracks the readiness status of each dependency
// Key is the dependency package name, value indicates if the dependency is ready
// +optional
Dependencies map[string]DependencyStatus `json:"dependencies,omitempty"`
}
// DependencyStatus represents the readiness status of a dependency
type DependencyStatus struct {
// Ready indicates whether the dependency is ready
Ready bool `json:"ready"`
}

View File

@@ -1,171 +0,0 @@
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +kubebuilder:object:root=true
// +kubebuilder:resource:scope=Cluster,shortName={pks}
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Variants",type="string",JSONPath=".status.variants",description="Package variants (comma-separated)"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Ready status"
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message",description="Ready message"
// PackageSource is the Schema for the packagesources API
type PackageSource struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PackageSourceSpec `json:"spec,omitempty"`
Status PackageSourceStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// PackageSourceList contains a list of PackageSources
type PackageSourceList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []PackageSource `json:"items"`
}
func init() {
SchemeBuilder.Register(&PackageSource{}, &PackageSourceList{})
}
// PackageSourceSpec defines the desired state of PackageSource
type PackageSourceSpec struct {
// SourceRef is the source reference for the package source charts
// +optional
SourceRef *PackageSourceRef `json:"sourceRef,omitempty"`
// Variants is a list of package source variants
// Each variant defines components, applications, dependencies, and libraries for a specific configuration
// +optional
Variants []Variant `json:"variants,omitempty"`
}
// Variant defines a single variant configuration
type Variant struct {
// Name is the unique identifier for this variant
// +required
Name string `json:"name"`
// DependsOn is a list of package source dependencies
// For example: "cozystack.networking"
// +optional
DependsOn []string `json:"dependsOn,omitempty"`
// Libraries is a list of Helm library charts used by components in this variant
// +optional
Libraries []Library `json:"libraries,omitempty"`
// Components is a list of Helm releases to be installed as part of this variant
// +optional
Components []Component `json:"components,omitempty"`
}
// Library defines a Helm library chart
type Library struct {
// Name is the optional name for library placed in charts
// +optional
Name string `json:"name,omitempty"`
// Path is the path to the library chart directory
// +required
Path string `json:"path"`
}
// PackageSourceRef defines the source reference for package source charts
type PackageSourceRef struct {
// Kind of the source reference
// +kubebuilder:validation:Enum=GitRepository;OCIRepository
// +required
Kind string `json:"kind"`
// Name of the source reference
// +required
Name string `json:"name"`
// Namespace of the source reference
// +required
Namespace string `json:"namespace"`
// Path is the base path where packages are located in the source.
// For GitRepository, defaults to "packages" if not specified.
// For OCIRepository, defaults to empty string (root) if not specified.
// +optional
Path string `json:"path,omitempty"`
}
// ComponentInstall defines installation parameters for a component
type ComponentInstall struct {
// ReleaseName is the name of the HelmRelease resource that will be created
// If not specified, defaults to the component Name field
// +optional
ReleaseName string `json:"releaseName,omitempty"`
// Namespace is the Kubernetes namespace where the release will be installed
// +optional
Namespace string `json:"namespace,omitempty"`
// Privileged indicates whether this release requires privileged access
// +optional
Privileged bool `json:"privileged,omitempty"`
// DependsOn is a list of component names that must be installed before this component
// +optional
DependsOn []string `json:"dependsOn,omitempty"`
}
// Component defines a single Helm release component within a package source
type Component struct {
// Name is the unique identifier for this component within the package source
// +required
Name string `json:"name"`
// Path is the path to the Helm chart directory
// +required
Path string `json:"path"`
// Install defines installation parameters for this component
// +optional
Install *ComponentInstall `json:"install,omitempty"`
// Libraries is a list of library names that this component depends on
// These libraries must be defined at the variant level
// +optional
Libraries []string `json:"libraries,omitempty"`
// ValuesFiles is a list of values file names to use
// +optional
ValuesFiles []string `json:"valuesFiles,omitempty"`
}
// PackageSourceStatus defines the observed state of PackageSource
type PackageSourceStatus struct {
// Variants is a comma-separated list of package variant names
// This field is populated by the controller based on spec.variants keys
// +optional
Variants string `json:"variants,omitempty"`
// Conditions represents the latest available observations of a PackageSource's state
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}

View File

@@ -21,33 +21,30 @@ limitations under the License.
package v1alpha1
import (
"github.com/fluxcd/helm-controller/api/v2"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinition) DeepCopyInto(out *ApplicationDefinition) {
func (in *CozystackResourceDefinition) DeepCopyInto(out *CozystackResourceDefinition) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinition.
func (in *ApplicationDefinition) DeepCopy() *ApplicationDefinition {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinition.
func (in *CozystackResourceDefinition) DeepCopy() *CozystackResourceDefinition {
if in == nil {
return nil
}
out := new(ApplicationDefinition)
out := new(CozystackResourceDefinition)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ApplicationDefinition) DeepCopyObject() runtime.Object {
func (in *CozystackResourceDefinition) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
@@ -55,22 +52,38 @@ func (in *ApplicationDefinition) DeepCopyObject() runtime.Object {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionApplication) DeepCopyInto(out *ApplicationDefinitionApplication) {
func (in *CozystackResourceDefinitionApplication) DeepCopyInto(out *CozystackResourceDefinitionApplication) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionApplication.
func (in *ApplicationDefinitionApplication) DeepCopy() *ApplicationDefinitionApplication {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionApplication.
func (in *CozystackResourceDefinitionApplication) DeepCopy() *CozystackResourceDefinitionApplication {
if in == nil {
return nil
}
out := new(ApplicationDefinitionApplication)
out := new(CozystackResourceDefinitionApplication)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionDashboard) DeepCopyInto(out *ApplicationDefinitionDashboard) {
func (in *CozystackResourceDefinitionChart) DeepCopyInto(out *CozystackResourceDefinitionChart) {
*out = *in
out.SourceRef = in.SourceRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionChart.
func (in *CozystackResourceDefinitionChart) DeepCopy() *CozystackResourceDefinitionChart {
if in == nil {
return nil
}
out := new(CozystackResourceDefinitionChart)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CozystackResourceDefinitionDashboard) DeepCopyInto(out *CozystackResourceDefinitionDashboard) {
*out = *in
if in.Tags != nil {
in, out := &in.Tags, &out.Tags
@@ -95,42 +108,42 @@ func (in *ApplicationDefinitionDashboard) DeepCopyInto(out *ApplicationDefinitio
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionDashboard.
func (in *ApplicationDefinitionDashboard) DeepCopy() *ApplicationDefinitionDashboard {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionDashboard.
func (in *CozystackResourceDefinitionDashboard) DeepCopy() *CozystackResourceDefinitionDashboard {
if in == nil {
return nil
}
out := new(ApplicationDefinitionDashboard)
out := new(CozystackResourceDefinitionDashboard)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionList) DeepCopyInto(out *ApplicationDefinitionList) {
func (in *CozystackResourceDefinitionList) DeepCopyInto(out *CozystackResourceDefinitionList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ApplicationDefinition, len(*in))
*out = make([]CozystackResourceDefinition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionList.
func (in *ApplicationDefinitionList) DeepCopy() *ApplicationDefinitionList {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionList.
func (in *CozystackResourceDefinitionList) DeepCopy() *CozystackResourceDefinitionList {
if in == nil {
return nil
}
out := new(ApplicationDefinitionList)
out := new(CozystackResourceDefinitionList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ApplicationDefinitionList) DeepCopyObject() runtime.Object {
func (in *CozystackResourceDefinitionList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
@@ -138,13 +151,9 @@ func (in *ApplicationDefinitionList) DeepCopyObject() runtime.Object {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionRelease) DeepCopyInto(out *ApplicationDefinitionRelease) {
func (in *CozystackResourceDefinitionRelease) DeepCopyInto(out *CozystackResourceDefinitionRelease) {
*out = *in
if in.ChartRef != nil {
in, out := &in.ChartRef, &out.ChartRef
*out = new(v2.CrossNamespaceSourceReference)
**out = **in
}
out.Chart = in.Chart
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
@@ -154,18 +163,18 @@ func (in *ApplicationDefinitionRelease) DeepCopyInto(out *ApplicationDefinitionR
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionRelease.
func (in *ApplicationDefinitionRelease) DeepCopy() *ApplicationDefinitionRelease {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionRelease.
func (in *CozystackResourceDefinitionRelease) DeepCopy() *CozystackResourceDefinitionRelease {
if in == nil {
return nil
}
out := new(ApplicationDefinitionRelease)
out := new(CozystackResourceDefinitionRelease)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionResourceSelector) DeepCopyInto(out *ApplicationDefinitionResourceSelector) {
func (in *CozystackResourceDefinitionResourceSelector) DeepCopyInto(out *CozystackResourceDefinitionResourceSelector) {
*out = *in
in.LabelSelector.DeepCopyInto(&out.LabelSelector)
if in.ResourceNames != nil {
@@ -175,55 +184,55 @@ func (in *ApplicationDefinitionResourceSelector) DeepCopyInto(out *ApplicationDe
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionResourceSelector.
func (in *ApplicationDefinitionResourceSelector) DeepCopy() *ApplicationDefinitionResourceSelector {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionResourceSelector.
func (in *CozystackResourceDefinitionResourceSelector) DeepCopy() *CozystackResourceDefinitionResourceSelector {
if in == nil {
return nil
}
out := new(ApplicationDefinitionResourceSelector)
out := new(CozystackResourceDefinitionResourceSelector)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionResources) DeepCopyInto(out *ApplicationDefinitionResources) {
func (in *CozystackResourceDefinitionResources) DeepCopyInto(out *CozystackResourceDefinitionResources) {
*out = *in
if in.Exclude != nil {
in, out := &in.Exclude, &out.Exclude
*out = make([]*ApplicationDefinitionResourceSelector, len(*in))
*out = make([]*CozystackResourceDefinitionResourceSelector, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(ApplicationDefinitionResourceSelector)
*out = new(CozystackResourceDefinitionResourceSelector)
(*in).DeepCopyInto(*out)
}
}
}
if in.Include != nil {
in, out := &in.Include, &out.Include
*out = make([]*ApplicationDefinitionResourceSelector, len(*in))
*out = make([]*CozystackResourceDefinitionResourceSelector, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(ApplicationDefinitionResourceSelector)
*out = new(CozystackResourceDefinitionResourceSelector)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionResources.
func (in *ApplicationDefinitionResources) DeepCopy() *ApplicationDefinitionResources {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionResources.
func (in *CozystackResourceDefinitionResources) DeepCopy() *CozystackResourceDefinitionResources {
if in == nil {
return nil
}
out := new(ApplicationDefinitionResources)
out := new(CozystackResourceDefinitionResources)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ApplicationDefinitionSpec) DeepCopyInto(out *ApplicationDefinitionSpec) {
func (in *CozystackResourceDefinitionSpec) DeepCopyInto(out *CozystackResourceDefinitionSpec) {
*out = *in
out.Application = in.Application
in.Release.DeepCopyInto(&out.Release)
@@ -232,360 +241,17 @@ func (in *ApplicationDefinitionSpec) DeepCopyInto(out *ApplicationDefinitionSpec
in.Ingresses.DeepCopyInto(&out.Ingresses)
if in.Dashboard != nil {
in, out := &in.Dashboard, &out.Dashboard
*out = new(ApplicationDefinitionDashboard)
*out = new(CozystackResourceDefinitionDashboard)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionSpec.
func (in *ApplicationDefinitionSpec) DeepCopy() *ApplicationDefinitionSpec {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionSpec.
func (in *CozystackResourceDefinitionSpec) DeepCopy() *CozystackResourceDefinitionSpec {
if in == nil {
return nil
}
out := new(ApplicationDefinitionSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Component) DeepCopyInto(out *Component) {
*out = *in
if in.Install != nil {
in, out := &in.Install, &out.Install
*out = new(ComponentInstall)
(*in).DeepCopyInto(*out)
}
if in.Libraries != nil {
in, out := &in.Libraries, &out.Libraries
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ValuesFiles != nil {
in, out := &in.ValuesFiles, &out.ValuesFiles
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Component.
func (in *Component) DeepCopy() *Component {
if in == nil {
return nil
}
out := new(Component)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ComponentInstall) DeepCopyInto(out *ComponentInstall) {
*out = *in
if in.DependsOn != nil {
in, out := &in.DependsOn, &out.DependsOn
*out = make([]string, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentInstall.
func (in *ComponentInstall) DeepCopy() *ComponentInstall {
if in == nil {
return nil
}
out := new(ComponentInstall)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DependencyStatus) DeepCopyInto(out *DependencyStatus) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyStatus.
func (in *DependencyStatus) DeepCopy() *DependencyStatus {
if in == nil {
return nil
}
out := new(DependencyStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Library) DeepCopyInto(out *Library) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Library.
func (in *Library) DeepCopy() *Library {
if in == nil {
return nil
}
out := new(Library)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Package) DeepCopyInto(out *Package) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Package.
func (in *Package) DeepCopy() *Package {
if in == nil {
return nil
}
out := new(Package)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Package) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageComponent) DeepCopyInto(out *PackageComponent) {
*out = *in
if in.Enabled != nil {
in, out := &in.Enabled, &out.Enabled
*out = new(bool)
**out = **in
}
if in.Values != nil {
in, out := &in.Values, &out.Values
*out = new(v1.JSON)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageComponent.
func (in *PackageComponent) DeepCopy() *PackageComponent {
if in == nil {
return nil
}
out := new(PackageComponent)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageList) DeepCopyInto(out *PackageList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Package, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageList.
func (in *PackageList) DeepCopy() *PackageList {
if in == nil {
return nil
}
out := new(PackageList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PackageList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageSource) DeepCopyInto(out *PackageSource) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSource.
func (in *PackageSource) DeepCopy() *PackageSource {
if in == nil {
return nil
}
out := new(PackageSource)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PackageSource) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageSourceList) DeepCopyInto(out *PackageSourceList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]PackageSource, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceList.
func (in *PackageSourceList) DeepCopy() *PackageSourceList {
if in == nil {
return nil
}
out := new(PackageSourceList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PackageSourceList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageSourceRef) DeepCopyInto(out *PackageSourceRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceRef.
func (in *PackageSourceRef) DeepCopy() *PackageSourceRef {
if in == nil {
return nil
}
out := new(PackageSourceRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageSourceSpec) DeepCopyInto(out *PackageSourceSpec) {
*out = *in
if in.SourceRef != nil {
in, out := &in.SourceRef, &out.SourceRef
*out = new(PackageSourceRef)
**out = **in
}
if in.Variants != nil {
in, out := &in.Variants, &out.Variants
*out = make([]Variant, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceSpec.
func (in *PackageSourceSpec) DeepCopy() *PackageSourceSpec {
if in == nil {
return nil
}
out := new(PackageSourceSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageSourceStatus) DeepCopyInto(out *PackageSourceStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceStatus.
func (in *PackageSourceStatus) DeepCopy() *PackageSourceStatus {
if in == nil {
return nil
}
out := new(PackageSourceStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageSpec) DeepCopyInto(out *PackageSpec) {
*out = *in
if in.IgnoreDependencies != nil {
in, out := &in.IgnoreDependencies, &out.IgnoreDependencies
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Components != nil {
in, out := &in.Components, &out.Components
*out = make(map[string]PackageComponent, len(*in))
for key, val := range *in {
(*out)[key] = *val.DeepCopy()
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSpec.
func (in *PackageSpec) DeepCopy() *PackageSpec {
if in == nil {
return nil
}
out := new(PackageSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PackageStatus) DeepCopyInto(out *PackageStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]metav1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Dependencies != nil {
in, out := &in.Dependencies, &out.Dependencies
*out = make(map[string]DependencyStatus, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageStatus.
func (in *PackageStatus) DeepCopy() *PackageStatus {
if in == nil {
return nil
}
out := new(PackageStatus)
out := new(CozystackResourceDefinitionSpec)
in.DeepCopyInto(out)
return out
}
@@ -612,33 +278,16 @@ func (in Selector) DeepCopy() Selector {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Variant) DeepCopyInto(out *Variant) {
func (in *SourceRef) DeepCopyInto(out *SourceRef) {
*out = *in
if in.DependsOn != nil {
in, out := &in.DependsOn, &out.DependsOn
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Libraries != nil {
in, out := &in.Libraries, &out.Libraries
*out = make([]Library, len(*in))
copy(*out, *in)
}
if in.Components != nil {
in, out := &in.Components, &out.Components
*out = make([]Component, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Variant.
func (in *Variant) DeepCopy() *Variant {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceRef.
func (in *SourceRef) DeepCopy() *SourceRef {
if in == nil {
return nil
}
out := new(Variant)
out := new(SourceRef)
in.DeepCopyInto(out)
return out
}

View File

@@ -29,8 +29,6 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
@@ -132,11 +130,6 @@ func main() {
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "core.backups.cozystack.io",
Cache: cache.Options{
ByObject: map[client.Object]cache.ByObject{
&backupsv1alpha1.BackupClass{}: {},
},
},
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly

View File

@@ -29,18 +29,14 @@ import (
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
"github.com/cozystack/cozystack/internal/backupcontroller"
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
// +kubebuilder:scaffold:imports
)
@@ -53,8 +49,6 @@ func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(backupsv1alpha1.AddToScheme(scheme))
utilruntime.Must(strategyv1alpha1.AddToScheme(scheme))
utilruntime.Must(velerov1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
@@ -136,11 +130,6 @@ func main() {
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "strategy.backups.cozystack.io",
Cache: cache.Options{
ByObject: map[client.Object]cache.ByObject{
&backupsv1alpha1.BackupClass{}: {},
},
},
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
@@ -158,21 +147,11 @@ func main() {
os.Exit(1)
}
if err = (&backupcontroller.BackupJobReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("backup-controller"),
if err = (&backupcontroller.BackupJobStrategyReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "BackupJob")
os.Exit(1)
}
if err = (&backupcontroller.RestoreJobReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("restore-controller"),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "RestoreJob")
setupLog.Error(err, "unable to create controller", "controller", "Job")
os.Exit(1)
}

View File

@@ -1,549 +0,0 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"bufio"
"context"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/spf13/cobra"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
_ "k8s.io/client-go/plugin/pkg/client/auth"
)
var addCmdFlags struct {
files []string
kubeconfig string
}
var addCmd = &cobra.Command{
Use: "add [package]...",
Short: "Install PackageSource and its dependencies interactively",
Long: `Install PackageSource and its dependencies interactively.
You can specify packages as arguments or use -f flag to read from files.
Multiple -f flags can be specified, and they can point to files or directories.`,
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Collect package names from arguments and files
packageNames := make(map[string]bool)
packagesFromFiles := make(map[string]string) // packageName -> filePath
for _, arg := range args {
packageNames[arg] = true
}
// Read packages from files
for _, filePath := range addCmdFlags.files {
packages, err := readPackagesFromFile(filePath)
if err != nil {
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
}
for _, pkg := range packages {
packageNames[pkg] = true
if oldPath, ok := packagesFromFiles[pkg]; ok {
fmt.Fprintf(os.Stderr, "warning: package %q is defined in both %s and %s, using the latter\n", pkg, oldPath, filePath)
}
packagesFromFiles[pkg] = filePath
}
}
if len(packageNames) == 0 {
return fmt.Errorf("no packages specified")
}
// Create Kubernetes client config
var config *rest.Config
var err error
if addCmdFlags.kubeconfig != "" {
config, err = clientcmd.BuildConfigFromFlags("", addCmdFlags.kubeconfig)
if err != nil {
return fmt.Errorf("failed to load kubeconfig from %s: %w", addCmdFlags.kubeconfig, err)
}
} else {
config, err = ctrl.GetConfig()
if err != nil {
return fmt.Errorf("failed to get kubeconfig: %w", err)
}
}
scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("failed to create k8s client: %w", err)
}
// Process each package
for packageName := range packageNames {
// Check if package comes from a file
if filePath, fromFile := packagesFromFiles[packageName]; fromFile {
// Try to create Package directly from file
if err := createPackageFromFile(ctx, k8sClient, filePath, packageName); err == nil {
fmt.Fprintf(os.Stderr, "✓ Added Package %s\n", packageName)
continue
}
// If failed, fall back to interactive installation
}
// Interactive installation from PackageSource
if err := installPackage(ctx, k8sClient, packageName); err != nil {
return err
}
}
return nil
},
}
func readPackagesFromFile(filePath string) ([]string, error) {
info, err := os.Stat(filePath)
if err != nil {
return nil, err
}
var packages []string
if info.IsDir() {
// Read all YAML files from directory
err := filepath.Walk(filePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() || !strings.HasSuffix(path, ".yaml") && !strings.HasSuffix(path, ".yml") {
return nil
}
pkgs, err := readPackagesFromYAMLFile(path)
if err != nil {
return fmt.Errorf("failed to read %s: %w", path, err)
}
packages = append(packages, pkgs...)
return nil
})
if err != nil {
return nil, err
}
} else {
packages, err = readPackagesFromYAMLFile(filePath)
if err != nil {
return nil, err
}
}
return packages, nil
}
func readPackagesFromYAMLFile(filePath string) ([]string, error) {
data, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
var packages []string
// Split YAML documents (in case of multiple resources)
documents := strings.Split(string(data), "---")
for _, doc := range documents {
doc = strings.TrimSpace(doc)
if doc == "" {
continue
}
// Parse using Kubernetes decoder
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
obj := &unstructured.Unstructured{}
_, _, err := decoder.Decode([]byte(doc), nil, obj)
if err != nil {
continue
}
// Check if it's a Package
if obj.GetKind() == "Package" {
name := obj.GetName()
if name != "" {
packages = append(packages, name)
}
continue
}
// Check if it's a PackageSource
if obj.GetKind() == "PackageSource" {
name := obj.GetName()
if name != "" {
packages = append(packages, name)
}
continue
}
// Try to parse as PackageList or PackageSourceList
if obj.GetKind() == "PackageList" || obj.GetKind() == "PackageSourceList" {
items, found, err := unstructured.NestedSlice(obj.Object, "items")
if err == nil && found {
for _, item := range items {
if itemMap, ok := item.(map[string]interface{}); ok {
if metadata, ok := itemMap["metadata"].(map[string]interface{}); ok {
if name, ok := metadata["name"].(string); ok && name != "" {
packages = append(packages, name)
}
}
}
}
}
continue
}
}
// Return empty list if no packages found - don't error out
// The check for whether any packages were specified at all is handled later in RunE
return packages, nil
}
// buildDependencyTree builds a dependency tree starting from the root PackageSource
// Returns both the dependency tree and a map of dependencies to their requesters
func buildDependencyTree(ctx context.Context, k8sClient client.Client, rootName string) (map[string][]string, map[string]string, error) {
tree := make(map[string][]string)
dependencyRequesters := make(map[string]string) // dep -> requester
visited := make(map[string]bool)
// Ensure root is in tree even if it has no dependencies
tree[rootName] = []string{}
var buildTree func(string) error
buildTree = func(pkgName string) error {
if visited[pkgName] {
return nil
}
visited[pkgName] = true
// Get PackageSource
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(ctx, client.ObjectKey{Name: pkgName}, ps); err != nil {
// If PackageSource doesn't exist, just skip it
return nil
}
// Collect all dependencies from all variants
deps := make(map[string]bool)
for _, variant := range ps.Spec.Variants {
for _, dep := range variant.DependsOn {
deps[dep] = true
}
}
// Add dependencies to tree
for dep := range deps {
if _, exists := tree[pkgName]; !exists {
tree[pkgName] = []string{}
}
tree[pkgName] = append(tree[pkgName], dep)
// Track who requested this dependency
dependencyRequesters[dep] = pkgName
// Recursively build tree for dependencies
if err := buildTree(dep); err != nil {
return err
}
}
return nil
}
if err := buildTree(rootName); err != nil {
return nil, nil, err
}
return tree, dependencyRequesters, nil
}
// topologicalSort performs topological sort on the dependency tree
// Returns order from root to leaves (dependencies first)
func topologicalSort(tree map[string][]string) ([]string, error) {
// Build reverse graph (dependencies -> dependents)
reverseGraph := make(map[string][]string)
allNodes := make(map[string]bool)
for node, deps := range tree {
allNodes[node] = true
for _, dep := range deps {
allNodes[dep] = true
reverseGraph[dep] = append(reverseGraph[dep], node)
}
}
// Calculate in-degrees (how many dependencies a node has)
inDegree := make(map[string]int)
for node := range allNodes {
inDegree[node] = 0
}
for node, deps := range tree {
inDegree[node] = len(deps)
}
// Kahn's algorithm - start with nodes that have no dependencies
var queue []string
for node, degree := range inDegree {
if degree == 0 {
queue = append(queue, node)
}
}
var result []string
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node)
// Process dependents (nodes that depend on this node)
for _, dependent := range reverseGraph[node] {
inDegree[dependent]--
if inDegree[dependent] == 0 {
queue = append(queue, dependent)
}
}
}
// Check for cycles
if len(result) != len(allNodes) {
return nil, fmt.Errorf("dependency cycle detected")
}
return result, nil
}
// createPackageFromFile creates a Package resource directly from a YAML file
func createPackageFromFile(ctx context.Context, k8sClient client.Client, filePath string, packageName string) error {
data, err := os.ReadFile(filePath)
if err != nil {
return err
}
// Split YAML documents
documents := strings.Split(string(data), "---")
for _, doc := range documents {
doc = strings.TrimSpace(doc)
if doc == "" {
continue
}
// Parse using Kubernetes decoder
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
obj := &unstructured.Unstructured{}
_, _, err := decoder.Decode([]byte(doc), nil, obj)
if err != nil {
continue
}
// Check if it's a Package with matching name
if obj.GetKind() == "Package" && obj.GetName() == packageName {
// Convert to Package
var pkg cozyv1alpha1.Package
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &pkg); err != nil {
return fmt.Errorf("failed to convert Package: %w", err)
}
// Create Package
if err := k8sClient.Create(ctx, &pkg); err != nil {
return fmt.Errorf("failed to create Package: %w", err)
}
return nil
}
}
return fmt.Errorf("Package %s not found in file", packageName)
}
func installPackage(ctx context.Context, k8sClient client.Client, packageSourceName string) error {
// Get PackageSource
packageSource := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(ctx, client.ObjectKey{Name: packageSourceName}, packageSource); err != nil {
return fmt.Errorf("failed to get PackageSource %s: %w", packageSourceName, err)
}
// Build dependency tree
dependencyTree, dependencyRequesters, err := buildDependencyTree(ctx, k8sClient, packageSourceName)
if err != nil {
return fmt.Errorf("failed to build dependency tree: %w", err)
}
// Topological sort (install from root to leaves)
installOrder, err := topologicalSort(dependencyTree)
if err != nil {
return fmt.Errorf("failed to sort dependencies: %w", err)
}
// Get all PackageSources for variant selection
var allPackageSources cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &allPackageSources); err != nil {
return fmt.Errorf("failed to list PackageSources: %w", err)
}
packageSourceMap := make(map[string]*cozyv1alpha1.PackageSource)
for i := range allPackageSources.Items {
packageSourceMap[allPackageSources.Items[i].Name] = &allPackageSources.Items[i]
}
// Get all installed Packages
var installedPackages cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &installedPackages); err != nil {
return fmt.Errorf("failed to list Packages: %w", err)
}
installedMap := make(map[string]*cozyv1alpha1.Package)
for i := range installedPackages.Items {
installedMap[installedPackages.Items[i].Name] = &installedPackages.Items[i]
}
// First, collect all variant selections
fmt.Fprintf(os.Stderr, "Installing %s and its dependencies...\n\n", packageSourceName)
packageVariants := make(map[string]string) // packageName -> variant
for _, pkgName := range installOrder {
// Check if already installed
if installed, exists := installedMap[pkgName]; exists {
variant := installed.Spec.Variant
if variant == "" {
variant = "default"
}
fmt.Fprintf(os.Stderr, "✓ %s (already installed, variant: %s)\n", pkgName, variant)
packageVariants[pkgName] = variant
continue
}
// Get PackageSource for this dependency
ps, exists := packageSourceMap[pkgName]
if !exists {
requester := dependencyRequesters[pkgName]
if requester != "" {
return fmt.Errorf("PackageSource %s not found (required by %s)", pkgName, requester)
}
return fmt.Errorf("PackageSource %s not found", pkgName)
}
// Select variant interactively
variant, err := selectVariantInteractive(ps)
if err != nil {
return fmt.Errorf("failed to select variant for %s: %w", pkgName, err)
}
packageVariants[pkgName] = variant
}
// Now create all Package resources
for _, pkgName := range installOrder {
// Skip if already installed
if _, exists := installedMap[pkgName]; exists {
continue
}
variant := packageVariants[pkgName]
// Create Package
pkg := &cozyv1alpha1.Package{
ObjectMeta: metav1.ObjectMeta{
Name: pkgName,
},
Spec: cozyv1alpha1.PackageSpec{
Variant: variant,
},
}
if err := k8sClient.Create(ctx, pkg); err != nil {
return fmt.Errorf("failed to create Package %s: %w", pkgName, err)
}
fmt.Fprintf(os.Stderr, "✓ Added Package %s\n", pkgName)
}
return nil
}
// selectVariantInteractive prompts user to select a variant
func selectVariantInteractive(ps *cozyv1alpha1.PackageSource) (string, error) {
if len(ps.Spec.Variants) == 0 {
return "", fmt.Errorf("no variants available for PackageSource %s", ps.Name)
}
reader := bufio.NewReader(os.Stdin)
fmt.Fprintf(os.Stderr, "\nPackageSource: %s\n", ps.Name)
fmt.Fprintf(os.Stderr, "Available variants:\n")
for i, variant := range ps.Spec.Variants {
fmt.Fprintf(os.Stderr, " %d. %s\n", i+1, variant.Name)
}
// If only one variant, use it as default
defaultVariant := ps.Spec.Variants[0].Name
var prompt string
if len(ps.Spec.Variants) == 1 {
prompt = "Select variant [1]: "
} else {
prompt = fmt.Sprintf("Select variant (1-%d): ", len(ps.Spec.Variants))
}
for {
fmt.Fprintf(os.Stderr, prompt)
input, err := reader.ReadString('\n')
if err != nil {
return "", fmt.Errorf("failed to read input: %w", err)
}
input = strings.TrimSpace(input)
// If input is empty and there's a default variant, use it
if input == "" && len(ps.Spec.Variants) == 1 {
return defaultVariant, nil
}
choice, err := strconv.Atoi(input)
if err != nil || choice < 1 || choice > len(ps.Spec.Variants) {
fmt.Fprintf(os.Stderr, "Invalid choice. Please enter a number between 1 and %d.\n", len(ps.Spec.Variants))
continue
}
return ps.Spec.Variants[choice-1].Name, nil
}
}
func init() {
rootCmd.AddCommand(addCmd)
addCmd.Flags().StringArrayVarP(&addCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
addCmd.Flags().StringVar(&addCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
}

View File

@@ -1,396 +0,0 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
_ "k8s.io/client-go/plugin/pkg/client/auth"
)
var delCmdFlags struct {
files []string
kubeconfig string
}
var delCmd = &cobra.Command{
Use: "del [package]...",
Short: "Delete Package resources",
Long: `Delete Package resources.
You can specify packages as arguments or use -f flag to read from files.
Multiple -f flags can be specified, and they can point to files or directories.`,
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Collect package names from arguments and files
packageNames := make(map[string]bool)
packagesFromFiles := make(map[string]string) // packageName -> filePath
for _, arg := range args {
packageNames[arg] = true
}
// Read packages from files (reuse function from add.go)
for _, filePath := range delCmdFlags.files {
packages, err := readPackagesFromFile(filePath)
if err != nil {
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
}
for _, pkg := range packages {
packageNames[pkg] = true
if oldPath, ok := packagesFromFiles[pkg]; ok {
fmt.Fprintf(os.Stderr, "warning: package %q is defined in both %s and %s, using the latter\n", pkg, oldPath, filePath)
}
packagesFromFiles[pkg] = filePath
}
}
if len(packageNames) == 0 {
return fmt.Errorf("no packages specified")
}
// Create Kubernetes client config
var config *rest.Config
var err error
if delCmdFlags.kubeconfig != "" {
config, err = clientcmd.BuildConfigFromFlags("", delCmdFlags.kubeconfig)
if err != nil {
return fmt.Errorf("failed to load kubeconfig from %s: %w", delCmdFlags.kubeconfig, err)
}
} else {
config, err = ctrl.GetConfig()
if err != nil {
return fmt.Errorf("failed to get kubeconfig: %w", err)
}
}
scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("failed to create k8s client: %w", err)
}
// Check which requested packages are installed
var installedPackages cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &installedPackages); err != nil {
return fmt.Errorf("failed to list Packages: %w", err)
}
installedMap := make(map[string]bool)
for _, pkg := range installedPackages.Items {
installedMap[pkg.Name] = true
}
// Warn about requested packages that are not installed
for pkgName := range packageNames {
if !installedMap[pkgName] {
fmt.Fprintf(os.Stderr, "⚠ Package %s is not installed, skipping\n", pkgName)
}
}
// Find all packages to delete (including dependents)
packagesToDelete, err := findPackagesToDelete(ctx, k8sClient, packageNames)
if err != nil {
return fmt.Errorf("failed to analyze dependencies: %w", err)
}
if len(packagesToDelete) == 0 {
fmt.Fprintf(os.Stderr, "No packages found to delete\n")
return nil
}
// Show packages to be deleted and ask for confirmation
if err := confirmDeletion(packagesToDelete, packageNames); err != nil {
return err
}
// Delete packages in reverse topological order (dependents first, then dependencies)
deleteOrder, err := getDeleteOrder(ctx, k8sClient, packagesToDelete)
if err != nil {
return fmt.Errorf("failed to determine delete order: %w", err)
}
// Delete each package
for _, packageName := range deleteOrder {
pkg := &cozyv1alpha1.Package{}
pkg.Name = packageName
if err := k8sClient.Delete(ctx, pkg); err != nil {
if apierrors.IsNotFound(err) {
fmt.Fprintf(os.Stderr, "⚠ Package %s not found, skipping\n", packageName)
continue
}
return fmt.Errorf("failed to delete Package %s: %w", packageName, err)
}
fmt.Fprintf(os.Stderr, "✓ Deleted Package %s\n", packageName)
}
return nil
},
}
// findPackagesToDelete finds all packages that need to be deleted, including dependents
func findPackagesToDelete(ctx context.Context, k8sClient client.Client, requestedPackages map[string]bool) (map[string]bool, error) {
// Get all installed Packages
var installedPackages cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &installedPackages); err != nil {
return nil, fmt.Errorf("failed to list Packages: %w", err)
}
installedMap := make(map[string]bool)
for _, pkg := range installedPackages.Items {
installedMap[pkg.Name] = true
}
// Get all PackageSources to build dependency graph
var packageSources cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &packageSources); err != nil {
return nil, fmt.Errorf("failed to list PackageSources: %w", err)
}
// Build reverse dependency graph (dependents -> dependencies)
// This tells us: for each package, which packages depend on it
reverseDeps := make(map[string][]string)
for _, ps := range packageSources.Items {
// Only consider installed packages
if !installedMap[ps.Name] {
continue
}
for _, variant := range ps.Spec.Variants {
for _, dep := range variant.DependsOn {
// Only consider installed dependencies
if installedMap[dep] {
reverseDeps[dep] = append(reverseDeps[dep], ps.Name)
}
}
}
}
// Find all packages to delete (requested + their dependents)
packagesToDelete := make(map[string]bool)
visited := make(map[string]bool)
var findDependents func(string)
findDependents = func(pkgName string) {
if visited[pkgName] {
return
}
visited[pkgName] = true
// Only add if it's installed
if installedMap[pkgName] {
packagesToDelete[pkgName] = true
}
// Recursively find all dependents
for _, dependent := range reverseDeps[pkgName] {
if installedMap[dependent] {
findDependents(dependent)
}
}
}
// Start from requested packages
for pkgName := range requestedPackages {
if !installedMap[pkgName] {
continue
}
findDependents(pkgName)
}
return packagesToDelete, nil
}
// confirmDeletion shows the list of packages to be deleted and asks for user confirmation
func confirmDeletion(packagesToDelete map[string]bool, requestedPackages map[string]bool) error {
// Separate requested packages from dependents
var requested []string
var dependents []string
for pkg := range packagesToDelete {
if requestedPackages[pkg] {
requested = append(requested, pkg)
} else {
dependents = append(dependents, pkg)
}
}
fmt.Fprintf(os.Stderr, "\nThe following packages will be deleted:\n\n")
if len(requested) > 0 {
fmt.Fprintf(os.Stderr, "Requested packages:\n")
for _, pkg := range requested {
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
}
fmt.Fprintf(os.Stderr, "\n")
}
if len(dependents) > 0 {
fmt.Fprintf(os.Stderr, "Dependent packages (will also be deleted):\n")
for _, pkg := range dependents {
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
}
fmt.Fprintf(os.Stderr, "\n")
}
fmt.Fprintf(os.Stderr, "Total: %d package(s)\n\n", len(packagesToDelete))
fmt.Fprintf(os.Stderr, "Do you want to continue? [y/N]: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
return fmt.Errorf("failed to read input: %w", err)
}
input = strings.TrimSpace(strings.ToLower(input))
if input != "y" && input != "yes" {
return fmt.Errorf("deletion cancelled")
}
return nil
}
// getDeleteOrder returns packages in reverse topological order (dependents first, then dependencies)
// This ensures we delete dependents before their dependencies
func getDeleteOrder(ctx context.Context, k8sClient client.Client, packagesToDelete map[string]bool) ([]string, error) {
// Get all PackageSources to build dependency graph
var packageSources cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &packageSources); err != nil {
return nil, fmt.Errorf("failed to list PackageSources: %w", err)
}
// Build forward dependency graph (package -> dependencies)
dependencyGraph := make(map[string][]string)
for _, ps := range packageSources.Items {
if !packagesToDelete[ps.Name] {
continue
}
deps := make(map[string]bool)
for _, variant := range ps.Spec.Variants {
for _, dep := range variant.DependsOn {
if packagesToDelete[dep] {
deps[dep] = true
}
}
}
var depList []string
for dep := range deps {
depList = append(depList, dep)
}
dependencyGraph[ps.Name] = depList
}
// Build reverse graph for topological sort
reverseGraph := make(map[string][]string)
allNodes := make(map[string]bool)
for node, deps := range dependencyGraph {
allNodes[node] = true
for _, dep := range deps {
allNodes[dep] = true
reverseGraph[dep] = append(reverseGraph[dep], node)
}
}
// Add nodes that have no dependencies
for pkg := range packagesToDelete {
if !allNodes[pkg] {
allNodes[pkg] = true
dependencyGraph[pkg] = []string{}
}
}
// Calculate in-degrees
inDegree := make(map[string]int)
for node := range allNodes {
inDegree[node] = 0
}
for node, deps := range dependencyGraph {
inDegree[node] = len(deps)
}
// Kahn's algorithm - start with nodes that have no dependencies
var queue []string
for node, degree := range inDegree {
if degree == 0 {
queue = append(queue, node)
}
}
var result []string
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node)
// Process dependents
for _, dependent := range reverseGraph[node] {
inDegree[dependent]--
if inDegree[dependent] == 0 {
queue = append(queue, dependent)
}
}
}
// Check for cycles: if not all nodes were processed, there's a cycle
if len(result) != len(allNodes) {
// Find unprocessed nodes
processed := make(map[string]bool)
for _, node := range result {
processed[node] = true
}
var unprocessed []string
for node := range allNodes {
if !processed[node] {
unprocessed = append(unprocessed, node)
}
}
return nil, fmt.Errorf("dependency cycle detected: the following packages form a cycle and cannot be deleted: %v", unprocessed)
}
// Reverse the result to get dependents first, then dependencies
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return result, nil
}
func init() {
rootCmd.AddCommand(delCmd)
delCmd.Flags().StringArrayVarP(&delCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
delCmd.Flags().StringVar(&delCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
}

View File

@@ -1,564 +0,0 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"context"
"fmt"
"os"
"strings"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"github.com/emicklei/dot"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var dotCmdFlags struct {
installed bool
components bool
files []string
kubeconfig string
}
var dotCmd = &cobra.Command{
Use: "dot [package]...",
Short: "Generate dependency graph as graphviz DOT format",
Long: `Generate dependency graph as graphviz DOT format.
Pipe the output through the "dot" program (part of graphviz package) to render the graph:
cozypkg dot | dot -Tpng > graph.png
By default, shows dependencies for all PackageSource resources.
Use --installed to show only installed Package resources.
Specify packages as arguments or use -f flag to read from files.`,
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Collect package names from arguments and files
packageNames := make(map[string]bool)
for _, arg := range args {
packageNames[arg] = true
}
// Read packages from files (reuse function from add.go)
for _, filePath := range dotCmdFlags.files {
packages, err := readPackagesFromFile(filePath)
if err != nil {
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
}
for _, pkg := range packages {
packageNames[pkg] = true
}
}
// Convert to slice, empty means all packages
var selectedPackages []string
if len(packageNames) > 0 {
for pkg := range packageNames {
selectedPackages = append(selectedPackages, pkg)
}
}
// If multiple packages specified, show graph for all of them
// If single package, use packageName for backward compatibility
var packageName string
if len(selectedPackages) == 1 {
packageName = selectedPackages[0]
} else if len(selectedPackages) > 1 {
// Multiple packages - pass empty string to packageName, use selectedPackages
packageName = ""
}
// packagesOnly is inverse of components flag (if components=false, then packagesOnly=true)
packagesOnly := !dotCmdFlags.components
graph, allNodes, edgeVariants, packageNames, err := buildGraphFromCluster(ctx, dotCmdFlags.kubeconfig, packagesOnly, dotCmdFlags.installed, packageName, selectedPackages)
if err != nil {
return fmt.Errorf("error getting PackageSource dependencies: %w", err)
}
dotGraph := generateDOTGraph(graph, allNodes, packagesOnly, edgeVariants, packageNames)
dotGraph.Write(os.Stdout)
return nil
},
}
func init() {
rootCmd.AddCommand(dotCmd)
dotCmd.Flags().BoolVarP(&dotCmdFlags.installed, "installed", "i", false, "show dependencies only for installed Package resources")
dotCmd.Flags().BoolVar(&dotCmdFlags.components, "components", false, "show component-level dependencies")
dotCmd.Flags().StringArrayVarP(&dotCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
dotCmd.Flags().StringVar(&dotCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
}
var (
dependenciesScheme = runtime.NewScheme()
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(dependenciesScheme))
utilruntime.Must(cozyv1alpha1.AddToScheme(dependenciesScheme))
}
// buildGraphFromCluster builds a dependency graph from PackageSource resources in the cluster.
// Returns: graph, allNodes, edgeVariants (map[edgeKey]variants), packageNames, error
func buildGraphFromCluster(ctx context.Context, kubeconfig string, packagesOnly bool, installedOnly bool, packageName string, selectedPackages []string) (map[string][]string, map[string]bool, map[string][]string, map[string]bool, error) {
// Create Kubernetes client config
var config *rest.Config
var err error
if kubeconfig != "" {
// Load kubeconfig from explicit path
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to load kubeconfig from %s: %w", kubeconfig, err)
}
} else {
// Use default kubeconfig loading (from env var or ~/.kube/config)
config, err = ctrl.GetConfig()
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to get kubeconfig: %w", err)
}
}
k8sClient, err := client.New(config, client.Options{Scheme: dependenciesScheme})
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to create k8s client: %w", err)
}
// Get installed Packages if needed
installedPackages := make(map[string]bool)
if installedOnly {
var packageList cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &packageList); err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to list Packages: %w", err)
}
for _, pkg := range packageList.Items {
installedPackages[pkg.Name] = true
}
}
// List all PackageSource resources
var packageSourceList cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &packageSourceList); err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to list PackageSources: %w", err)
}
// Build map of existing packages and components
packageNames := make(map[string]bool)
allExistingComponents := make(map[string]bool) // "package.component" -> true
for _, ps := range packageSourceList.Items {
if ps.Name != "" {
packageNames[ps.Name] = true
for _, variant := range ps.Spec.Variants {
for _, component := range variant.Components {
if component.Install != nil {
componentFullName := fmt.Sprintf("%s.%s", ps.Name, component.Name)
allExistingComponents[componentFullName] = true
}
}
}
}
}
graph := make(map[string][]string)
allNodes := make(map[string]bool)
edgeVariants := make(map[string][]string) // key: "source->target", value: list of variant names
existingEdges := make(map[string]bool) // key: "source->target" to avoid duplicates
componentHasLocalDeps := make(map[string]bool) // componentName -> has local component dependencies
// Process each PackageSource
for _, ps := range packageSourceList.Items {
psName := ps.Name
if psName == "" {
continue
}
// Filter by package name if specified
if packageName != "" && psName != packageName {
continue
}
// Filter by selected packages if specified
if len(selectedPackages) > 0 {
found := false
for _, selected := range selectedPackages {
if psName == selected {
found = true
break
}
}
if !found {
continue
}
}
// Filter by installed packages if flag is set
if installedOnly && !installedPackages[psName] {
continue
}
allNodes[psName] = true
// Track package dependencies per variant
packageDepVariants := make(map[string]map[string]bool) // dep -> variant -> true
allVariantNames := make(map[string]bool)
for _, v := range ps.Spec.Variants {
allVariantNames[v.Name] = true
}
// Track component dependencies per variant
componentDepVariants := make(map[string]map[string]map[string]bool) // componentName -> dep -> variant -> true
componentVariants := make(map[string]map[string]bool) // componentName -> variant -> true
// Extract dependencies from variants
for _, variant := range ps.Spec.Variants {
// Variant-level dependencies (package-level)
for _, dep := range variant.DependsOn {
// If installedOnly is set, only include dependencies that are installed
if installedOnly && !installedPackages[dep] {
continue
}
// Track which variant this dependency comes from
if packageDepVariants[dep] == nil {
packageDepVariants[dep] = make(map[string]bool)
}
packageDepVariants[dep][variant.Name] = true
edgeKey := fmt.Sprintf("%s->%s", psName, dep)
if !existingEdges[edgeKey] {
graph[psName] = append(graph[psName], dep)
existingEdges[edgeKey] = true
}
// Add to allNodes only if package exists
if packageNames[dep] {
allNodes[dep] = true
}
// If package doesn't exist, don't add to allNodes - it will be shown as missing (red)
}
// Component-level dependencies
if !packagesOnly {
for _, component := range variant.Components {
// Skip components without install section
if component.Install == nil {
continue
}
componentName := fmt.Sprintf("%s.%s", psName, component.Name)
allNodes[componentName] = true
// Track which variants this component appears in
if componentVariants[componentName] == nil {
componentVariants[componentName] = make(map[string]bool)
}
componentVariants[componentName][variant.Name] = true
if component.Install != nil {
if componentDepVariants[componentName] == nil {
componentDepVariants[componentName] = make(map[string]map[string]bool)
}
for _, dep := range component.Install.DependsOn {
// Track which variant this dependency comes from
if componentDepVariants[componentName][dep] == nil {
componentDepVariants[componentName][dep] = make(map[string]bool)
}
componentDepVariants[componentName][dep][variant.Name] = true
// Check if it's a local component dependency or external
if strings.Contains(dep, ".") {
// External component dependency (package.component format)
// Mark that this component has local dependencies (for edge to package logic)
componentHasLocalDeps[componentName] = true
// Check if target component exists
if allExistingComponents[dep] {
// Component exists
edgeKey := fmt.Sprintf("%s->%s", componentName, dep)
if !existingEdges[edgeKey] {
graph[componentName] = append(graph[componentName], dep)
existingEdges[edgeKey] = true
}
allNodes[dep] = true
} else {
// Component doesn't exist - create missing component node
edgeKey := fmt.Sprintf("%s->%s", componentName, dep)
if !existingEdges[edgeKey] {
graph[componentName] = append(graph[componentName], dep)
existingEdges[edgeKey] = true
}
// Don't add to allNodes - will be shown as missing (red)
// Add edge from missing component to its package
parts := strings.SplitN(dep, ".", 2)
if len(parts) == 2 {
depPackageName := parts[0]
missingEdgeKey := fmt.Sprintf("%s->%s", dep, depPackageName)
if !existingEdges[missingEdgeKey] {
graph[dep] = append(graph[dep], depPackageName)
existingEdges[missingEdgeKey] = true
}
// Add package to allNodes only if it exists
if packageNames[depPackageName] {
allNodes[depPackageName] = true
}
// If package doesn't exist, it will be shown as missing (red)
}
}
} else {
// Local component dependency (same package)
// Mark that this component has local dependencies
componentHasLocalDeps[componentName] = true
localDep := fmt.Sprintf("%s.%s", psName, dep)
// Check if target component exists
if allExistingComponents[localDep] {
// Component exists
edgeKey := fmt.Sprintf("%s->%s", componentName, localDep)
if !existingEdges[edgeKey] {
graph[componentName] = append(graph[componentName], localDep)
existingEdges[edgeKey] = true
}
allNodes[localDep] = true
} else {
// Component doesn't exist - create missing component node
edgeKey := fmt.Sprintf("%s->%s", componentName, localDep)
if !existingEdges[edgeKey] {
graph[componentName] = append(graph[componentName], localDep)
existingEdges[edgeKey] = true
}
// Don't add to allNodes - will be shown as missing (red)
// Add edge from missing component to its package
missingEdgeKey := fmt.Sprintf("%s->%s", localDep, psName)
if !existingEdges[missingEdgeKey] {
graph[localDep] = append(graph[localDep], psName)
existingEdges[missingEdgeKey] = true
}
}
}
}
}
}
}
}
// Store variant information for package dependencies that are not in all variants
for dep, variants := range packageDepVariants {
if len(variants) < len(allVariantNames) {
var variantList []string
for v := range variants {
variantList = append(variantList, v)
}
edgeKey := fmt.Sprintf("%s->%s", psName, dep)
edgeVariants[edgeKey] = variantList
}
}
// Add component->package edges for components without local dependencies
if !packagesOnly {
for componentName := range componentVariants {
// Only add edge to package if component has no local component dependencies
if !componentHasLocalDeps[componentName] {
edgeKey := fmt.Sprintf("%s->%s", componentName, psName)
if !existingEdges[edgeKey] {
graph[componentName] = append(graph[componentName], psName)
existingEdges[edgeKey] = true
}
// If component is not in all variants, store variant info for component->package edge
componentAllVariants := componentVariants[componentName]
if len(componentAllVariants) < len(allVariantNames) {
var variantList []string
for v := range componentAllVariants {
variantList = append(variantList, v)
}
edgeVariants[edgeKey] = variantList
}
}
}
}
// Store variant information for component dependencies that are not in all variants
for componentName, deps := range componentDepVariants {
componentAllVariants := componentVariants[componentName]
for dep, variants := range deps {
if len(variants) < len(componentAllVariants) {
var variantList []string
for v := range variants {
variantList = append(variantList, v)
}
// Determine the actual target name
var targetName string
if strings.Contains(dep, ".") {
targetName = dep
} else {
targetName = fmt.Sprintf("%s.%s", psName, dep)
}
edgeKey := fmt.Sprintf("%s->%s", componentName, targetName)
edgeVariants[edgeKey] = variantList
}
}
}
}
return graph, allNodes, edgeVariants, packageNames, nil
}
// generateDOTGraph generates a DOT graph from the dependency graph.
func generateDOTGraph(graph map[string][]string, allNodes map[string]bool, packagesOnly bool, edgeVariants map[string][]string, packageNames map[string]bool) *dot.Graph {
g := dot.NewGraph(dot.Directed)
g.Attr("rankdir", "RL")
g.Attr("nodesep", "0.5")
g.Attr("ranksep", "1.0")
// Helper function to check if a node is a package
// A node is a package if:
// 1. It's directly in packageNames
// 2. It doesn't contain a dot (simple package name)
// 3. It contains a dot but the part before the first dot is a package name
isPackage := func(nodeName string) bool {
if packageNames[nodeName] {
return true
}
if !strings.Contains(nodeName, ".") {
return true
}
// If it contains a dot, check if the part before the first dot is a package
parts := strings.SplitN(nodeName, ".", 2)
if len(parts) > 0 {
return packageNames[parts[0]]
}
return false
}
// Add nodes
for node := range allNodes {
if packagesOnly && !isPackage(node) {
// Skip component nodes when packages-only is enabled
continue
}
n := g.Node(node)
// Style nodes based on type
if isPackage(node) {
// Package node
n.Attr("shape", "box")
n.Attr("style", "rounded,filled")
n.Attr("fillcolor", "lightblue")
n.Attr("label", node)
} else {
// Component node
n.Attr("shape", "box")
n.Attr("style", "rounded,filled")
n.Attr("fillcolor", "lightyellow")
// Extract component name (part after last dot)
parts := strings.Split(node, ".")
if len(parts) > 0 {
n.Attr("label", parts[len(parts)-1])
} else {
n.Attr("label", node)
}
}
}
// Add edges
for source, targets := range graph {
if packagesOnly && !isPackage(source) {
// Skip component edges when packages-only is enabled
continue
}
for _, target := range targets {
if packagesOnly && !isPackage(target) {
// Skip component edges when packages-only is enabled
continue
}
// Check if target exists
targetExists := allNodes[target]
// Determine edge type for coloring
sourceIsPackage := isPackage(source)
targetIsPackage := isPackage(target)
// Add edge
edge := g.Edge(g.Node(source), g.Node(target))
// Set edge color based on type (if target exists)
if targetExists {
if sourceIsPackage && targetIsPackage {
// Package -> Package: black (default)
edge.Attr("color", "black")
} else {
// Component -> Package or Component -> Component: green
edge.Attr("color", "green")
}
}
// If target doesn't exist, mark it as missing (red color)
if !targetExists {
edge.Attr("color", "red")
edge.Attr("style", "dashed")
// Also add the missing node with red color
missingNode := g.Node(target)
missingNode.Attr("shape", "box")
missingNode.Attr("style", "rounded,filled,dashed")
missingNode.Attr("fillcolor", "lightcoral")
// Determine label based on node type
if isPackage(target) {
// Package node
missingNode.Attr("label", target)
} else {
// Component node - extract component name
parts := strings.Split(target, ".")
if len(parts) > 0 {
missingNode.Attr("label", parts[len(parts)-1])
} else {
missingNode.Attr("label", target)
}
}
} else {
// Check if this edge has variant information (dependency not in all variants)
edgeKey := fmt.Sprintf("%s->%s", source, target)
if variants, hasVariants := edgeVariants[edgeKey]; hasVariants {
// Add label with variant names
edge.Attr("label", strings.Join(variants, ","))
}
}
}
}
return g
}

View File

@@ -1,220 +0,0 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"context"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
_ "k8s.io/client-go/plugin/pkg/client/auth"
)
var listCmdFlags struct {
installed bool
components bool
kubeconfig string
}
var listCmd = &cobra.Command{
Use: "list",
Short: "List PackageSource or Package resources",
Long: `List PackageSource or Package resources in table format.
By default, lists PackageSource resources. Use --installed flag to list installed Package resources.
Use --components flag to show components on separate lines.`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
// Create Kubernetes client config
var config *rest.Config
var err error
if listCmdFlags.kubeconfig != "" {
config, err = clientcmd.BuildConfigFromFlags("", listCmdFlags.kubeconfig)
if err != nil {
return fmt.Errorf("failed to load kubeconfig from %s: %w", listCmdFlags.kubeconfig, err)
}
} else {
config, err = ctrl.GetConfig()
if err != nil {
return fmt.Errorf("failed to get kubeconfig: %w", err)
}
}
scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("failed to create k8s client: %w", err)
}
if listCmdFlags.installed {
return listPackages(ctx, k8sClient, listCmdFlags.components)
}
return listPackageSources(ctx, k8sClient, listCmdFlags.components)
},
}
func listPackageSources(ctx context.Context, k8sClient client.Client, showComponents bool) error {
var psList cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &psList); err != nil {
return fmt.Errorf("failed to list PackageSources: %w", err)
}
// Use tabwriter for better column alignment
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
defer w.Flush()
// Print header
fmt.Fprintln(w, "NAME\tVARIANTS\tREADY\tSTATUS")
// Print rows
for _, ps := range psList.Items {
// Get variants
var variants []string
for _, variant := range ps.Spec.Variants {
variants = append(variants, variant.Name)
}
variantsStr := strings.Join(variants, ",")
if len(variantsStr) > 28 {
variantsStr = variantsStr[:25] + "..."
}
// Get Ready condition
ready := "Unknown"
status := ""
for _, condition := range ps.Status.Conditions {
if condition.Type == "Ready" {
ready = string(condition.Status)
status = condition.Message
if len(status) > 48 {
status = status[:45] + "..."
}
break
}
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", ps.Name, variantsStr, ready, status)
// Show components if requested
if showComponents {
for _, variant := range ps.Spec.Variants {
for _, component := range variant.Components {
fmt.Fprintf(w, " %s\t%s\t\t\n",
fmt.Sprintf("%s.%s", ps.Name, component.Name),
variant.Name)
}
}
}
}
return nil
}
func listPackages(ctx context.Context, k8sClient client.Client, showComponents bool) error {
var pkgList cozyv1alpha1.PackageList
if err := k8sClient.List(ctx, &pkgList); err != nil {
return fmt.Errorf("failed to list Packages: %w", err)
}
// Fetch all PackageSource resources once if components are requested
var psMap map[string]*cozyv1alpha1.PackageSource
if showComponents {
var psList cozyv1alpha1.PackageSourceList
if err := k8sClient.List(ctx, &psList); err != nil {
return fmt.Errorf("failed to list PackageSources: %w", err)
}
psMap = make(map[string]*cozyv1alpha1.PackageSource)
for i := range psList.Items {
psMap[psList.Items[i].Name] = &psList.Items[i]
}
}
// Use tabwriter for better column alignment
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
defer w.Flush()
// Print header
fmt.Fprintln(w, "NAME\tVARIANT\tREADY\tSTATUS")
// Print rows
for _, pkg := range pkgList.Items {
variant := pkg.Spec.Variant
if variant == "" {
variant = "default"
}
// Get Ready condition
ready := "Unknown"
status := ""
for _, condition := range pkg.Status.Conditions {
if condition.Type == "Ready" {
ready = string(condition.Status)
status = condition.Message
if len(status) > 48 {
status = status[:45] + "..."
}
break
}
}
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", pkg.Name, variant, ready, status)
// Show components if requested
if showComponents {
// Look up PackageSource from map instead of making API call
if ps, exists := psMap[pkg.Name]; exists {
// Find the variant
for _, v := range ps.Spec.Variants {
if v.Name == variant {
for _, component := range v.Components {
fmt.Fprintf(w, " %s\t%s\t\t\n",
fmt.Sprintf("%s.%s", pkg.Name, component.Name),
variant)
}
break
}
}
}
}
}
return nil
}
func init() {
rootCmd.AddCommand(listCmd)
listCmd.Flags().BoolVarP(&listCmdFlags.installed, "installed", "i", false, "list installed Package resources instead of PackageSource resources")
listCmd.Flags().BoolVar(&listCmdFlags.components, "components", false, "show components on separate lines")
listCmd.Flags().StringVar(&listCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
}

View File

@@ -1,52 +0,0 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
// Version is set at build time via -ldflags.
var Version = "dev"
// rootCmd represents the base command when called without any subcommands.
var rootCmd = &cobra.Command{
Use: "cozypkg",
Short: "A CLI for managing Cozystack packages",
Long: ``,
SilenceErrors: true,
SilenceUsage: true,
DisableAutoGenTag: true,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() error {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
return err
}
return nil
}
func init() {
rootCmd.Version = Version
}

View File

@@ -1,30 +0,0 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
"github.com/cozystack/cozystack/cmd/cozypkg/cmd"
)
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}

View File

@@ -0,0 +1,29 @@
package main
import (
"flag"
"log"
"net/http"
"path/filepath"
)
func main() {
addr := flag.String("address", ":8123", "Address to listen on")
dir := flag.String("dir", "/cozystack/assets", "Directory to serve files from")
flag.Parse()
absDir, err := filepath.Abs(*dir)
if err != nil {
log.Fatalf("Error getting absolute path for %s: %v", *dir, err)
}
fs := http.FileServer(http.Dir(absDir))
http.Handle("/", fs)
log.Printf("Server starting on %s, serving directory %s", *addr, absDir)
err = http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}

View File

@@ -68,6 +68,8 @@ func main() {
var disableTelemetry bool
var telemetryEndpoint string
var telemetryInterval string
var cozystackVersion string
var reconcileDeployment bool
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
@@ -85,6 +87,10 @@ func main() {
"Endpoint for sending telemetry data")
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
"Interval between telemetry data collection (e.g. 15m, 1h)")
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
"Version of Cozystack")
flag.BoolVar(&reconcileDeployment, "reconcile-deployment", false,
"If set, the Cozystack API server is assumed to run as a Deployment, else as a DaemonSet.")
opts := zap.Options{
Development: false,
}
@@ -100,9 +106,10 @@ func main() {
// Configure telemetry
telemetryConfig := telemetry.Config{
Disabled: disableTelemetry,
Endpoint: telemetryEndpoint,
Interval: interval,
Disabled: disableTelemetry,
Endpoint: telemetryEndpoint,
Interval: interval,
CozystackVersion: cozystackVersion,
}
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
@@ -193,19 +200,32 @@ func main() {
os.Exit(1)
}
if err = (&controller.ApplicationDefinitionReconciler{
if err = (&controller.TenantHelmReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ApplicationDefinitionReconciler")
setupLog.Error(err, "unable to create controller", "controller", "TenantHelmReconciler")
os.Exit(1)
}
if err = (&controller.ApplicationDefinitionHelmReconciler{
if err = (&controller.CozystackConfigReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ApplicationDefinitionHelmReconciler")
setupLog.Error(err, "unable to create controller", "controller", "CozystackConfigReconciler")
os.Exit(1)
}
cozyAPIKind := "DaemonSet"
if reconcileDeployment {
cozyAPIKind = "Deployment"
}
if err = (&controller.CozystackResourceDefinitionReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
CozystackAPIKind: cozyAPIKind,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionReconciler")
os.Exit(1)
}

View File

@@ -1,653 +0,0 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"flag"
"fmt"
"os"
"strings"
"time"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"github.com/cozystack/cozystack/internal/cozyvaluesreplicator"
"github.com/cozystack/cozystack/internal/crdinstall"
"github.com/cozystack/cozystack/internal/fluxinstall"
"github.com/cozystack/cozystack/internal/operator"
"github.com/cozystack/cozystack/internal/telemetry"
// +kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
utilruntime.Must(helmv2.AddToScheme(scheme))
utilruntime.Must(sourcev1.AddToScheme(scheme))
utilruntime.Must(sourcewatcherv1beta1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var installCRDs bool
var installFlux bool
var disableTelemetry bool
var telemetryEndpoint string
var telemetryInterval string
var cozyValuesSecretName string
var cozyValuesSecretNamespace string
var cozyValuesNamespaceSelector string
var platformSourceURL string
var platformSourceName string
var platformSourceRef string
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.BoolVar(&secureMetrics, "metrics-secure", false,
"If set the metrics endpoint is served securely")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
flag.BoolVar(&installCRDs, "install-crds", false, "Install Cozystack CRDs before starting reconcile loop")
flag.BoolVar(&installFlux, "install-flux", false, "Install Flux components before starting reconcile loop")
flag.BoolVar(&disableTelemetry, "disable-telemetry", false,
"Disable telemetry collection")
flag.StringVar(&telemetryEndpoint, "telemetry-endpoint", "https://telemetry.cozystack.io",
"Endpoint for sending telemetry data")
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
"Interval between telemetry data collection (e.g. 15m, 1h)")
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-platform", "Name for the generated platform source resource and PackageSource")
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
flag.StringVar(&cozyValuesSecretName, "cozy-values-secret-name", "cozystack-values", "The name of the secret containing cluster-wide configuration values.")
flag.StringVar(&cozyValuesSecretNamespace, "cozy-values-secret-namespace", "cozy-system", "The namespace of the secret containing cluster-wide configuration values.")
flag.StringVar(&cozyValuesNamespaceSelector, "cozy-values-namespace-selector", "cozystack.io/system=true", "The label selector for namespaces where the cluster-wide configuration values must be replicated.")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
config := ctrl.GetConfigOrDie()
// Create a direct client (without cache) for pre-start operations
directClient, err := client.New(config, client.Options{Scheme: scheme})
if err != nil {
setupLog.Error(err, "unable to create direct client")
os.Exit(1)
}
targetNSSelector, err := labels.Parse(cozyValuesNamespaceSelector)
if err != nil {
setupLog.Error(err, "could not parse namespace label selector")
os.Exit(1)
}
// Initialize the controller manager
mgr, err := ctrl.NewManager(config, ctrl.Options{
Scheme: scheme,
Cache: cache.Options{
ByObject: map[client.Object]cache.ByObject{
// Cache only Secrets named <secretName> (in any namespace)
&corev1.Secret{}: {
Field: fields.OneTermEqualSelector("metadata.name", cozyValuesSecretName),
},
// Cache only Namespaces that match a label selector
&corev1.Namespace{}: {
Label: targetNSSelector,
},
},
},
Metrics: metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
},
WebhookServer: webhook.NewServer(webhook.Options{
Port: 9443,
}),
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "cozystack-operator.cozystack.io",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, setting this significantly speeds up voluntary
// leader transitions as the new leader don't have to wait LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
// Set up signal handler early so install phases respect SIGTERM
mgrCtx := ctrl.SetupSignalHandler()
// Install Cozystack CRDs before starting reconcile loop
if installCRDs {
setupLog.Info("Installing Cozystack CRDs before starting reconcile loop")
installCtx, installCancel := context.WithTimeout(mgrCtx, 2*time.Minute)
defer installCancel()
if err := crdinstall.Install(installCtx, directClient, crdinstall.WriteEmbeddedManifests); err != nil {
setupLog.Error(err, "failed to install CRDs")
os.Exit(1)
}
setupLog.Info("CRD installation completed successfully")
}
// Install Flux before starting reconcile loop
if installFlux {
setupLog.Info("Installing Flux components before starting reconcile loop")
installCtx, installCancel := context.WithTimeout(mgrCtx, 5*time.Minute)
defer installCancel()
// Use direct client for pre-start operations (cache is not ready yet)
if err := fluxinstall.Install(installCtx, directClient, fluxinstall.WriteEmbeddedManifests); err != nil {
setupLog.Error(err, "failed to install Flux")
os.Exit(1)
}
setupLog.Info("Flux installation completed successfully")
}
// Generate and install platform source resource if specified
if platformSourceURL != "" {
setupLog.Info("Generating platform source resource", "url", platformSourceURL, "name", platformSourceName, "ref", platformSourceRef)
installCtx, installCancel := context.WithTimeout(mgrCtx, 2*time.Minute)
defer installCancel()
// Use direct client for pre-start operations (cache is not ready yet)
if err := installPlatformSourceResource(installCtx, directClient, platformSourceURL, platformSourceName, platformSourceRef); err != nil {
setupLog.Error(err, "failed to install platform source resource")
os.Exit(1)
} else {
setupLog.Info("Platform source resource installation completed successfully")
}
}
// Create platform PackageSource when CRDs are managed by the operator and
// a platform source URL is configured. Without a URL there is no Flux source
// resource to reference, so creating a PackageSource would leave a dangling SourceRef.
if installCRDs && platformSourceURL != "" {
sourceRefKind := "OCIRepository"
sourceType, _, err := parsePlatformSourceURL(platformSourceURL)
if err != nil {
setupLog.Error(err, "failed to parse platform source URL for PackageSource")
os.Exit(1)
}
if sourceType == "git" {
sourceRefKind = "GitRepository"
}
setupLog.Info("Creating platform PackageSource", "platformSourceName", platformSourceName)
psCtx, psCancel := context.WithTimeout(mgrCtx, 2*time.Minute)
defer psCancel()
if err := installPlatformPackageSource(psCtx, directClient, platformSourceName, sourceRefKind); err != nil {
setupLog.Error(err, "failed to create platform PackageSource")
os.Exit(1)
}
setupLog.Info("Platform PackageSource creation completed successfully")
}
// Setup PackageSource reconciler
if err := (&operator.PackageSourceReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "PackageSource")
os.Exit(1)
}
// Setup Package reconciler
if err := (&operator.PackageReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Package")
os.Exit(1)
}
// Setup CozyValuesReplicator reconciler
if err := (&cozyvaluesreplicator.SecretReplicatorReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
SourceNamespace: cozyValuesSecretNamespace,
SecretName: cozyValuesSecretName,
TargetNamespaceSelector: targetNSSelector,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CozyValuesReplicator")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
// Parse telemetry interval
interval, err := time.ParseDuration(telemetryInterval)
if err != nil {
setupLog.Error(err, "invalid telemetry interval")
os.Exit(1)
}
// Configure telemetry
telemetryConfig := telemetry.Config{
Disabled: disableTelemetry,
Endpoint: telemetryEndpoint,
Interval: interval,
}
// Initialize telemetry collector
// Use APIReader (non-cached) because the manager's cache is filtered
// and doesn't include resources needed for telemetry (e.g., kube-system namespace, nodes, etc.)
collector, err := telemetry.NewOperatorCollector(mgr.GetAPIReader(), &telemetryConfig, config)
if err != nil {
setupLog.V(1).Info("unable to create telemetry collector, telemetry will be disabled", "error", err)
}
if collector != nil {
if err := mgr.Add(collector); err != nil {
setupLog.V(1).Info("unable to set up telemetry collector, continuing without telemetry", "error", err)
}
}
setupLog.Info("Starting controller manager")
if err := mgr.Start(mgrCtx); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}
// installPlatformSourceResource generates and installs a Flux source resource (OCIRepository or GitRepository)
// based on the platform source URL
func installPlatformSourceResource(ctx context.Context, k8sClient client.Client, sourceURL, resourceName, refSpec string) error {
logger := log.FromContext(ctx)
// Parse the source URL to determine type
sourceType, repoURL, err := parsePlatformSourceURL(sourceURL)
if err != nil {
return fmt.Errorf("failed to parse platform source URL: %w", err)
}
// Parse reference specification
refMap, err := parseRefSpec(refSpec)
if err != nil {
return fmt.Errorf("failed to parse reference specification: %w", err)
}
var obj client.Object
switch sourceType {
case "oci":
obj, err = generateOCIRepository(resourceName, repoURL, refMap)
if err != nil {
return fmt.Errorf("failed to generate OCIRepository: %w", err)
}
case "git":
obj, err = generateGitRepository(resourceName, repoURL, refMap)
if err != nil {
return fmt.Errorf("failed to generate GitRepository: %w", err)
}
default:
return fmt.Errorf("unsupported source type: %s (expected oci:// or https://)", sourceType)
}
// Apply the resource (create or update)
logger.Info("Applying platform source resource",
"apiVersion", obj.GetObjectKind().GroupVersionKind().GroupVersion().String(),
"kind", obj.GetObjectKind().GroupVersionKind().Kind,
"name", obj.GetName(),
"namespace", obj.GetNamespace(),
)
existing := obj.DeepCopyObject().(client.Object)
key := client.ObjectKeyFromObject(obj)
err = k8sClient.Get(ctx, key, existing)
if err != nil {
if client.IgnoreNotFound(err) == nil {
// Resource doesn't exist, create it
if err := k8sClient.Create(ctx, obj); err != nil {
return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)
}
logger.Info("Created platform source resource", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
} else {
return fmt.Errorf("failed to check if resource exists: %w", err)
}
} else {
// Resource exists, update it
obj.SetResourceVersion(existing.GetResourceVersion())
if err := k8sClient.Update(ctx, obj); err != nil {
return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)
}
logger.Info("Updated platform source resource", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
}
return nil
}
// parsePlatformSourceURL parses the source URL and returns the source type and repository URL.
// Supports formats:
// - oci://registry.example.com/repo
// - https://github.com/user/repo
// - http://github.com/user/repo
// - ssh://git@github.com/user/repo
func parsePlatformSourceURL(sourceURL string) (sourceType, repoURL string, err error) {
sourceURL = strings.TrimSpace(sourceURL)
if strings.HasPrefix(sourceURL, "oci://") {
return "oci", sourceURL, nil
}
if strings.HasPrefix(sourceURL, "https://") || strings.HasPrefix(sourceURL, "http://") || strings.HasPrefix(sourceURL, "ssh://") {
return "git", sourceURL, nil
}
return "", "", fmt.Errorf("unsupported source URL scheme (expected oci://, https://, http://, or ssh://): %s", sourceURL)
}
// parseRefSpec parses a reference specification string in the format "key1=value1,key2=value2".
// Returns a map of key-value pairs.
func parseRefSpec(refSpec string) (map[string]string, error) {
result := make(map[string]string)
refSpec = strings.TrimSpace(refSpec)
if refSpec == "" {
return result, nil
}
pairs := strings.Split(refSpec, ",")
for _, pair := range pairs {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}
// Split on first '=' only to allow '=' in values (e.g., digest=sha256:...)
idx := strings.Index(pair, "=")
if idx == -1 {
return nil, fmt.Errorf("invalid reference specification format: %q (expected key=value)", pair)
}
key := strings.TrimSpace(pair[:idx])
value := strings.TrimSpace(pair[idx+1:])
if key == "" {
return nil, fmt.Errorf("empty key in reference specification: %q", pair)
}
if value == "" {
return nil, fmt.Errorf("empty value for key %q in reference specification", key)
}
result[key] = value
}
return result, nil
}
// Valid reference keys for OCI repositories
var validOCIRefKeys = map[string]bool{
"digest": true,
"semver": true,
"semverFilter": true,
"tag": true,
}
// Valid reference keys for Git repositories
var validGitRefKeys = map[string]bool{
"branch": true,
"tag": true,
"semver": true,
"name": true,
"commit": true,
}
// validateOCIRef validates reference keys for OCI repositories
func validateOCIRef(refMap map[string]string) error {
for key := range refMap {
if !validOCIRefKeys[key] {
return fmt.Errorf("invalid OCI reference key %q (valid keys: digest, semver, semverFilter, tag)", key)
}
}
// Validate digest format if provided
if digest, ok := refMap["digest"]; ok {
if !strings.HasPrefix(digest, "sha256:") {
return fmt.Errorf("digest must be in format 'sha256:<hash>', got: %s", digest)
}
}
return nil
}
// validateGitRef validates reference keys for Git repositories
func validateGitRef(refMap map[string]string) error {
for key := range refMap {
if !validGitRefKeys[key] {
return fmt.Errorf("invalid Git reference key %q (valid keys: branch, tag, semver, name, commit)", key)
}
}
// Validate commit format if provided (should be a hex string)
if commit, ok := refMap["commit"]; ok {
if len(commit) < 7 {
return fmt.Errorf("commit SHA should be at least 7 characters, got: %s", commit)
}
for _, c := range commit {
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
return fmt.Errorf("commit SHA should be a hexadecimal string, got: %s", commit)
}
}
}
return nil
}
// generateOCIRepository creates an OCIRepository resource
func generateOCIRepository(name, repoURL string, refMap map[string]string) (*sourcev1.OCIRepository, error) {
if err := validateOCIRef(refMap); err != nil {
return nil, err
}
obj := &sourcev1.OCIRepository{
TypeMeta: metav1.TypeMeta{
APIVersion: sourcev1.GroupVersion.String(),
Kind: sourcev1.OCIRepositoryKind,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "cozy-system",
},
Spec: sourcev1.OCIRepositorySpec{
URL: repoURL,
Interval: metav1.Duration{Duration: 5 * time.Minute},
},
}
// Set reference if any ref options are provided
if len(refMap) > 0 {
obj.Spec.Reference = &sourcev1.OCIRepositoryRef{
Digest: refMap["digest"],
SemVer: refMap["semver"],
SemverFilter: refMap["semverFilter"],
Tag: refMap["tag"],
}
}
return obj, nil
}
// generateGitRepository creates a GitRepository resource
func generateGitRepository(name, repoURL string, refMap map[string]string) (*sourcev1.GitRepository, error) {
if err := validateGitRef(refMap); err != nil {
return nil, err
}
obj := &sourcev1.GitRepository{
TypeMeta: metav1.TypeMeta{
APIVersion: sourcev1.GroupVersion.String(),
Kind: sourcev1.GitRepositoryKind,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "cozy-system",
},
Spec: sourcev1.GitRepositorySpec{
URL: repoURL,
Interval: metav1.Duration{Duration: 5 * time.Minute},
},
}
// Set reference if any ref options are provided
if len(refMap) > 0 {
obj.Spec.Reference = &sourcev1.GitRepositoryRef{
Branch: refMap["branch"],
Tag: refMap["tag"],
SemVer: refMap["semver"],
Name: refMap["name"],
Commit: refMap["commit"],
}
}
return obj, nil
}
// installPlatformPackageSource creates the platform PackageSource resource
// that references the Flux source resource (OCIRepository or GitRepository).
//
// The variant list is intentionally hardcoded here. These are platform-defined
// deployment profiles (not user-extensible), matching what was previously in
// the Helm template. Changes require a new operator build and release.
func installPlatformPackageSource(ctx context.Context, k8sClient client.Client, platformSourceName, sourceRefKind string) error {
logger := log.FromContext(ctx)
packageSourceName := "cozystack." + platformSourceName
ps := &cozyv1alpha1.PackageSource{
TypeMeta: metav1.TypeMeta{
APIVersion: cozyv1alpha1.GroupVersion.String(),
Kind: "PackageSource",
},
ObjectMeta: metav1.ObjectMeta{
Name: packageSourceName,
Annotations: map[string]string{
"operator.cozystack.io/skip-cozystack-values": "true",
},
},
Spec: cozyv1alpha1.PackageSourceSpec{
SourceRef: &cozyv1alpha1.PackageSourceRef{
Kind: sourceRefKind,
Name: platformSourceName,
Namespace: "cozy-system",
Path: "/",
},
},
}
variantData := []struct {
name string
valuesFiles []string
}{
{"default", []string{"values.yaml"}},
{"isp-full", []string{"values.yaml", "values-isp-full.yaml"}},
{"isp-hosted", []string{"values.yaml", "values-isp-hosted.yaml"}},
{"isp-full-generic", []string{"values.yaml", "values-isp-full-generic.yaml"}},
}
variants := make([]cozyv1alpha1.Variant, len(variantData))
for i, v := range variantData {
variants[i] = cozyv1alpha1.Variant{
Name: v.name,
Components: []cozyv1alpha1.Component{
{
Name: "platform",
Path: "core/platform",
Install: &cozyv1alpha1.ComponentInstall{
Namespace: "cozy-system",
ReleaseName: "cozystack-platform",
},
ValuesFiles: v.valuesFiles,
},
},
}
}
ps.Spec.Variants = variants
logger.Info("Applying platform PackageSource", "name", packageSourceName)
patchOptions := &client.PatchOptions{
FieldManager: "cozystack-operator",
Force: func() *bool { b := true; return &b }(),
}
if err := k8sClient.Patch(ctx, ps, client.Apply, patchOptions); err != nil {
return fmt.Errorf("failed to apply PackageSource %s: %w", packageSourceName, err)
}
logger.Info("Applied platform PackageSource", "name", packageSourceName)
return nil
}

View File

@@ -1,574 +0,0 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"testing"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func newTestScheme() *runtime.Scheme {
s := runtime.NewScheme()
_ = cozyv1alpha1.AddToScheme(s)
return s
}
func TestInstallPlatformPackageSource_Creates(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
// Verify name
if ps.Name != "cozystack.cozystack-platform" {
t.Errorf("expected name %q, got %q", "cozystack.cozystack-platform", ps.Name)
}
// Verify annotation
if ps.Annotations["operator.cozystack.io/skip-cozystack-values"] != "true" {
t.Errorf("expected skip-cozystack-values annotation to be 'true', got %q", ps.Annotations["operator.cozystack.io/skip-cozystack-values"])
}
// Verify sourceRef
if ps.Spec.SourceRef == nil {
t.Fatal("expected SourceRef to be set")
}
if ps.Spec.SourceRef.Kind != "OCIRepository" {
t.Errorf("expected sourceRef.kind %q, got %q", "OCIRepository", ps.Spec.SourceRef.Kind)
}
if ps.Spec.SourceRef.Name != "cozystack-platform" {
t.Errorf("expected sourceRef.name %q, got %q", "cozystack-platform", ps.Spec.SourceRef.Name)
}
if ps.Spec.SourceRef.Namespace != "cozy-system" {
t.Errorf("expected sourceRef.namespace %q, got %q", "cozy-system", ps.Spec.SourceRef.Namespace)
}
if ps.Spec.SourceRef.Path != "/" {
t.Errorf("expected sourceRef.path %q, got %q", "/", ps.Spec.SourceRef.Path)
}
// Verify variants
expectedVariants := []string{"default", "isp-full", "isp-hosted", "isp-full-generic"}
if len(ps.Spec.Variants) != len(expectedVariants) {
t.Fatalf("expected %d variants, got %d", len(expectedVariants), len(ps.Spec.Variants))
}
for i, name := range expectedVariants {
if ps.Spec.Variants[i].Name != name {
t.Errorf("expected variant[%d].name %q, got %q", i, name, ps.Spec.Variants[i].Name)
}
if len(ps.Spec.Variants[i].Components) != 1 {
t.Errorf("expected variant[%d] to have 1 component, got %d", i, len(ps.Spec.Variants[i].Components))
}
}
}
func TestInstallPlatformPackageSource_Updates(t *testing.T) {
s := newTestScheme()
existing := &cozyv1alpha1.PackageSource{
ObjectMeta: metav1.ObjectMeta{
Name: "cozystack.cozystack-platform",
ResourceVersion: "1",
Labels: map[string]string{
"custom-label": "should-be-preserved",
},
},
Spec: cozyv1alpha1.PackageSourceSpec{
SourceRef: &cozyv1alpha1.PackageSourceRef{
Kind: "OCIRepository",
Name: "old-name",
Namespace: "cozy-system",
},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(existing).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
// Verify sourceRef was updated
if ps.Spec.SourceRef.Name != "cozystack-platform" {
t.Errorf("expected updated sourceRef.name %q, got %q", "cozystack-platform", ps.Spec.SourceRef.Name)
}
// Verify all 4 variants are present after update
if len(ps.Spec.Variants) != 4 {
t.Errorf("expected 4 variants after update, got %d", len(ps.Spec.Variants))
}
// Verify that labels set by other controllers are preserved (SSA does not overwrite unmanaged fields)
if ps.Labels["custom-label"] != "should-be-preserved" {
t.Errorf("expected custom-label to be preserved, got %q", ps.Labels["custom-label"])
}
}
func TestParsePlatformSourceURL(t *testing.T) {
tests := []struct {
name string
url string
wantType string
wantURL string
wantErr bool
}{
{
name: "OCI URL",
url: "oci://ghcr.io/cozystack/cozystack/cozystack-packages",
wantType: "oci",
wantURL: "oci://ghcr.io/cozystack/cozystack/cozystack-packages",
},
{
name: "HTTPS URL",
url: "https://github.com/cozystack/cozystack",
wantType: "git",
wantURL: "https://github.com/cozystack/cozystack",
},
{
name: "SSH URL",
url: "ssh://git@github.com/cozystack/cozystack",
wantType: "git",
wantURL: "ssh://git@github.com/cozystack/cozystack",
},
{
name: "empty URL",
url: "",
wantErr: true,
},
{
name: "unsupported scheme",
url: "ftp://example.com/repo",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sourceType, repoURL, err := parsePlatformSourceURL(tt.url)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error for URL %q, got nil", tt.url)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if sourceType != tt.wantType {
t.Errorf("expected type %q, got %q", tt.wantType, sourceType)
}
if repoURL != tt.wantURL {
t.Errorf("expected URL %q, got %q", tt.wantURL, repoURL)
}
})
}
}
func TestInstallPlatformPackageSource_VariantValuesFiles(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "cozystack-platform", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.cozystack-platform"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
expectedValuesFiles := map[string][]string{
"default": {"values.yaml"},
"isp-full": {"values.yaml", "values-isp-full.yaml"},
"isp-hosted": {"values.yaml", "values-isp-hosted.yaml"},
"isp-full-generic": {"values.yaml", "values-isp-full-generic.yaml"},
}
for _, v := range ps.Spec.Variants {
expected, ok := expectedValuesFiles[v.Name]
if !ok {
t.Errorf("unexpected variant %q", v.Name)
continue
}
if len(v.Components) != 1 {
t.Errorf("variant %q: expected 1 component, got %d", v.Name, len(v.Components))
continue
}
comp := v.Components[0]
if comp.Name != "platform" {
t.Errorf("variant %q: expected component name %q, got %q", v.Name, "platform", comp.Name)
}
if comp.Path != "core/platform" {
t.Errorf("variant %q: expected component path %q, got %q", v.Name, "core/platform", comp.Path)
}
if comp.Install == nil {
t.Errorf("variant %q: expected Install to be set", v.Name)
} else {
if comp.Install.Namespace != "cozy-system" {
t.Errorf("variant %q: expected install namespace %q, got %q", v.Name, "cozy-system", comp.Install.Namespace)
}
if comp.Install.ReleaseName != "cozystack-platform" {
t.Errorf("variant %q: expected install releaseName %q, got %q", v.Name, "cozystack-platform", comp.Install.ReleaseName)
}
}
if len(comp.ValuesFiles) != len(expected) {
t.Errorf("variant %q: expected %d valuesFiles, got %d", v.Name, len(expected), len(comp.ValuesFiles))
continue
}
for i, f := range expected {
if comp.ValuesFiles[i] != f {
t.Errorf("variant %q: expected valuesFiles[%d] %q, got %q", v.Name, i, f, comp.ValuesFiles[i])
}
}
}
}
func TestInstallPlatformPackageSource_CustomName(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "custom-source", "OCIRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.custom-source"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
if ps.Name != "cozystack.custom-source" {
t.Errorf("expected name %q, got %q", "cozystack.custom-source", ps.Name)
}
if ps.Spec.SourceRef.Name != "custom-source" {
t.Errorf("expected sourceRef.name %q, got %q", "custom-source", ps.Spec.SourceRef.Name)
}
}
func TestInstallPlatformPackageSource_GitRepository(t *testing.T) {
s := newTestScheme()
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
err := installPlatformPackageSource(context.Background(), k8sClient, "my-source", "GitRepository")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ps := &cozyv1alpha1.PackageSource{}
if err := k8sClient.Get(context.Background(), client.ObjectKey{Name: "cozystack.my-source"}, ps); err != nil {
t.Fatalf("PackageSource not found: %v", err)
}
if ps.Spec.SourceRef.Kind != "GitRepository" {
t.Errorf("expected sourceRef.kind %q, got %q", "GitRepository", ps.Spec.SourceRef.Kind)
}
if ps.Spec.SourceRef.Name != "my-source" {
t.Errorf("expected sourceRef.name %q, got %q", "my-source", ps.Spec.SourceRef.Name)
}
}
func TestParseRefSpec(t *testing.T) {
tests := []struct {
name string
input string
want map[string]string
wantErr bool
}{
{
name: "empty string",
input: "",
want: map[string]string{},
},
{
name: "single key-value",
input: "tag=v1.0",
want: map[string]string{"tag": "v1.0"},
},
{
name: "multiple key-values",
input: "digest=sha256:abc123,tag=v1.0",
want: map[string]string{"digest": "sha256:abc123", "tag": "v1.0"},
},
{
name: "whitespace around pairs",
input: " tag=v1.0 , branch=main ",
want: map[string]string{"tag": "v1.0", "branch": "main"},
},
{
name: "equals sign in value",
input: "digest=sha256:abc=123",
want: map[string]string{"digest": "sha256:abc=123"},
},
{
name: "missing equals sign",
input: "tag",
wantErr: true,
},
{
name: "empty key",
input: "=value",
wantErr: true,
},
{
name: "empty value",
input: "tag=",
wantErr: true,
},
{
name: "trailing comma",
input: "tag=v1.0,",
want: map[string]string{"tag": "v1.0"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseRefSpec(tt.input)
if tt.wantErr {
if err == nil {
t.Fatalf("expected error for input %q, got nil", tt.input)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(got) != len(tt.want) {
t.Fatalf("expected %d entries, got %d: %v", len(tt.want), len(got), got)
}
for k, v := range tt.want {
if got[k] != v {
t.Errorf("expected %q=%q, got %q=%q", k, v, k, got[k])
}
}
})
}
}
func TestValidateOCIRef(t *testing.T) {
tests := []struct {
name string
refMap map[string]string
wantErr bool
}{
{
name: "valid tag",
refMap: map[string]string{"tag": "v1.0"},
},
{
name: "valid digest",
refMap: map[string]string{"digest": "sha256:abc123def456"},
},
{
name: "valid semver",
refMap: map[string]string{"semver": ">=1.0.0"},
},
{
name: "multiple valid keys",
refMap: map[string]string{"tag": "v1.0", "digest": "sha256:abc"},
},
{
name: "empty map",
refMap: map[string]string{},
},
{
name: "invalid key",
refMap: map[string]string{"branch": "main"},
wantErr: true,
},
{
name: "invalid digest format",
refMap: map[string]string{"digest": "md5:abc"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateOCIRef(tt.refMap)
if tt.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestValidateGitRef(t *testing.T) {
tests := []struct {
name string
refMap map[string]string
wantErr bool
}{
{
name: "valid branch",
refMap: map[string]string{"branch": "main"},
},
{
name: "valid commit",
refMap: map[string]string{"commit": "abc1234"},
},
{
name: "valid tag and branch",
refMap: map[string]string{"tag": "v1.0", "branch": "release"},
},
{
name: "empty map",
refMap: map[string]string{},
},
{
name: "invalid key",
refMap: map[string]string{"digest": "sha256:abc"},
wantErr: true,
},
{
name: "commit too short",
refMap: map[string]string{"commit": "abc"},
wantErr: true,
},
{
name: "commit not hex",
refMap: map[string]string{"commit": "zzzzzzz"},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateGitRef(tt.refMap)
if tt.wantErr && err == nil {
t.Fatal("expected error, got nil")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestGenerateOCIRepository(t *testing.T) {
refMap := map[string]string{"tag": "v1.0", "digest": "sha256:abc123"}
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", refMap)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Name != "my-repo" {
t.Errorf("expected name %q, got %q", "my-repo", obj.Name)
}
if obj.Namespace != "cozy-system" {
t.Errorf("expected namespace %q, got %q", "cozy-system", obj.Namespace)
}
if obj.Spec.URL != "oci://registry.example.com/repo" {
t.Errorf("expected URL %q, got %q", "oci://registry.example.com/repo", obj.Spec.URL)
}
if obj.Spec.Reference == nil {
t.Fatal("expected Reference to be set")
}
if obj.Spec.Reference.Tag != "v1.0" {
t.Errorf("expected tag %q, got %q", "v1.0", obj.Spec.Reference.Tag)
}
if obj.Spec.Reference.Digest != "sha256:abc123" {
t.Errorf("expected digest %q, got %q", "sha256:abc123", obj.Spec.Reference.Digest)
}
}
func TestGenerateOCIRepository_NoRef(t *testing.T) {
obj, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Spec.Reference != nil {
t.Error("expected Reference to be nil for empty refMap")
}
}
func TestGenerateOCIRepository_InvalidRef(t *testing.T) {
_, err := generateOCIRepository("my-repo", "oci://registry.example.com/repo", map[string]string{"branch": "main"})
if err == nil {
t.Fatal("expected error for invalid OCI ref key, got nil")
}
}
func TestGenerateGitRepository(t *testing.T) {
refMap := map[string]string{"branch": "main", "commit": "abc1234def5678"}
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", refMap)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Name != "my-repo" {
t.Errorf("expected name %q, got %q", "my-repo", obj.Name)
}
if obj.Namespace != "cozy-system" {
t.Errorf("expected namespace %q, got %q", "cozy-system", obj.Namespace)
}
if obj.Spec.URL != "https://github.com/user/repo" {
t.Errorf("expected URL %q, got %q", "https://github.com/user/repo", obj.Spec.URL)
}
if obj.Spec.Reference == nil {
t.Fatal("expected Reference to be set")
}
if obj.Spec.Reference.Branch != "main" {
t.Errorf("expected branch %q, got %q", "main", obj.Spec.Reference.Branch)
}
if obj.Spec.Reference.Commit != "abc1234def5678" {
t.Errorf("expected commit %q, got %q", "abc1234def5678", obj.Spec.Reference.Commit)
}
}
func TestGenerateGitRepository_NoRef(t *testing.T) {
obj, err := generateGitRepository("my-repo", "https://github.com/user/repo", map[string]string{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if obj.Spec.Reference != nil {
t.Error("expected Reference to be nil for empty refMap")
}
}
func TestGenerateGitRepository_InvalidRef(t *testing.T) {
_, err := generateGitRepository("my-repo", "https://github.com/user/repo", map[string]string{"digest": "sha256:abc"})
if err == nil {
t.Fatal("expected error for invalid Git ref key, got nil")
}
}

View File

@@ -1,151 +0,0 @@
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"crypto/tls"
"flag"
"os"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"github.com/cozystack/cozystack/internal/controller/fluxplunger"
// +kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(helmv2.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.BoolVar(&secureMetrics, "metrics-secure", true,
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics server")
opts := zap.Options{
Development: false,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
// Rapid Reset CVEs. For more information see:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8
disableHTTP2 := func(c *tls.Config) {
setupLog.Info("disabling http/2")
c.NextProtos = []string{"http/1.1"}
}
if !enableHTTP2 {
tlsOpts = append(tlsOpts, disableHTTP2)
}
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
// More info:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
TLSOpts: tlsOpts,
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "flux-plunger.cozystack.io",
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
// when the Manager ends. This requires the binary to immediately end when the
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
// speeds up voluntary leader transitions as the new leader don't have to wait
// LeaseDuration time first.
//
// In the default scaffold provided, the program ends immediately after
// the manager stops, so would be fine to enable this option. However,
// if you are doing or is intended to do any operation such as perform cleanups
// after the manager stops then its usage might be unsafe.
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
setupLog.Error(err, "unable to create manager")
os.Exit(1)
}
if err = (&fluxplunger.FluxPlunger{
Client: mgr.GetClient(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "FluxPlunger")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}

View File

@@ -1,602 +0,0 @@
{
"__inputs": [
{
"name": "DS_PROMETHEUS",
"label": "Prometheus",
"description": "",
"type": "datasource",
"pluginId": "prometheus",
"pluginName": "Prometheus"
}
],
"__elements": {},
"__requires": [
{
"type": "panel",
"id": "bargauge",
"name": "Bar gauge",
"version": ""
},
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "9.4.7"
},
{
"type": "datasource",
"id": "prometheus",
"name": "Prometheus",
"version": "1.0.0"
},
{
"type": "panel",
"id": "timeseries",
"name": "Time series",
"version": ""
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "",
"editable": true,
"fiscalYearStartMonth": 0,
"gnetId": 16612,
"graphTooltip": 0,
"id": null,
"links": [
{
"asDropdown": true,
"icon": "external link",
"includeVars": true,
"keepTime": true,
"tags": [
"cilium-overview"
],
"targetBlank": false,
"title": "Cilium Overviews",
"tooltip": "",
"type": "dashboards",
"url": ""
},
{
"asDropdown": true,
"icon": "external link",
"includeVars": false,
"keepTime": true,
"tags": [
"hubble"
],
"targetBlank": false,
"title": "Hubble",
"tooltip": "",
"type": "dashboards",
"url": ""
}
],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 2,
"panels": [],
"title": "DNS",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "normal"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 1
},
"id": 37,
"options": {
"legend": {
"calcs": [
"mean",
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(rate(hubble_dns_queries_total{cluster=~\"$cluster\", source_namespace=~\"$source_namespace\", destination_namespace=~\"$destination_namespace\"}[$__rate_interval])) by (source) > 0",
"legendFormat": "{{source}}",
"range": true,
"refId": "A"
}
],
"title": "DNS queries",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 1
},
"id": 41,
"options": {
"displayMode": "gradient",
"minVizHeight": 10,
"minVizWidth": 0,
"orientation": "horizontal",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showUnfilled": true
},
"pluginVersion": "9.4.7",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "topk(10, sum(rate(hubble_dns_queries_total{cluster=~\"$cluster\", source_namespace=~\"$source_namespace\", destination_namespace=~\"$destination_namespace\"}[$__rate_interval])*60) by (query))",
"legendFormat": "{{query}}",
"range": true,
"refId": "A"
}
],
"title": "Top 10 DNS queries",
"type": "bargauge"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "normal"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 10
},
"id": 39,
"options": {
"legend": {
"calcs": [
"mean",
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "round(sum(rate(hubble_dns_queries_total{cluster=~\"$cluster\", source_namespace=~\"$source_namespace\", destination_namespace=~\"$destination_namespace\"}[$__rate_interval])) by (source) - sum(label_replace(sum(rate(hubble_dns_responses_total{cluster=~\"$cluster\", source_namespace=~\"$destination_namespace\", destination_namespace=~\"$source_namespace\"}[$__rate_interval])) by (destination), \"source\", \"$1\", \"destination\", \"(.*)\")) without (destination), 0.001) > 0",
"legendFormat": "{{source}}",
"range": true,
"refId": "A"
}
],
"title": "Missing DNS responses",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "normal"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "reqps"
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 10
},
"id": 43,
"options": {
"legend": {
"calcs": [
"mean",
"lastNotNull"
],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"editorMode": "code",
"expr": "sum(rate(hubble_dns_responses_total{cluster=~\"$cluster\", source_namespace=~\"$destination_namespace\", destination_namespace=~\"$source_namespace\", rcode!=\"No Error\"}[$__rate_interval])) by (destination, rcode) > 0",
"legendFormat": "{{destination}}: {{rcode}}",
"range": true,
"refId": "A"
}
],
"title": "DNS errors",
"type": "timeseries"
}
],
"refresh": "",
"revision": 1,
"schemaVersion": 38,
"style": "dark",
"tags": [
"kubecon-demo"
],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "default",
"value": "default"
},
"hide": 0,
"includeAll": false,
"label": "Data Source",
"multi": false,
"name": "DS_PROMETHEUS",
"options": [],
"query": "prometheus",
"queryValue": "",
"refresh": 1,
"regex": "(?!grafanacloud-usage|grafanacloud-ml-metrics).+",
"skipUrlSync": false,
"type": "datasource"
},
{
"current": {},
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"definition": "label_values(cilium_version, cluster)",
"hide": 0,
"includeAll": true,
"multi": true,
"name": "cluster",
"options": [],
"query": {
"query": "label_values(cilium_version, cluster)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"allValue": ".*",
"current": {},
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"definition": "label_values(source_namespace)",
"hide": 0,
"includeAll": true,
"label": "Source Namespace",
"multi": true,
"name": "source_namespace",
"options": [],
"query": {
"query": "label_values(source_namespace)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
},
{
"allValue": ".*",
"current": {},
"datasource": {
"type": "prometheus",
"uid": "${DS_PROMETHEUS}"
},
"definition": "label_values(destination_namespace)",
"hide": 0,
"includeAll": true,
"label": "Destination Namespace",
"multi": true,
"name": "destination_namespace",
"options": [],
"query": {
"query": "label_values(destination_namespace)",
"refId": "StandardVariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"type": "query"
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "Hubble / DNS Overview (Namespace)",
"uid": "_f0DUpY4k",
"version": 26,
"weekStart": ""
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ When the user asks to generate a changelog, follow these steps in the specified
- [ ] Step 5: Get the list of commits for the release period
- [ ] Step 6: Check additional repositories (website is REQUIRED, optional repos if tags exist)
- [ ] **MANDATORY**: Check website repository for documentation changes WITH authors and PR links via GitHub CLI
- [ ] **MANDATORY**: Check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) for tags during release period
- [ ] **MANDATORY**: Check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy) for tags during release period
- [ ] **MANDATORY**: For ALL commits from additional repos, get GitHub username via CLI, prioritizing PR author over commit author.
- [ ] Step 7: Analyze commits (extract PR numbers, authors, user impact)
- [ ] **MANDATORY**: For EVERY PR in main repo, get PR author via `gh pr view <PR_NUMBER> --json author --jq .author.login` (do NOT skip this step)
@@ -146,7 +146,7 @@ Cozystack release may include changes from related repositories. Check and inclu
**Optional repositories (MUST check ALL of them for tags during release period):**
- [https://github.com/cozystack/talm](https://github.com/cozystack/talm)
- [https://github.com/cozystack/boot-to-talos](https://github.com/cozystack/boot-to-talos)
- [https://github.com/cozystack/cozyhr](https://github.com/cozystack/cozyhr)
- [https://github.com/cozystack/cozypkg](https://github.com/cozystack/cozypkg)
- [https://github.com/cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy)
**⚠️ IMPORTANT**: You MUST check ALL optional repositories for tags created during the release period. Do NOT skip this step even if you think there might not be any tags. Use the process below to verify.
@@ -195,7 +195,7 @@ Cozystack release may include changes from related repositories. Check and inclu
3. **For optional repositories, check if tags exist during release period:**
**⚠️ MANDATORY: You MUST check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy). Do NOT skip any repository!**
**⚠️ MANDATORY: You MUST check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy). Do NOT skip any repository!**
**Use the helper script:**
```bash
@@ -208,7 +208,7 @@ Cozystack release may include changes from related repositories. Check and inclu
```
The script will:
- Check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy)
- Check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy)
- Look for tags created during the release period
- Get commits between tags (if tags exist) or by date range (if no tags)
- Extract PR numbers from commit messages
@@ -569,7 +569,7 @@ Create a new changelog file in the format matching previous versions:
- [ ] Step 5 completed: **ALL commits included** (including merge commits and backports) - do not skip any commits
- [ ] Step 5 completed: **Backports identified and handled correctly** - original PR author used, both original and backport PR numbers included
- [ ] Step 6 completed: Website repository checked for documentation changes WITH authors and PR links via GitHub CLI
- [ ] Step 6 completed: **ALL** optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) checked for tags during release period
- [ ] Step 6 completed: **ALL** optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy) checked for tags during release period
- [ ] Step 6 completed: For ALL commits from additional repos, GitHub username obtained via GitHub CLI (not skipped). For commits with PR numbers, PR author used via `gh pr view` (not commit author)
- [ ] Step 7 completed: For EVERY PR in main repo (including backports), PR author obtained via `gh pr view <PR_NUMBER> --json author --jq .author.login` (not skipped or assumed). Commit author NOT used - always use PR author
- [ ] Step 7 completed: **Backports verified** - for each backport PR, original PR found and original PR author used in changelog
@@ -628,7 +628,7 @@ Save the changelog to file `docs/changelogs/v<version>.md` according to the vers
- **Additional repositories (Step 6) - MANDATORY**:
- **⚠️ CRITICAL**: Always check the **website** repository for documentation changes during the release period. This is a required step and MUST NOT be skipped.
- **⚠️ CRITICAL**: You MUST check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) for tags during the release period. Do NOT skip any repository even if you think there might not be tags.
- **⚠️ CRITICAL**: You MUST check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy) for tags during the release period. Do NOT skip any repository even if you think there might not be tags.
- **CRITICAL**: For ALL entries from additional repositories (website and optional), you MUST:
- **MANDATORY**: Extract PR number from commit message first
- **MANDATORY**: For commits with PR numbers, ALWAYS use `gh pr view <PR_NUMBER> --repo cozystack/<repo> --json author --jq .author.login` to get PR author (not commit author)
@@ -637,7 +637,7 @@ Save the changelog to file `docs/changelogs/v<version>.md` according to the vers
- **MANDATORY**: Do NOT use commit author for PRs - always use PR author
- Include PR link or commit hash reference
- Format: `* **[repo] Description**: details ([**@username**](https://github.com/username) in cozystack/repo#123)`
- For **optional repositories** (talm, boot-to-talos, cozyhr, cozy-proxy), you MUST check ALL of them for tags during the release period. Use the loop provided in Step 6 to check each repository systematically.
- For **optional repositories** (talm, boot-to-talos, cozypkg, cozy-proxy), you MUST check ALL of them for tags during the release period. Use the loop provided in Step 6 to check each repository systematically.
- When including changes from additional repositories, use the format: `[repo-name] Description` and link to the repository's PR/issue if available
- **Prefer PR numbers over commit hashes**: For commits from additional repositories, extract PR number from commit message using GitHub API. Use PR format (`cozystack/website#123`) instead of commit hash (`cozystack/website@abc1234`) when available
- **Never add entries without author and PR/commit reference**: Every entry from additional repositories must have both author and link

View File

@@ -27,7 +27,7 @@ git commit --signoff -m "[component] Brief description of changes"
**Component prefixes:**
- System: `[dashboard]`, `[platform]`, `[cilium]`, `[kube-ovn]`, `[linstor]`, `[fluxcd]`, `[cluster-api]`
- Apps: `[postgres]`, `[mariadb]`, `[redis]`, `[kafka]`, `[clickhouse]`, `[virtual-machine]`, `[kubernetes]`
- Apps: `[postgres]`, `[mysql]`, `[redis]`, `[kafka]`, `[clickhouse]`, `[virtual-machine]`, `[kubernetes]`
- Other: `[tests]`, `[ci]`, `[docs]`, `[maintenance]`
**Examples:**
@@ -155,91 +155,6 @@ git diff
The user will commit and push when ready.
## Code Review Comments
When asked to fix code review comments, **always work only with unresolved (open) comments**. Resolved comments should be ignored as they have already been addressed.
### Getting Unresolved Review Comments
Use GitHub GraphQL API to fetch only unresolved review comments from a pull request:
```bash
gh api graphql -F owner=cozystack -F repo=cozystack -F pr=<PR_NUMBER> -f query='
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
isResolved
comments(first: 100) {
nodes {
id
path
line
author { login }
bodyText
url
createdAt
}
}
}
}
}
}
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[]'
```
### Filtering for Unresolved Comments
The key filter is `select(.isResolved == false)` which ensures only unresolved review threads are processed. Each thread can contain multiple comments, but if the thread is resolved, all its comments should be ignored.
### Working with Review Comments
1. **Fetch unresolved comments** using the GraphQL query above
2. **Parse the results** to identify:
- File path (`path`)
- Line number (`line` or `originalLine`)
- Comment text (`bodyText`)
- Author (`author.login`)
3. **Address each unresolved comment** by:
- Locating the relevant code section
- Making the requested changes
- Ensuring the fix addresses the concern raised
4. **Do NOT process resolved comments** - they have already been handled
### Example: Compact List of Unresolved Comments
For a quick overview of unresolved comments:
```bash
gh api graphql -F owner=cozystack -F repo=cozystack -F pr=<PR_NUMBER> -f query='
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
reviewThreads(first: 100) {
nodes {
isResolved
comments(first: 100) {
nodes {
path
line
author { login }
bodyText
}
}
}
}
}
}
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[] | "\(.path):\(.line // "N/A") - \(.author.login): \(.bodyText[:150])"'
```
### Important Notes
- **REST API limitation**: The REST endpoint `/pulls/{pr}/reviews` returns review summaries, not individual review comments. Use GraphQL API for accessing `reviewThreads` with `isResolved` status.
- **Thread-based resolution**: Comments are organized in threads. If a thread is resolved (`isResolved: true`), ignore all comments in that thread.
- **Always filter**: Never process comments from resolved threads, even if they appear in the results.
### Example Workflow
```bash

View File

@@ -10,7 +10,7 @@ Cozystack is an open-source Kubernetes-based platform and framework for building
- **Multi-tenancy**: Full isolation and self-service for tenants
- **GitOps-driven**: FluxCD-based continuous delivery
- **Modular Architecture**: Extensible with custom packages and services
- **Developer Experience**: Simplified local development with cozyhr tool
- **Developer Experience**: Simplified local development with cozypkg tool
The platform exposes infrastructure services via the Kubernetes API with ready-made configs, built-in monitoring, and alerts.

View File

@@ -5,13 +5,10 @@ https://github.com/cozystack/cozystack/releases/tag/v0.36.2
## Features and Improvements
* [vm-disk] New SVG icon for VM disk application. (@kvaps and @kvapsova in https://github.com/cozystack/cozystack/pull/1435)
## Security
## Fixes
* [kubernetes] Pin CoreDNS image tag to v1.12.4 for consistent, reproducible deployments. (@kvaps in https://github.com/cozystack/cozystack/pull/1469)
* [dashboard] Fix FerretDB spec typo that prevented deploy/display in the web UI. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1440)
## Dependencies
## Development, Testing, and CI/CD

View File

@@ -1,16 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.37.10
-->
## Features and Improvements
* **[virtual-machine] Improve check for resizing job**: Improved storage resize logic to only expand persistent volume claims when storage is being increased, preventing unintended storage reduction operations. Added validation to accurately compare current and desired storage sizes before triggering resize operations ([**@kvaps**](https://github.com/kvaps) in #1688, #1702).
## Fixes
* **[dashboard] Fix CustomFormsOverride schema to nest properties under spec.properties**: Fixed the logic for generating CustomFormsOverride schema to properly nest properties under `spec.properties` instead of directly under `properties`, ensuring correct form schema generation in the dashboard ([**@kvaps**](https://github.com/kvaps) in #1692, #1699).
---
**Full Changelog**: [v0.37.9...v0.37.10](https://github.com/cozystack/cozystack/compare/v0.37.9...v0.37.10)

View File

@@ -1,14 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.38.3
-->
## Improvements
* **[core:installer] Address buildx warnings for installer image builds**: Aligns Dockerfile syntax casing to remove buildx warnings, keeping installer builds clean ([**@nbykov0**](https://github.com/nbykov0) in #1682).
* **[system:coredns] Align CoreDNS app labels with Talos defaults**: Matches CoreDNS labels to Talos conventions so services select pods consistently across platform and tenant clusters ([**@nbykov0**](https://github.com/nbykov0) in #1675).
* **[system:monitoring-agents] Rename CoreDNS metrics service to avoid conflicts**: Renames the metrics service so it no longer clashes with the CoreDNS service used for name resolution in tenant clusters ([**@nbykov0**](https://github.com/nbykov0) in #1676).
---
**Full Changelog**: [v0.38.2...v0.38.3](https://github.com/cozystack/cozystack/compare/v0.38.2...v0.38.3)

View File

@@ -1,18 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.38.4
-->
## Fixes
* **[linstor] Update piraeus-operator v2.10.2 to handle fsck checks reliably**: Upgrades LINSTOR CSI to avoid failed mounts when fsck sees mounted volumes, improving volume publish reliability ([**@kvaps**](https://github.com/kvaps) in #1689, #1697).
* **[dashboard] Nest CustomFormsOverride properties under spec.properties**: Fixes schema generation so custom form properties are placed under `spec.properties`, preventing mis-rendered or missing form fields ([**@kvaps**](https://github.com/kvaps) in #1692, #1700).
* **[virtual-machine] Guard PVC resize to only expand storage**: Ensures resize jobs run only when storage size increases, avoiding unintended shrink attempts during VM updates ([**@kvaps**](https://github.com/kvaps) in #1688, #1701).
## Documentation
* **[website] Clarify GPU check command**: Makes the kubectl command for validating GPU binding more explicit, including namespace context ([**@nbykov0**](https://github.com/nbykov0) in cozystack/website#379).
---
**Full Changelog**: [v0.38.3...v0.38.4](https://github.com/cozystack/cozystack/compare/v0.38.3...v0.38.4)

View File

@@ -1,18 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.38.5
-->
## Features and Improvements
* **[virtual-machine,vm-instance] Add nodeAffinity for Windows VMs based on scheduling config**: Added nodeAffinity configuration to virtual-machine and vm-instance charts to support dedicated nodes for Windows VMs. When `dedicatedNodesForWindowsVMs` is enabled in the `cozystack-scheduling` ConfigMap, Windows VMs are scheduled on nodes with label `scheduling.cozystack.io/vm-windows=true`, while non-Windows VMs prefer nodes without this label ([**@kvaps**](https://github.com/kvaps) in #1693, #1744).
* **[cilium] Enable automatic pod rollout on configmap updates**: Cilium and Cilium operator pods now automatically restart when the cilium-config ConfigMap is updated, ensuring configuration changes are applied immediately without manual intervention ([**@kvaps**](https://github.com/kvaps) in #1728, #1745).
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 with improved S3 daemon performance and fixes. This update includes better S3 compatibility and performance improvements ([**@kvaps**](https://github.com/kvaps) in #1725, #1732).
## Fixes
* **[apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK**: Refactored the apiserver REST handlers to use typed objects (`appsv1alpha1.Application`) instead of `unstructured.Unstructured`, eliminating the need for runtime conversions and simplifying the codebase. Additionally, fixed an issue where `UnstructuredList` objects were using the first registered kind from `typeToGVK` instead of the kind from the object's field when multiple kinds are registered with the same Go type. This fix includes the upstream fix from kubernetes/kubernetes#135537 ([**@kvaps**](https://github.com/kvaps) in #1679, #1709).
---
**Full Changelog**: [v0.38.4...v0.38.5](https://github.com/cozystack/cozystack/compare/v0.38.4...v0.38.5)

View File

@@ -1,12 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.38.6
-->
## Development, Testing, and CI/CD
* **[kubernetes] Add lb tests for tenant k8s**: Added load balancer tests for tenant Kubernetes clusters, improving test coverage and ensuring proper load balancer functionality in tenant environments ([**@IvanHunters**](https://github.com/IvanHunters) in #1783, #1792).
---
**Full Changelog**: [v0.38.5...v0.38.6](https://github.com/cozystack/cozystack/compare/v0.38.5...v0.38.6)

View File

@@ -1,13 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.38.7
-->
## Fixes
* **[kubevirt-operator] Fix typo in VMNotRunningFor10Minutes alert**: Fixed typo in VM alert name, ensuring proper alert triggering and monitoring for virtual machines that are not running for extended periods ([**@lexfrei**](https://github.com/lexfrei) in #1770).
* **[kubevirt-operator] Revert incorrect case change in VM alerts**: Reverted incorrect case change in VM alert names to maintain consistency with alert naming conventions ([**@lexfrei**](https://github.com/lexfrei) in #1804, #1805).
---
**Full Changelog**: [v0.38.6...v0.38.7](https://github.com/cozystack/cozystack/compare/v0.38.6...v0.38.7)

View File

@@ -1,12 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.38.8
-->
## Improvements
* **[multus] Remove memory limit**: Removed memory limit for Multus daemonset due to unpredictable memory consumption spikes during startup after node reboots (reported up to 3Gi). This temporary change prevents out-of-memory issues while the root cause is addressed in future releases ([**@nbykov0**](https://github.com/nbykov0) in #1834).
---
**Full Changelog**: [v0.38.7...v0.38.8](https://github.com/cozystack/cozystack/compare/v0.38.7...v0.38.8)

View File

@@ -1,95 +0,0 @@
# Cozystack v0.39 — "Enhanced Networking & Monitoring"
This release introduces topology-aware routing for Cilium services, automatic pod rollouts on configuration changes, improved monitoring capabilities, and numerous bug fixes and improvements across the platform.
## Highlights
* **Topology-Aware Routing**: Enabled topology-aware routing for Cilium services, improving traffic distribution and reducing latency by routing traffic to endpoints in the same zone when possible ([**@nbykov0**](https://github.com/nbykov0) in #1734).
* **Automatic Pod Rollouts**: Cilium and Cilium operator pods now automatically restart when configuration changes, ensuring configuration updates are applied immediately ([**@kvaps**](https://github.com/kvaps) in #1728).
* **Windows VM Scheduling**: Added nodeAffinity configuration for Windows VMs based on scheduling config, enabling dedicated nodes for Windows workloads ([**@kvaps**](https://github.com/kvaps) in #1693).
* **SeaweedFS Updates**: Updated to SeaweedFS v4.02 with improved S3 daemon performance and fixes ([**@kvaps**](https://github.com/kvaps) in #1725).
---
## Major Features and Improvements
### Networking
* **[system/cilium] Enable topology-aware routing for services**: Enabled topology-aware routing for services, improving traffic distribution and reducing latency by routing traffic to endpoints in the same zone when possible. This feature helps optimize network performance in multi-zone deployments ([**@nbykov0**](https://github.com/nbykov0) in #1734).
* **[cilium] Enable automatic pod rollout on configmap updates**: Cilium and Cilium operator pods now automatically restart when the cilium-config ConfigMap is updated, ensuring configuration changes are applied immediately without manual intervention ([**@kvaps**](https://github.com/kvaps) in #1728).
### Virtual Machines
* **[virtual-machine,vm-instance] Add nodeAffinity for Windows VMs based on scheduling config**: Added nodeAffinity configuration to virtual-machine and vm-instance charts to support dedicated nodes for Windows VMs. When `dedicatedNodesForWindowsVMs` is enabled in the `cozystack-scheduling` ConfigMap, Windows VMs are scheduled on nodes with label `scheduling.cozystack.io/vm-windows=true`, while non-Windows VMs prefer nodes without this label ([**@kvaps**](https://github.com/kvaps) in #1693).
### Storage
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 with improved performance for S3 daemon and fixes for known issues. This update includes better S3 compatibility and performance improvements ([**@kvaps**](https://github.com/kvaps) in #1725).
### Tools
* **[talm] feat(init)!: require --name flag for cluster name**: Breaking change: The `talm init` command now requires the `--name` flag to specify the cluster name. This ensures consistent cluster naming and prevents accidental initialization without a name ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#86).
* **[talm] feat(template): preserve extra YAML documents in output**: Templates now preserve extra YAML documents in the output, allowing for more flexible template processing ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#87).
* **[talm] feat: add directory expansion for -f flag**: Added directory expansion support for the `-f` flag, allowing users to specify directories instead of individual files ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@ca5713e).
* **[talm] Introduce automatic root detection**: Added automatic root detection logic to simplify talm usage and reduce manual configuration ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@d165162).
* **[talm] Introduce talm kubeconfig --login command**: Added new `talm kubeconfig --login` command for easier kubeconfig management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@5f7e05b).
* **[talm] Introduce encryption**: Added encryption support to talm for secure configuration management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#81).
* **[talm] Replace code-generation with wrapper on talosctl**: Refactored talm to use a wrapper on talosctl instead of code generation, simplifying the codebase and improving maintainability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#80).
* **[talm] Use go embed instead of code generation**: Migrated from code generation to go embed for better build performance and simpler dependency management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#79).
* **[boot-to-talos] Cozystack: Update Talos Linux v1.11.3**: Updated boot-to-talos to use Talos Linux v1.11.3 ([**@kvaps**](https://github.com/kvaps) in cozystack/boot-to-talos#7).
## Improvements
* **[seaweedfs] Extended CA certificate duration to reduce disruptive CA rotations**: Extended CA certificate duration to reduce disruptive CA rotations, improving long-term certificate management and reducing operational overhead ([**@IvanHunters**](https://github.com/IvanHunters) in #1657).
* **[dashboard] Add config hash annotations to restart pods on config changes**: Added config hash annotations to dashboard deployment templates to ensure pods are automatically restarted when their configuration changes, ensuring configuration updates are applied immediately ([**@kvaps**](https://github.com/kvaps) in #1662).
* **[tenant][kubernetes] Introduce better cleanup logic**: Improved cleanup logic for tenant Kubernetes resources, ensuring proper resource cleanup when tenants are deleted or updated. Added automated pre-delete cleanup job for tenant namespaces to remove tenant-related releases during uninstall ([**@kvaps**](https://github.com/kvaps) in #1661).
* **[system:coredns] update coredns app labels to match Talos coredns labels**: Updated coredns app labels to match Talos coredns labels, ensuring consistency across the platform ([**@nbykov0**](https://github.com/nbykov0) in #1675).
* **[system:monitoring-agents] rename coredns metrics service**: Renamed coredns metrics service to avoid interference with coredns service used for name resolution in tenant k8s clusters ([**@nbykov0**](https://github.com/nbykov0) in #1676).
* **[core:installer] Address buildx warnings**: Fixed Dockerfile syntax warnings from buildx, ensuring clean builds without warnings ([**@nbykov0**](https://github.com/nbykov0) in #1682).
* **[talm] Refactor root detection logic into single file**: Improved code organization by consolidating root detection logic into a single file ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@487b479).
* **[talm] Refactor init logic, better upgrade**: Improved initialization logic and upgrade process for better reliability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@c512777).
* **[talm] Sugar for kubeconfig command**: Added convenience features to the kubeconfig command for improved usability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@a4010b3).
* **[talm] wrap upgrade command**: Wrapped upgrade command for better integration and error handling ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@2e1afbf).
* **[talm] docs(readme): add Homebrew installation option**: Added Homebrew installation option to the README for easier installation on macOS ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm@12bd4f2).
* **[talm] cozystack: disable nodeCIDRs allocation**: Disabled nodeCIDRs allocation in talm for better network configuration control ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#82).
* **[talm] Update license to Apache2.0**: Updated license to Apache 2.0 for better compatibility and clarity ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@eda1032).
## Fixes
* **[apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK**: Refactored the apiserver REST handlers to use typed objects (`appsv1alpha1.Application`) instead of `unstructured.Unstructured`, eliminating the need for runtime conversions and simplifying the codebase. Additionally, fixed an issue where `UnstructuredList` objects were using the first registered kind from `typeToGVK` instead of the kind from the object's field when multiple kinds are registered with the same Go type. This fix includes the upstream fix from kubernetes/kubernetes#135537 ([**@kvaps**](https://github.com/kvaps) in #1679).
* **[dashboard] Fix CustomFormsOverride schema to nest properties under spec.properties**: Fixed the logic for generating CustomFormsOverride schema to properly nest properties under `spec.properties` instead of directly under `properties`, ensuring correct form schema generation ([**@kvaps**](https://github.com/kvaps) in #1692).
* **[virtual-machine] Improve check for resizing job**: Improved storage resize logic to only expand persistent volume claims when storage is being increased, preventing unintended storage reduction operations. Added validation to accurately compare current and desired storage sizes before triggering resize operations ([**@kvaps**](https://github.com/kvaps) in #1688).
* **[linstor] Update piraeus-operator v2.10.2**: Updated LINSTOR CSI to fix issues with the new fsck behaviour, resolving mount failures when fsck attempts to run on mounted devices ([**@kvaps**](https://github.com/kvaps) in #1689).
* **[api] Revert dynamic list kinds representation fix (fixes namespace deletion regression)**: Reverted changes from #1630 that caused a regression affecting namespace deletion and upgrades from previous versions. The regression caused namespace deletion failures with errors like "content is not a list: []unstructured.Unstructured" during namespace finalization. This revert restores compatibility with namespace deletion controller and fixes upgrade issues from previous versions ([**@kvaps**](https://github.com/kvaps) in #1677).
* **[talm] fix: normalize template paths for Windows compatibility**: Fixed template path handling to ensure Windows compatibility by normalizing paths ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#88).
## Dependencies
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 ([**@kvaps**](https://github.com/kvaps) in #1725).
* **[linstor] Update piraeus-operator v2.10.2**: Updated piraeus-operator to version 2.10.2 ([**@kvaps**](https://github.com/kvaps) in #1689).
* **[talm] Cozystack: Update Talos Linux v1.11.3**: Updated talm to use Talos Linux v1.11.3 ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#83).
## Documentation
* **[website] Add article: Talm v0.17: Built-in Age Encryption for Secrets Management**: Added comprehensive blog post announcing Talm v0.17 and its built-in age-based encryption for secrets. Covers initial setup and key generation, encryption/decryption workflows, idempotent encryption behavior, automatic .gitignore handling, file permission safeguards, security best practices, and guidance for GitOps and CI/CD integration ([**@kvaps**](https://github.com/kvaps) in [cozystack/website#384](https://github.com/cozystack/website/pull/384)).
* **[website] docs(talm): update talm init syntax for mandatory --preset and --name flags**: Updated documentation to reflect breaking changes in talm, adding mandatory `--preset` and `--name` flags to the talm init command ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#386).
---
## Contributors
We'd like to thank all contributors who made this release possible:
* [**@IvanHunters**](https://github.com/IvanHunters)
* [**@kvaps**](https://github.com/kvaps)
* [**@lexfrei**](https://github.com/lexfrei)
* [**@nbykov0**](https://github.com/nbykov0)
---
**Full Changelog**: [v0.38.0...v0.39.0](https://github.com/cozystack/cozystack/compare/v0.38.0...v0.39.0)
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.39.0
-->

View File

@@ -1,12 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.39.1
-->
## Features and Improvements
* **[monitoring] Add SLACK_SEVERITY_FILTER field and VMAgent for tenant monitoring**: Introduced the SLACK_SEVERITY_FILTER environment variable in the Alerta deployment to enable filtering of alert severities for Slack notifications based on the disabledSeverity configuration. Additionally, added a VMAgent resource template for scraping metrics within tenant namespaces, improving monitoring granularity and control. This enhancement allows administrators to configure which alert severities are sent to Slack and enables tenant-specific metrics collection for better observability ([**@IvanHunters**](https://github.com/IvanHunters) in #1712).
---
**Full Changelog**: [v0.39.0...v0.39.1](https://github.com/cozystack/cozystack/compare/v0.39.0...v0.39.1)

View File

@@ -1,19 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.39.2
-->
## Features and Improvements
* **[vm] Always expose VMs with a service**: Virtual machines are now always exposed with at least a ClusterIP service, ensuring they have in-cluster DNS names and can be accessed from other pods even without public IP addresses ([**@lllamnyp**](https://github.com/lllamnyp) in #1738, #1751).
* **[tenant] Allow egress to parent ingress pods**: Updated tenant network policies to allow egress traffic to parent cluster ingress pods, enabling proper communication patterns between tenant namespaces and parent cluster ingress controllers ([**@lexfrei**](https://github.com/lexfrei) in #1765, #1776).
* **[system] Add resource requests and limits to etcd-defrag**: Added resource requests and limits to etcd-defrag job to ensure proper resource allocation and prevent resource contention during etcd maintenance operations ([**@matthieu-robin**](https://github.com/matthieu-robin) in #1785, #1786).
* **[tenant] Run cleanup job from system namespace**: Moved tenant cleanup job to run from system namespace, improving security and resource isolation for tenant cleanup operations ([**@lllamnyp**](https://github.com/lllamnyp) in #1774, #1777).
## Fixes
* **[kubevirt-operator] Fix typo in VMNotRunningFor10Minutes alert**: Fixed typo in VM alert name, ensuring proper alert triggering and monitoring for virtual machines that are not running for extended periods ([**@lexfrei**](https://github.com/lexfrei) in #1770, #1775).
---
**Full Changelog**: [v0.39.1...v0.39.2](https://github.com/cozystack/cozystack/compare/v0.39.1...v0.39.2)

View File

@@ -1,36 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.39.3
-->
## Features and Improvements
* **[seaweedfs] Traffic locality**: Upgraded SeaweedFS to v4.05 with traffic locality capabilities, new admin component with web-based UI, worker component for distributed operations, and enhanced S3 monitoring with Grafana dashboards. Improves S3 service performance by routing requests to nearest available volume servers ([**@nbykov0**](https://github.com/nbykov0) in #1748, #1830).
* **[kube-ovn] Update to v1.14.25**: Updated Kube-OVN to version 1.14.25 with improved stability and new features ([**@kvaps**](https://github.com/kvaps) in #1819, #1837).
* **[linstor] Build linstor-server with custom patches**: Added custom patches to linstor-server build process, enabling platform-specific optimizations and fixes ([**@kvaps**](https://github.com/kvaps) in #1726, #1818).
* **[api, lineage] Tolerate all taints**: Updated API and lineage webhook to tolerate all taints, ensuring controllers can run on any node regardless of taint configuration ([**@nbykov0**](https://github.com/nbykov0) in #1781, #1827).
* **[ingress] Add topology anti-affinities**: Added topology anti-affinity rules to ingress controller deployment for better pod distribution across nodes ([**@kvaps**](https://github.com/kvaps) in commit 25f31022).
## Fixes
* **[linstor] fix: prevent DRBD device race condition in updateDiscGran**: Fixed race condition in DRBD device management during granularity updates, preventing potential data corruption or device conflicts ([**@kvaps**](https://github.com/kvaps) in #1829, #1836).
* **fix(linstor): prevent orphaned DRBD devices during toggle-disk retry**: Fixed issue where retry logic during disk toggle operations could leave orphaned DRBD devices, now properly cleans up devices during retry attempts ([**@kvaps**](https://github.com/kvaps) in #1823, #1825).
* **[kubernetes] Fix endpoints for cilium-gateway**: Fixed endpoint configuration for cilium-gateway, ensuring proper service discovery and connectivity ([**@kvaps**](https://github.com/kvaps) in #1729, #1808).
* **[kubevirt-operator] Revert incorrect case change in VM alerts**: Reverted incorrect case change in VM alert names to maintain consistency with alert naming conventions ([**@lexfrei**](https://github.com/lexfrei) in #1804, #1806).
## System Configuration
* **[kubeovn] Package from external repo**: Extracted Kube-OVN packaging from main repository to external repository, improving modularity ([**@lllamnyp**](https://github.com/lllamnyp) in #1535).
## Development, Testing, and CI/CD
* **[testing] Add aliases and autocomplete**: Added shell aliases and autocomplete support for testing commands, improving developer experience ([**@lllamnyp**](https://github.com/lllamnyp) in #1803, #1809).
## Dependencies
* **[seaweedfs] Traffic locality**: Upgraded SeaweedFS to v4.05 with traffic locality capabilities ([**@nbykov0**](https://github.com/nbykov0) in #1748, #1830).
* **[kube-ovn] Update to v1.14.25**: Updated Kube-OVN to version 1.14.25 ([**@kvaps**](https://github.com/kvaps) in #1819, #1837).
---
**Full Changelog**: [v0.39.2...v0.39.3](https://github.com/cozystack/cozystack/compare/v0.39.2...v0.39.3)

View File

@@ -1,12 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.39.4
-->
## Features and Improvements
* **[paas-full] Add multus dependencies similar to other CNIs**: Added Multus as a dependency in the paas-full package, consistent with how other CNIs are included. This ensures proper dependency management and simplifies the installation process for environments using Multus networking ([**@nbykov0**](https://github.com/nbykov0) in #1835).
---
**Full Changelog**: [v0.39.3...v0.39.4](https://github.com/cozystack/cozystack/compare/v0.39.3...v0.39.4)

View File

@@ -1,11 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.39.5
-->
## Fixes
* **[linstor] Update piraeus-server patches with critical fixes**: Backported critical patches to piraeus-server that address storage stability issues and improve DRBD resource handling. These patches fix edge cases in device management and ensure more reliable storage operations ([**@kvaps**](https://github.com/kvaps) in #1850, #1853).
---
**Full Changelog**: [v0.39.4...v0.39.5](https://github.com/cozystack/cozystack/compare/v0.39.4...v0.39.5)

View File

@@ -1,206 +0,0 @@
# Cozystack v0.40 — "Enhanced Storage & Platform Architecture"
This release introduces LINSTOR scheduler for optimal pod placement, SeaweedFS traffic locality, a new valuesFrom-based configuration mechanism, auto-diskful for LINSTOR, automated version management systems, and numerous improvements across the platform.
## Feature Highlights
### LINSTOR Scheduler for Optimal Pod Placement
Cozystack now includes a custom Kubernetes scheduler extender that works alongside the default kube-scheduler to optimize pod placement on nodes with LINSTOR storage. When a pod requests LINSTOR-backed storage, the scheduler communicates with the LINSTOR controller to find nodes that have local replicas of the requested volumes, prioritizing placement on nodes with existing data to minimize network traffic and improve I/O performance.
The scheduler includes an admission webhook that automatically routes pods using LINSTOR CSI volumes to the custom scheduler, ensuring seamless integration without manual configuration. This feature significantly improves performance for workloads using LINSTOR storage by reducing network latency and improving data locality.
Learn more about LINSTOR in the [documentation](https://cozystack.io/docs/operations/storage/linstor/).
### SeaweedFS Traffic Locality
SeaweedFS has been upgraded to version 4.05 with new traffic locality capabilities that optimize S3 service traffic distribution. The update includes a new admin component with a web-based UI and authentication support, as well as a worker component for distributed operations. These enhancements improve S3 service performance and provide better visibility through enhanced Grafana dashboard panels for buckets, API calls, costs, and performance metrics.
The traffic locality feature ensures that S3 requests are routed to the nearest available volume servers, reducing latency and improving overall performance for distributed storage operations. TLS certificate support for admin and worker components adds an extra layer of security for management operations.
### ValuesFrom Configuration Mechanism
Cozystack now uses FluxCD's valuesFrom mechanism to replace Helm lookup functions for configuration propagation. This architectural improvement provides cleaner config propagation and eliminates the need for force reconcile controllers. Configuration from ConfigMaps (cozystack, cozystack-branding, cozystack-scheduling) and namespace service references (etcd, host, ingress, monitoring, seaweedfs) is now centrally managed through a `cozystack-values` Secret in each namespace.
This change simplifies Helm chart templates by replacing complex lookup functions with direct value references, improves configuration consistency, and reduces the reconciliation overhead. All HelmReleases now automatically receive cluster and namespace configuration through the valuesFrom mechanism, making configuration management more transparent and maintainable.
### Auto-diskful for LINSTOR
The LINSTOR integration now includes automatic diskful functionality that converts diskless nodes to diskful when they hold DRBD resources in Primary state for an extended period (30 minutes). This feature addresses scenarios where workloads are scheduled on nodes without local storage replicas by automatically creating local disk replicas when needed, improving I/O performance for long-running workloads.
When enabled with cleanup options, the system can automatically remove disk replicas that are no longer needed, preventing storage waste from temporary replicas. This intelligent storage management reduces network traffic for frequently accessed data while maintaining efficient storage utilization.
### Automated Version Management Systems
Cozystack now includes automated version management systems for PostgreSQL, Kubernetes, MariaDB, and Redis applications. These systems automatically track upstream versions and provide mechanisms for automated version updates, ensuring that platform users always have access to the latest stable versions while maintaining compatibility with existing deployments.
The version management systems integrate with the Cozystack API and dashboard, providing administrators with visibility into available versions and update paths. This infrastructure sets the foundation for future automated upgrade workflows and version compatibility management.
---
## Major Features and Improvements
### Storage
* **[linstor] Add linstor-scheduler package**: Added LINSTOR scheduler extender for optimal pod placement on nodes with LINSTOR storage. Includes admission webhook that automatically routes pods using LINSTOR CSI volumes to the custom scheduler, ensuring pods are placed on nodes with local replicas to minimize network traffic and improve I/O performance ([**@kvaps**](https://github.com/kvaps) in #1824).
* **[linstor] Enable auto-diskful for diskless nodes**: Enabled DRBD auto-diskful functionality to automatically convert diskless nodes to diskful when they hold volumes in Primary state for more than 30 minutes. Improves I/O performance for long-running workloads by creating local replicas and includes automatic cleanup options to prevent storage waste ([**@kvaps**](https://github.com/kvaps) in #1826).
* **[linstor] Build linstor-server with custom patches**: Added custom patches to linstor-server build process, enabling platform-specific optimizations and fixes ([**@kvaps**](https://github.com/kvaps) in #1726).
* **[seaweedfs] Traffic locality**: Upgraded SeaweedFS to v4.05 with traffic locality capabilities, new admin component with web-based UI, worker component for distributed operations, and enhanced S3 monitoring with Grafana dashboards. Improves S3 service performance by routing requests to nearest available volume servers ([**@nbykov0**](https://github.com/nbykov0) in #1748).
* **[linstor] fix: prevent DRBD device race condition in updateDiscGran**: Fixed race condition in DRBD device management during granularity updates, preventing potential data corruption or device conflicts ([**@kvaps**](https://github.com/kvaps) in #1829).
* **fix(linstor): prevent orphaned DRBD devices during toggle-disk retry**: Fixed issue where retry logic during disk toggle operations could leave orphaned DRBD devices, now properly cleans up devices during retry attempts ([**@kvaps**](https://github.com/kvaps) in #1823).
### Platform Architecture
* **[platform] Replace Helm lookup with valuesFrom mechanism**: Replaced Helm lookup functions with FluxCD valuesFrom mechanism for configuration propagation. Configuration from ConfigMaps and namespace references is now managed through `cozystack-values` Secret, simplifying templates and eliminating force reconcile controllers ([**@kvaps**](https://github.com/kvaps) in #1787).
* **[platform] refactor: split cozystack-resource-definitions into separate packages**: Refactored cozystack-resource-definitions into separate packages for better organization and maintainability, improving code structure and reducing coupling between components ([**@kvaps**](https://github.com/kvaps) in #1778).
* **[platform] Separate assets server into dedicated deployment**: Separated assets server from main platform deployment, improving scalability and allowing independent scaling of asset delivery infrastructure ([**@kvaps**](https://github.com/kvaps) in #1705).
* **[core] Extract Talos package from installer**: Extracted Talos package configuration from installer into a separate package, improving modularity and enabling independent updates ([**@kvaps**](https://github.com/kvaps) in #1724).
* **[registry] Add application labels and update filtering mechanism**: Added application labels to registry resources and improved filtering mechanism for better resource discovery and organization ([**@kvaps**](https://github.com/kvaps) in #1707).
* **fix(registry): implement field selector filtering for label-based resources**: Implemented field selector filtering for label-based resources in the registry, improving query performance and resource lookup efficiency ([**@kvaps**](https://github.com/kvaps) in #1845).
* **[platform] Add alphabetical sorting to registry resource lists**: Added alphabetical sorting to registry resource lists in the API and dashboard, improving user experience when browsing available applications ([**@lexfrei**](https://github.com/lexfrei) in #1764).
### Version Management
* **[postgres] Add version management system with automated version updates**: Introduced version management system for PostgreSQL with automated version tracking and update mechanisms ([**@kvaps**](https://github.com/kvaps) in #1671).
* **[kubernetes] Add version management system with automated version updates**: Added version management system for Kubernetes tenant clusters with automated version tracking and update capabilities ([**@kvaps**](https://github.com/kvaps) in #1672).
* **[mariadb] Add version management system with automated version updates**: Implemented version management system for MariaDB with automated version tracking and update mechanisms ([**@kvaps**](https://github.com/kvaps) in #1680).
* **[redis] Add version management system with automated version updates**: Added version management system for Redis with automated version tracking and update capabilities ([**@kvaps**](https://github.com/kvaps) in #1681).
### Networking
* **[kube-ovn] Update to v1.14.25**: Updated Kube-OVN to version 1.14.25 with improved stability and new features ([**@kvaps**](https://github.com/kvaps) in #1819).
* **[kubeovn] Package from external repo**: Extracted Kube-OVN packaging from main repository to external repository, improving modularity ([**@lllamnyp**](https://github.com/lllamnyp) in #1535).
* **[cilium] Update Cilium to v1.18.5**: Updated Cilium to version 1.18.5 with latest features and bug fixes ([**@lexfrei**](https://github.com/lexfrei) in #1769).
* **[system/cilium] Enable topology-aware routing for services**: Enabled topology-aware routing for Cilium services, improving traffic distribution and reducing latency by routing traffic to endpoints in the same zone when possible ([**@nbykov0**](https://github.com/nbykov0) in #1734).
* **[cilium] Enable automatic pod rollout on configmap updates**: Cilium and Cilium operator pods now automatically restart when the cilium-config ConfigMap is updated, ensuring configuration changes are applied immediately ([**@kvaps**](https://github.com/kvaps) in #1728).
* **[kubernetes] Fix endpoints for cilium-gateway**: Fixed endpoint configuration for cilium-gateway, ensuring proper service discovery and connectivity ([**@kvaps**](https://github.com/kvaps) in #1729).
* **[multus] Increase memory limit**: Increased memory limits for Multus components to handle larger network configurations and reduce out-of-memory issues ([**@nbykov0**](https://github.com/nbykov0) in #1773).
* **[main][paas-full] Add multus dependencies similar to other CNIs**: Added Multus as a dependency in the paas-full package, consistent with how other CNIs are included ([**@nbykov0**](https://github.com/nbykov0) in #1842).
### Virtual Machines
* **[vm] Always expose VMs with a service**: Virtual machines are now always exposed with at least a ClusterIP service, ensuring they have in-cluster DNS names and can be accessed from other pods even without public IP addresses ([**@lllamnyp**](https://github.com/lllamnyp) in #1738).
* **[virtual-machine] Improve check for resizing job**: Improved storage resize logic to only expand persistent volume claims when storage is being increased, preventing unintended storage reduction operations ([**@kvaps**](https://github.com/kvaps) in #1688).
* **[virtual-machine,vm-instance] Add nodeAffinity for Windows VMs based on scheduling config**: Added nodeAffinity configuration to virtual-machine and vm-instance charts to support dedicated nodes for Windows VMs ([**@kvaps**](https://github.com/kvaps) in #1693).
### Monitoring
* **[monitoring] Add SLACK_SEVERITY_FILTER field and VMAgent for tenant monitoring**: Introduced SLACK_SEVERITY_FILTER environment variable in Alerta deployment to enable filtering of alert severities for Slack notifications. Added VMAgent resource template for scraping metrics within tenant namespaces, improving monitoring granularity ([**@IvanHunters**](https://github.com/IvanHunters) in #1712).
* **[monitoring] Improve tenant metrics collection**: Improved tenant metrics collection mechanisms for better observability and monitoring coverage ([**@IvanHunters**](https://github.com/IvanHunters) in #1684).
### System Configuration
* **[api, lineage] Tolerate all taints**: Updated API and lineage webhook to tolerate all taints, ensuring controllers can run on any node regardless of taint configuration ([**@nbykov0**](https://github.com/nbykov0) in #1781).
* **[system] Add resource requests and limits to etcd-defrag**: Added resource requests and limits to etcd-defrag job to ensure proper resource allocation and prevent resource contention ([**@matthieu-robin**](https://github.com/matthieu-robin) in #1785).
* **[system:coredns] update coredns app labels to match Talos coredns labels**: Updated coredns app labels to match Talos coredns labels, ensuring consistency across the platform ([**@nbykov0**](https://github.com/nbykov0) in #1675).
* **[system:monitoring-agents] rename coredns metrics service**: Renamed coredns metrics service to avoid interference with coredns service used for name resolution in tenant k8s clusters ([**@nbykov0**](https://github.com/nbykov0) in #1676).
### Tenants and Namespaces
* **[tenant] Allow egress to parent ingress pods**: Updated tenant network policies to allow egress traffic to parent cluster ingress pods, enabling proper communication patterns ([**@lexfrei**](https://github.com/lexfrei) in #1765).
* **[tenant] Run cleanup job from system namespace**: Moved tenant cleanup job to run from system namespace, improving security and resource isolation ([**@lllamnyp**](https://github.com/lllamnyp) in #1774).
### FluxCD
* **[fluxcd] Add flux-aio module and migration**: Added FluxCD all-in-one module with migration support, simplifying FluxCD installation and management ([**@kvaps**](https://github.com/kvaps) in #1698).
* **[fluxcd] Enable source-watcher**: Enabled source-watcher in FluxCD configuration for improved GitOps synchronization and faster update detection ([**@kvaps**](https://github.com/kvaps) in #1706).
### Applications
* **[dashboard] Fix CustomFormsOverride schema to nest properties under spec.properties**: Fixed CustomFormsOverride schema generation to properly nest properties under `spec.properties` instead of directly under `properties`, ensuring correct form schema generation ([**@kvaps**](https://github.com/kvaps) in #1692).
* **[apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK**: Refactored apiserver REST handlers to use typed objects instead of unstructured.Unstructured, eliminating runtime conversions. Fixed UnstructuredList GVK issue where objects were using the first registered kind instead of the correct kind ([**@kvaps**](https://github.com/kvaps) in #1679).
* **[keycloak] Make kubernetes client public**: Made Kubernetes client public in Keycloak configuration, enabling broader access patterns for Kubernetes integrations ([**@lllamnyp**](https://github.com/lllamnyp) in #1802).
## Improvements
* **[granular kubernetes application extensions dependencies]**: Improved dependency management for Kubernetes application extensions with more granular control over dependencies ([**@nbykov0**](https://github.com/nbykov0) in #1683).
* **[core:installer] Address buildx warnings**: Fixed Dockerfile syntax warnings from buildx, ensuring clean builds without warnings ([**@nbykov0**](https://github.com/nbykov0) in #1682).
* **[linstor] Update piraeus-operator v2.10.2**: Updated LINSTOR CSI to version 2.10.2 with improved stability and bug fixes ([**@kvaps**](https://github.com/kvaps) in #1689).
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 with improved S3 daemon performance and fixes ([**@kvaps**](https://github.com/kvaps) in #1725).
* **[installer,dx] Rename cozypkg to cozyhr**: Renamed cozypkg tool to cozyhr for better branding and consistency ([**@kvaps**](https://github.com/kvaps) in #1763).
## Fixes
* **fix(platform): fix migrations for v0.40 release**: Fixed platform migrations for v0.40 release, ensuring smooth upgrades from previous versions ([**@kvaps**](https://github.com/kvaps) in #1846).
* **[platform] fix migration for removing fluxcd-operator**: Fixed migration logic for removing fluxcd-operator, ensuring clean removal without leaving orphaned resources ([**@kvaps**](https://github.com/kvaps) in commit 4a83d2c7).
* **[kubevirt-operator] Fix typo in VMNotRunningFor10Minutes alert**: Fixed typo in VM alert name, ensuring proper alert triggering and monitoring ([**@kvaps**](https://github.com/kvaps) in #1770).
* **[kubevirt-operator] Revert incorrect case change in VM alerts**: Reverted incorrect case change in VM alert names to maintain consistency ([**@lexfrei**](https://github.com/lexfrei) in #1804).
* **[cozystack-controller] Fix: move crds to definitions**: Fixed CRD placement by moving them to definitions directory, ensuring proper resource organization ([**@kvaps**](https://github.com/kvaps) in #1759).
## Dependencies
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 ([**@kvaps**](https://github.com/kvaps) in #1725).
* **[seaweedfs] Traffic locality**: Upgraded SeaweedFS to v4.05 with traffic locality capabilities ([**@nbykov0**](https://github.com/nbykov0) in #1748).
* **[linstor] Update piraeus-operator v2.10.2**: Updated piraeus-operator to version 2.10.2 ([**@kvaps**](https://github.com/kvaps) in #1689).
* **[kube-ovn] Update to v1.14.25**: Updated Kube-OVN to version 1.14.25 ([**@kvaps**](https://github.com/kvaps) in #1819).
* **[cilium] Update Cilium to v1.18.5**: Updated Cilium to version 1.18.5 ([**@lexfrei**](https://github.com/lexfrei) in #1769).
* **Update go modules**: Updated Go modules to latest versions ([**@kvaps**](https://github.com/kvaps) in #1736).
## Development, Testing, and CI/CD
* **[ci] Fix auto-release workflow**: Fixed auto-release workflow to ensure correct release publishing and tagging ([**@kvaps**](https://github.com/kvaps) in commit 526af294).
* **fix(ci): ensure correct latest release after backport publishing**: Fixed CI workflow to correctly identify and tag the latest release after backport publishing ([**@kvaps**](https://github.com/kvaps) in #1800).
* **[workflows] Add auto patch release workflow**: Added automated patch release workflow for streamlined release management ([**@kvaps**](https://github.com/kvaps) in #1754).
* **[workflow] Add GitHub Action to update release notes from changelogs**: Added GitHub Action to automatically update release notes from changelog files ([**@kvaps**](https://github.com/kvaps) in #1752).
* **[ci] Improve backport workflow with merge_commits skip and conflict resolution**: Improved backport workflow with better merge commit handling and conflict resolution ([**@kvaps**](https://github.com/kvaps) in #1694).
* **[testing] Add aliases and autocomplete**: Added shell aliases and autocomplete support for testing commands, improving developer experience ([**@lllamnyp**](https://github.com/lllamnyp) in #1803).
* **[kubernetes] Add lb tests for tenant k8s**: Added load balancer tests for tenant Kubernetes clusters, improving test coverage ([**@IvanHunters**](https://github.com/IvanHunters) in #1783).
* **[agents] Add instructions for working with unresolved code review comments**: Added documentation and instructions for working with unresolved code review comments in agent workflows ([**@kvaps**](https://github.com/kvaps) in #1710).
* **feat(ci): add /retest command to rerun tests from Prepare environment**: Added `/retest` command to rerun tests from Prepare environment workflow ([**@kvaps**](https://github.com/kvaps) in commit 30c1041e).
* **fix(ci): remove GITHUB_TOKEN extraheader to trigger workflows**: Removed GITHUB_TOKEN extraheader to properly trigger workflows ([**@kvaps**](https://github.com/kvaps) in commit 68a639b3).
* **Fix: Add missing components to `distro-full` bundle**: Fixed missing components in distro-full bundle, ensuring all required components are included ([**@LoneExile**](https://github.com/LoneExile) in #1620).
* **Update Flux Operator (v0.33.0)**: Updated Flux Operator to version 0.33.0 ([**@kingdonb**](https://github.com/kingdonb) in #1649).
* **Add changelogs for v0.38.3 and v.0.38.4**: Added missing changelogs for v0.38.3 and v0.38.4 releases ([**@androndo**](https://github.com/androndo) in #1743).
* **Add changelogs to v.0.39.1**: Added changelog for v0.39.1 release ([**@androndo**](https://github.com/androndo) in #1750).
* **Add Cloupard to ADOPTERS.md**: Added Cloupard to the adopters list ([**@SerjioTT**](https://github.com/SerjioTT) in #1733).
## Documentation
* **[website] docs: expand monitoring and alerting documentation**: Expanded monitoring and alerting documentation with comprehensive guides, examples, and troubleshooting information ([**@IvanHunters**](https://github.com/IvanHunters) in [cozystack/website#388](https://github.com/cozystack/website/pull/388)).
* **[website] fix auto-generation of documentation**: Fixed automatic documentation generation process, ensuring all documentation is properly generated and formatted ([**@IvanHunters**](https://github.com/IvanHunters) in [cozystack/website#391](https://github.com/cozystack/website/pull/391)).
* **[website] secure boot**: Added documentation for Secure Boot support in Talos Linux ([**@kvaps**](https://github.com/kvaps) in [cozystack/website#387](https://github.com/cozystack/website/pull/387)).
## Tools
* **[talm] feat(helpers): add bond interface discovery helpers**: Added bond interface discovery helpers to talm for easier network configuration ([**@kvaps**](https://github.com/kvaps) in [cozystack/talm#94](https://github.com/cozystack/talm/pull/94)).
* **[talm] feat(talosconfig): add certificate regeneration from secrets.yaml**: Added certificate regeneration functionality to talm talosconfig command, allowing certificates to be regenerated from secrets.yaml ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@1319dde).
* **[talm] fix(init): make name optional for -u flag**: Made name parameter optional for init command with -u flag, improving flexibility ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@da29320).
* **[talm] fix(wrapper): copy NoOptDefVal when remapping -f to -F flag**: Fixed wrapper to properly copy NoOptDefVal when remapping flags, ensuring correct default value handling ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@f6a6f1d).
* **[talm] fix(root): detect project root with secrets.encrypted.yaml**: Fixed root detection to properly identify project root when secrets.encrypted.yaml is present ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@cf56780).
* **[talm] Fix interfaces helper for Talos v1.12**: Fixed interfaces helper to work correctly with Talos v1.12 ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@34984ae).
* **[talm] Fix typo on README.md**: Fixed typo in README documentation ([**@diegolakatos**](https://github.com/diegolakatos) in [cozystack/talm#92](https://github.com/cozystack/talm/pull/92)).
* **[talm] fix(template): return error for invalid YAML in template output**: Fixed template command to return proper error for invalid YAML output ([**@kvaps**](https://github.com/kvaps) in [cozystack/talm#93](https://github.com/cozystack/talm/pull/93)).
* **[talm] feat(cozystack): enable allocateNodeCIDRs by default**: Enabled allocateNodeCIDRs by default in talm cozystack preset ([**@lexfrei**](https://github.com/lexfrei) in [cozystack/talm#91](https://github.com/cozystack/talm/pull/91)).
* **[boot-to-talos] feat(network): add VLAN interface support via netlink**: Added VLAN interface support via netlink in boot-to-talos for advanced network configuration ([**@kvaps**](https://github.com/kvaps) in cozystack/boot-to-talos@02874d7).
* **[boot-to-talos] feat(network): add bond interface support via netlink**: Added bond interface support via netlink in boot-to-talos for network bonding configurations ([**@kvaps**](https://github.com/kvaps) in cozystack/boot-to-talos@067822d).
* **[boot-to-talos] Draft EFI Support**: Added draft EFI support in boot-to-talos for UEFI boot scenarios ([**@kvaps**](https://github.com/kvaps) in cozystack/boot-to-talos@e194bc8).
* **[boot-to-talos] Change default install image size from 2GB to 3GB**: Changed default install image size from 2GB to 3GB to accommodate larger installations ([**@kvaps**](https://github.com/kvaps) in cozystack/boot-to-talos@3bfb035).
* **[cozyhr] feat(values): add valuesFrom support for HelmRelease**: Added valuesFrom support for HelmRelease in cozyhr tool, enabling better configuration management ([**@kvaps**](https://github.com/kvaps) in cozystack/cozyhr@7dff0c8).
* **[cozyhr] Rename cozypkg to cozyhr**: Renamed cozypkg tool to cozyhr for better branding ([**@kvaps**](https://github.com/kvaps) in cozystack/cozyhr@1029461).
---
## Contributors
We'd like to thank all contributors who made this release possible:
* [**@IvanHunters**](https://github.com/IvanHunters)
* [**@kvaps**](https://github.com/kvaps)
* [**@lexfrei**](https://github.com/lexfrei)
* [**@lllamnyp**](https://github.com/lllamnyp)
* [**@nbykov0**](https://github.com/nbykov0)
* [**@LoneExile**](https://github.com/LoneExile)
* [**@kingdonb**](https://github.com/kingdonb)
* [**@androndo**](https://github.com/androndo)
* [**@SerjioTT**](https://github.com/SerjioTT)
* [**@matthieu-robin**](https://github.com/matthieu-robin)
* [**@diegolakatos**](https://github.com/diegolakatos)
---
**Full Changelog**: [v0.39.0...v0.40.0](https://github.com/cozystack/cozystack/compare/v0.39.0...v0.40.0)
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.40.0
-->

View File

@@ -1,11 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.40.1
-->
## Fixes
* **[linstor] Update piraeus-server patches with critical fixes**: Backported critical patches to piraeus-server that address storage stability issues and improve DRBD resource handling. These patches fix edge cases in device management and ensure more reliable storage operations ([**@kvaps**](https://github.com/kvaps) in #1850, #1852).
---
**Full Changelog**: [v0.40.0...v0.40.1](https://github.com/cozystack/cozystack/compare/v0.40.0...v0.40.1)

View File

@@ -1,15 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.40.2
-->
## Improvements
* **[linstor] Refactor node-level RWX validation**: Refactored the node-level ReadWriteMany (RWX) validation logic in LINSTOR CSI. The validation has been moved to the CSI driver level with a custom linstor-csi image build, providing more reliable RWX volume handling and clearer error messages when RWX requirements cannot be satisfied ([**@kvaps**](https://github.com/kvaps) in #1856, #1857).
## Fixes
* **[linstor] Remove node-level RWX validation**: Removed the problematic node-level RWX validation that was causing issues with volume provisioning. The validation logic has been refactored and moved to a more appropriate location in the LINSTOR CSI driver ([**@kvaps**](https://github.com/kvaps) in #1851).
---
**Full Changelog**: [v0.40.1...v0.40.2](https://github.com/cozystack/cozystack/compare/v0.40.1...v0.40.2)

View File

@@ -1,15 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.40.3
-->
## Fixes
* **[apiserver] Fix Watch resourceVersion and bookmark handling**: Fixed issues with Watch API handling of resourceVersion and bookmarks, ensuring proper event streaming and state synchronization for API clients ([**@kvaps**](https://github.com/kvaps) in #1860).
## Dependencies
* **[cilium] Update Cilium to v1.18.6**: Updated Cilium CNI to v1.18.6 with security fixes and performance improvements ([**@sircthulhu**](https://github.com/sircthulhu) in #1868, #1870).
---
**Full Changelog**: [v0.40.2...v0.40.3](https://github.com/cozystack/cozystack/compare/v0.40.2...v0.40.3)

View File

@@ -1,23 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.40.4
-->
## Improvements
* **[kubernetes] Increase default apiServer resourcesPreset to large**: Increased the default resource preset for kube-apiserver to `large` to ensure more reliable operation under higher workloads and prevent resource constraints ([**@kvaps**](https://github.com/kvaps) in #1875, #1882).
* **[kubernetes] Increase kube-apiserver startup probe threshold**: Increased the startup probe threshold for kube-apiserver to allow more time for the API server to become ready, especially in scenarios with slow storage or high load ([**@kvaps**](https://github.com/kvaps) in #1876, #1883).
* **[etcd] Increase probe thresholds for better recovery**: Increased etcd probe thresholds to provide more time for recovery operations, improving cluster resilience during network issues or temporary slowdowns ([**@kvaps**](https://github.com/kvaps) in #1874, #1878).
## Fixes
* **[dashboard] Fix view of loadbalancer IP in services window**: Fixed an issue where load balancer IP addresses were not displayed correctly in the services window of the dashboard ([**@IvanHunters**](https://github.com/IvanHunters) in #1884, #1887).
## Dependencies
* **Update Talos Linux v1.11.6**: Updated Talos Linux to v1.11.6 with latest security patches and improvements ([**@kvaps**](https://github.com/kvaps) in #1879).
---
**Full Changelog**: [v0.40.3...v0.40.4](https://github.com/cozystack/cozystack/compare/v0.40.3...v0.40.4)

View File

@@ -1,15 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.40.5
-->
## Improvements
* **[dashboard] Improve dashboard session params**: Improved session parameter handling in the dashboard for better user experience and more reliable session management ([**@lllamnyp**](https://github.com/lllamnyp) in #1913, #1919).
## Dependencies
* **Update cozyhr to v1.6.1**: Updated cozyhr to v1.6.1, which fixes a critical bug causing helm-controller v0.37.0+ to unexpectedly uninstall HelmReleases after cozyhr apply by correcting history snapshot fields for helm-controller compatibility ([**@kvaps**](https://github.com/kvaps) in cozystack/cozyhr#10).
---
**Full Changelog**: [v0.40.4...v0.40.5](https://github.com/cozystack/cozystack/compare/v0.40.4...v0.40.5)

View File

@@ -1,11 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.40.6
-->
## Fixes
* **[kubernetes] Fix manifests for kubernetes deployment**: Fixed incorrect manifests that prevented proper Kubernetes deployment, restoring correct application behavior ([**@IvanHunters**](https://github.com/IvanHunters) in #1943, #1944).
---
**Full Changelog**: [v0.40.5...v0.40.6](https://github.com/cozystack/cozystack/compare/v0.40.5...v0.40.6)

View File

@@ -1,11 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.40.7
-->
## Security
* **[dashboard] Verify JWT token**: Added JWT token verification to the dashboard, ensuring that authentication tokens are properly validated before granting access. This prevents unauthorized access through forged or expired tokens ([**@lllamnyp**](https://github.com/lllamnyp) in #1980, #1984).
---
**Full Changelog**: [v0.40.6...v0.40.7](https://github.com/cozystack/cozystack/compare/v0.40.6...v0.40.7)

View File

@@ -1,63 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.41.0
-->
# Cozystack v0.41.0 — "MongoDB"
This release introduces MongoDB as a new managed application, expanding Cozystack's database offerings alongside existing PostgreSQL, MySQL, and Redis services. The release also includes storage improvements, Kubernetes stability enhancements, and updated documentation.
## Feature Highlights
### MongoDB Managed Application
Cozystack now includes MongoDB as a fully managed database service. Users can deploy production-ready MongoDB instances directly from the application catalog with minimal configuration.
Key capabilities:
- **Replica Set deployment**: Automatic configuration of MongoDB replica sets for high availability
- **Persistent storage**: Integration with Cozystack storage backends for reliable data persistence
- **Resource management**: Configurable CPU, memory, and storage resources
- **Monitoring integration**: Built-in metrics export for platform monitoring
Deploy MongoDB through the Cozystack dashboard or using the standard application deployment workflow ([**@lexfrei**](https://github.com/lexfrei) in #1822, #1881).
## Improvements
* **[linstor] Update piraeus-server patches with critical fixes**: Backported critical patches to piraeus-server that address storage stability issues and improve DRBD resource handling. These patches fix edge cases in device management and ensure more reliable storage operations ([**@kvaps**](https://github.com/kvaps) in #1850, #1852).
* **[linstor] Refactor node-level RWX validation**: Refactored the node-level ReadWriteMany (RWX) validation logic in LINSTOR CSI. The validation has been moved to the CSI driver level with a custom linstor-csi image build, providing more reliable RWX volume handling and clearer error messages when RWX requirements cannot be satisfied ([**@kvaps**](https://github.com/kvaps) in #1856, #1857).
* **[kubernetes] Increase default apiServer resourcesPreset to large**: Increased the default resource preset for kube-apiserver to `large` to ensure more reliable operation under higher workloads and prevent resource constraints ([**@kvaps**](https://github.com/kvaps) in #1875, #1882).
* **[kubernetes] Increase kube-apiserver startup probe threshold**: Increased the startup probe threshold for kube-apiserver to allow more time for the API server to become ready, especially in scenarios with slow storage or high load ([**@kvaps**](https://github.com/kvaps) in #1876, #1883).
* **[etcd] Increase probe thresholds for better recovery**: Increased etcd probe thresholds to provide more time for recovery operations, improving cluster resilience during network issues or temporary slowdowns ([**@kvaps**](https://github.com/kvaps) in #1874, #1878).
## Fixes
* **[linstor] Remove node-level RWX validation**: Removed the problematic node-level RWX validation that was causing issues with volume provisioning. The validation logic has been refactored and moved to a more appropriate location in the LINSTOR CSI driver ([**@kvaps**](https://github.com/kvaps) in #1851).
* **[apiserver] Fix Watch resourceVersion and bookmark handling**: Fixed issues with Watch API handling of resourceVersion and bookmarks, ensuring proper event streaming and state synchronization for API clients ([**@kvaps**](https://github.com/kvaps) in #1860).
* **[dashboard] Fix view of loadbalancer IP in services window**: Fixed an issue where load balancer IP addresses were not displayed correctly in the services window of the dashboard ([**@IvanHunters**](https://github.com/IvanHunters) in #1884, #1887).
## Dependencies
* **[cilium] Update cilium to v1.18.6**: Updated Cilium CNI to v1.18.6 with security fixes and performance improvements ([**@sircthulhu**](https://github.com/sircthulhu) in #1868, #1870).
* **Update Talos Linux v1.11.6**: Updated Talos Linux to v1.11.6 with latest security patches and improvements ([**@kvaps**](https://github.com/kvaps) in #1879).
## Documentation
* **[website] Add documentation for creating and managing cloned virtual machines**: Added comprehensive guide for VM cloning operations ([**@sircthulhu**](https://github.com/sircthulhu) in [cozystack/website#401](https://github.com/cozystack/website/pull/401)).
* **[website] Simplify NFS driver setup instructions**: Improved NFS driver setup documentation with clearer instructions ([**@kvaps**](https://github.com/kvaps) in [cozystack/website#399](https://github.com/cozystack/website/pull/399)).
* **[website] Update Talos installation docs for Hetzner and Servers.com**: Updated installation documentation with improved instructions for Hetzner and Servers.com environments ([**@kvaps**](https://github.com/kvaps) in [cozystack/website#395](https://github.com/cozystack/website/pull/395)).
* **[website] Add Hetzner RobotLB documentation**: Added documentation for configuring public IP with Hetzner RobotLB ([**@kvaps**](https://github.com/kvaps) in [cozystack/website#394](https://github.com/cozystack/website/pull/394)).
* **[website] Add Hidora organization support details**: Added Hidora to the support page with organization details ([**@matthieu-robin**](https://github.com/matthieu-robin) in [cozystack/website#397](https://github.com/cozystack/website/pull/397), [cozystack/website#398](https://github.com/cozystack/website/pull/398)).
---
**Full Changelog**: [v0.40.0...v0.41.0](https://github.com/cozystack/cozystack/compare/v0.40.0...v0.41.0)

View File

@@ -1,11 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.41.1
-->
## Improvements
* **[kubernetes] Add enum validation for IngressNginx exposeMethod**: Added enum validation for the `exposeMethod` field in IngressNginx configuration, preventing invalid values and improving user experience with clear valid options ([**@sircthulhu**](https://github.com/sircthulhu) in #1895, #1897).
---
**Full Changelog**: [v0.41.0...v0.41.1](https://github.com/cozystack/cozystack/compare/v0.41.0...v0.41.1)

View File

@@ -1,13 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.41.2
-->
## Improvements
* **[monitoring-agents] Set minReplicas to 1 for VPA for VMAgent**: Configured VPA (Vertical Pod Autoscaler) to maintain at least 1 replica for VMAgent, ensuring monitoring availability during scaling operations ([**@sircthulhu**](https://github.com/sircthulhu) in #1894, #1905).
* **[mongodb] Remove user-configurable images from MongoDB chart**: Removed user-configurable image options from the MongoDB chart to simplify configuration and ensure consistency with tested image versions ([**@kvaps**](https://github.com/kvaps) in #1901, #1904).
---
**Full Changelog**: [v0.41.1...v0.41.2](https://github.com/cozystack/cozystack/compare/v0.41.1...v0.41.2)

View File

@@ -1,15 +0,0 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v0.41.3
-->
## Improvements
* **[kubernetes] Show Service and Ingress resources for Kubernetes app in dashboard**: Added visibility of Service and Ingress resources for Kubernetes applications in the dashboard, improving resource management and monitoring capabilities ([**@sircthulhu**](https://github.com/sircthulhu) in #1912, #1915).
## Fixes
* **[dashboard] Fix filtering on Pods tab for Service**: Fixed an issue where pod filtering was not working correctly on the Pods tab when viewing Services in the dashboard ([**@sircthulhu**](https://github.com/sircthulhu) in #1909, #1914).
---
**Full Changelog**: [v0.41.2...v0.41.3](https://github.com/cozystack/cozystack/compare/v0.41.2...v0.41.3)

Some files were not shown because too many files have changed in this diff Show More