mirror of
https://github.com/cozystack/cozystack.git
synced 2026-03-06 15:08:53 +00:00
Compare commits
96 Commits
cozystack-
...
remove-ass
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
725f70399f | ||
|
|
49f7ac9192 | ||
|
|
ab8cc5ffd4 | ||
|
|
f5e6107e3a | ||
|
|
0fb02e6470 | ||
|
|
3a1e7fdd8f | ||
|
|
05b2244b36 | ||
|
|
d2fc6f8470 | ||
|
|
daa91fd2f3 | ||
|
|
43df3a1b70 | ||
|
|
7b75903cee | ||
|
|
07b406e9bc | ||
|
|
5f36396ccc | ||
|
|
811fde9993 | ||
|
|
695fc05dec | ||
|
|
2e61810547 | ||
|
|
36836fd84e | ||
|
|
bf1928c96f | ||
|
|
069a3ca9b0 | ||
|
|
f4228ffc20 | ||
|
|
bfafcaa3ab | ||
|
|
f59665208c | ||
|
|
66a756b606 | ||
|
|
a8688744e9 | ||
|
|
7a964eb7de | ||
|
|
3a5977ff60 | ||
|
|
bbeaaccd0c | ||
|
|
08e5a25ce7 | ||
|
|
dd0bbd375f | ||
|
|
d26d99c925 | ||
|
|
675eaa6178 | ||
|
|
5638a7eae9 | ||
|
|
1fc48da514 | ||
|
|
2d6e50bbeb | ||
|
|
dea42d035f | ||
|
|
f3722d0b2a | ||
|
|
f4e43469dc | ||
|
|
7e86a04fab | ||
|
|
935addebe9 | ||
|
|
a902eaec91 | ||
|
|
c200a45824 | ||
|
|
c06f399d20 | ||
|
|
f28d639aea | ||
|
|
63baadfd9a | ||
|
|
2d4e568efe | ||
|
|
b94fc6c9c8 | ||
|
|
36ef62af52 | ||
|
|
715509f57e | ||
|
|
db8a006565 | ||
|
|
7fab919e07 | ||
|
|
c43db3b7cc | ||
|
|
14803a4162 | ||
|
|
3a1f4ab854 | ||
|
|
307f20f8af | ||
|
|
5af0d132a5 | ||
|
|
a03042e017 | ||
|
|
4dfdbfeb62 | ||
|
|
da56c88aa7 | ||
|
|
e9cad4507b | ||
|
|
19e06f7beb | ||
|
|
0b16f83eae | ||
|
|
8cf68149cb | ||
|
|
cbc998b2ec | ||
|
|
fe9fca6abf | ||
|
|
d3f1b821f7 | ||
|
|
7165cea57a | ||
|
|
2990f0520a | ||
|
|
d7931fdb20 | ||
|
|
0697c0221b | ||
|
|
59f62b7834 | ||
|
|
f28b53d8dc | ||
|
|
e5c3492089 | ||
|
|
9d85cfb647 | ||
|
|
db800b510e | ||
|
|
202410a438 | ||
|
|
f7877b9bce | ||
|
|
ab6c6bad16 | ||
|
|
09e0b5f4ec | ||
|
|
e31ee932c0 | ||
|
|
517369629f | ||
|
|
00cef35214 | ||
|
|
e213b068e8 | ||
|
|
98488100e4 | ||
|
|
0ed52f670d | ||
|
|
b6900a258a | ||
|
|
c919ea6e39 | ||
|
|
7fa875db52 | ||
|
|
cf15ad1073 | ||
|
|
ba50d01877 | ||
|
|
7e7716aa44 | ||
|
|
669bf3d2f5 | ||
|
|
8d43f993e4 | ||
|
|
97e8d2aa49 | ||
|
|
c0e0ef0f7c | ||
|
|
8866a307bf | ||
|
|
3975da93c6 |
188
.github/workflows/auto-release.yaml
vendored
Normal file
188
.github/workflows/auto-release.yaml
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
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`);
|
||||
|
||||
143
.github/workflows/pull-requests-release.yaml
vendored
143
.github/workflows/pull-requests-release.yaml
vendored
@@ -46,7 +46,12 @@ 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 }}
|
||||
|
||||
@@ -105,67 +110,95 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
# Get the latest published release
|
||||
- name: Get the latest published release
|
||||
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 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 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');
|
||||
|
||||
# Publish draft release with correct flags
|
||||
# Publish draft release and ensure correct latest flag
|
||||
- name: Publish draft release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const tag = '${{ steps.get_tag.outputs.tag }}';
|
||||
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]);
|
||||
|
||||
// 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
|
||||
const releases = await github.rest.repos.listReleases({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const draft = releases.data.find(r => r.tag_name === tag && r.draft);
|
||||
if (!draft) throw new Error(`Draft release for ${tag} not found`);
|
||||
await github.rest.repos.updateRelease({
|
||||
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 }}'
|
||||
repo: context.repo.repo,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
console.log(`🚀 Published release for ${tag}`);
|
||||
// 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
|
||||
});
|
||||
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`);
|
||||
}
|
||||
|
||||
2
.github/workflows/pull-requests.yaml
vendored
2
.github/workflows/pull-requests.yaml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
DOCKER_CONFIG: ${{ runner.temp }}/.docker
|
||||
|
||||
- name: Build Talos image
|
||||
run: make -C packages/core/installer talos-nocloud
|
||||
run: make -C packages/core/talos talos-nocloud
|
||||
|
||||
- name: Save git diff as patch
|
||||
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
|
||||
|
||||
78
.github/workflows/retest.yaml
vendored
Normal file
78
.github/workflows/retest.yaml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
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})`);
|
||||
26
.github/workflows/tags.yaml
vendored
26
.github/workflows/tags.yaml
vendored
@@ -123,32 +123,6 @@ jobs:
|
||||
git commit -m "Prepare release ${GITHUB_REF#refs/tags/}" -s || echo "No changes to commit"
|
||||
git push origin HEAD || true
|
||||
|
||||
# Get `latest_version` from latest published release
|
||||
- name: Get latest published release
|
||||
if: steps.check_release.outputs.skip == 'false'
|
||||
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
|
||||
if: steps.check_release.outputs.skip == 'false'
|
||||
|
||||
92
.github/workflows/update-releasenotes.yaml
vendored
Normal file
92
.github/workflows/update-releasenotes.yaml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
38
AGENTS.md
38
AGENTS.md
@@ -3,14 +3,38 @@
|
||||
This file provides structured guidance for AI coding assistants and agents
|
||||
working with the **Cozystack** project.
|
||||
|
||||
## Agent Documentation
|
||||
## Activation
|
||||
|
||||
| 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 |
|
||||
**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
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
||||
16
Makefile
16
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: manifests repos assets unit-tests helm-unit-tests
|
||||
.PHONY: manifests assets unit-tests helm-unit-tests
|
||||
|
||||
build-deps:
|
||||
@command -V find docker skopeo jq gh helm > /dev/null
|
||||
@@ -18,6 +18,7 @@ build: build-deps
|
||||
make -C packages/system/backup-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
|
||||
@@ -25,23 +26,18 @@ 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/platform image
|
||||
make -C packages/core/talos 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
|
||||
(cd packages/core/installer/; helm template -n cozy-installer installer .) > _out/assets/cozystack-installer.yaml
|
||||
(cd packages/core/installer/; helm template --namespace cozy-installer installer .) > _out/assets/cozystack-installer.yaml
|
||||
|
||||
assets:
|
||||
make -C packages/core/installer assets
|
||||
make -C packages/core/talos assets
|
||||
|
||||
test:
|
||||
make -C packages/core/testing apply
|
||||
|
||||
63
api/backups/strategy/v1alpha1/velero_types.go
Normal file
63
api/backups/strategy/v1alpha1/velero_types.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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"`
|
||||
}
|
||||
|
||||
type VeleroStatus struct {
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
@@ -121,3 +121,116 @@ 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
OwningJobNameLabel = thisGroup + "/owned-by.BackupJobName"
|
||||
OwningJobNamespaceLabel = thisGroup + "/owned-by.BackupJobNamespace"
|
||||
)
|
||||
|
||||
// BackupJobPhase represents the lifecycle phase of a BackupJob.
|
||||
type BackupJobPhase string
|
||||
|
||||
@@ -85,6 +90,8 @@ type BackupJobStatus struct {
|
||||
// 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`
|
||||
|
||||
@@ -25,8 +25,13 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
thisGroup = "backups.cozystack.io"
|
||||
thisVersion = "v1alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
GroupVersion = schema.GroupVersion{Group: "backups.cozystack.io", Version: "v1alpha1"}
|
||||
GroupVersion = schema.GroupVersion{Group: thisGroup, Version: thisVersion}
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addGroupVersion)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
@@ -61,24 +62,6 @@ type CozystackResourceDefinitionSpec struct {
|
||||
Dashboard *CozystackResourceDefinitionDashboard `json:"dashboard,omitempty"`
|
||||
}
|
||||
|
||||
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"`
|
||||
@@ -91,9 +74,8 @@ type CozystackResourceDefinitionApplication struct {
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionRelease struct {
|
||||
// Helm chart configuration
|
||||
// +optional
|
||||
Chart CozystackResourceDefinitionChart `json:"chart,omitempty"`
|
||||
// Reference to the chart source
|
||||
ChartRef *helmv2.CrossNamespaceSourceReference `json:"chartRef"`
|
||||
// Labels for the release
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Prefix for the release name
|
||||
|
||||
@@ -68,7 +68,7 @@ type PackageSpec struct {
|
||||
Components map[string]PackageComponent `json:"components,omitempty"`
|
||||
}
|
||||
|
||||
// PackageRelease defines overrides for a specific component
|
||||
// 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
|
||||
@@ -86,4 +86,15 @@ 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"`
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Cluster,shortName={pkgsrc,pkgsrcs}
|
||||
// +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"
|
||||
@@ -81,19 +81,6 @@ type Variant struct {
|
||||
Components []Component `json:"components,omitempty"`
|
||||
}
|
||||
|
||||
// DependencyTarget defines a named group of packages that can be referenced
|
||||
// by other package sources via dependsOn
|
||||
type DependencyTarget struct {
|
||||
// Name is the unique identifier for this dependency target
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// Packages is a list of package names that belong to this target
|
||||
// These packages will be added as dependencies when this target is referenced
|
||||
// +required
|
||||
Packages []string `json:"packages"`
|
||||
}
|
||||
|
||||
// Library defines a Helm library chart
|
||||
type Library struct {
|
||||
// Name is the optional name for library placed in charts
|
||||
|
||||
@@ -21,6 +21,7 @@ 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"
|
||||
@@ -118,22 +119,6 @@ func (in *CozystackResourceDefinitionApplication) DeepCopy() *CozystackResourceD
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
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
|
||||
@@ -205,7 +190,11 @@ func (in *CozystackResourceDefinitionList) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionRelease) DeepCopyInto(out *CozystackResourceDefinitionRelease) {
|
||||
*out = *in
|
||||
out.Chart = in.Chart
|
||||
if in.ChartRef != nil {
|
||||
in, out := &in.ChartRef, &out.ChartRef
|
||||
*out = new(v2.CrossNamespaceSourceReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
@@ -309,21 +298,16 @@ func (in *CozystackResourceDefinitionSpec) DeepCopy() *CozystackResourceDefiniti
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DependencyTarget) DeepCopyInto(out *DependencyTarget) {
|
||||
func (in *DependencyStatus) DeepCopyInto(out *DependencyStatus) {
|
||||
*out = *in
|
||||
if in.Packages != nil {
|
||||
in, out := &in.Packages, &out.Packages
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyTarget.
|
||||
func (in *DependencyTarget) DeepCopy() *DependencyTarget {
|
||||
// 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(DependencyTarget)
|
||||
out := new(DependencyStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
@@ -587,6 +571,13 @@ func (in *PackageStatus) DeepCopyInto(out *PackageStatus) {
|
||||
(*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.
|
||||
@@ -620,21 +611,6 @@ func (in Selector) DeepCopy() Selector {
|
||||
return *out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SourceRef) DeepCopyInto(out *SourceRef) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// 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(SourceRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Variant) DeepCopyInto(out *Variant) {
|
||||
*out = *in
|
||||
|
||||
@@ -35,8 +35,10 @@ import (
|
||||
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
|
||||
)
|
||||
|
||||
@@ -49,6 +51,8 @@ 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
|
||||
}
|
||||
|
||||
@@ -155,6 +159,15 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&backupcontroller.BackupJobReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Recorder: mgr.GetEventRecorderFor("backup-controller"),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "BackupJob")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
|
||||
@@ -147,7 +147,7 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&backupcontroller.BackupJobStrategyReconciler{
|
||||
if err = (&backupcontroller.BackupJobReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
|
||||
@@ -72,6 +72,9 @@ Multiple -f flags can be specified, and they can point to files or directories.`
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -208,25 +211,8 @@ func readPackagesFromYAMLFile(filePath string) ([]string, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to parse as PackageList
|
||||
if obj.GetKind() == "PackageList" {
|
||||
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
|
||||
}
|
||||
|
||||
// Try to parse as PackageSourceList
|
||||
if obj.GetKind() == "PackageSourceList" {
|
||||
// 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 {
|
||||
@@ -243,10 +229,8 @@ func readPackagesFromYAMLFile(filePath string) ([]string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(packages) == 0 {
|
||||
return nil, fmt.Errorf("no valid packages found in file")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -481,33 +465,33 @@ func installPackage(ctx context.Context, k8sClient client.Client, packageSourceN
|
||||
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)
|
||||
// Now create all Package resources
|
||||
for _, pkgName := range installOrder {
|
||||
// Skip if already installed
|
||||
if _, exists := installedMap[pkgName]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil
|
||||
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
|
||||
|
||||
@@ -17,12 +17,15 @@ 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"
|
||||
@@ -51,6 +54,8 @@ Multiple -f flags can be specified, and they can point to files or directories.`
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -63,6 +68,10 @@ Multiple -f flags can be specified, and they can point to files or directories.`
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,12 +104,51 @@ Multiple -f flags can be specified, and they can point to files or directories.`
|
||||
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 packageNames {
|
||||
for _, packageName := range deleteOrder {
|
||||
pkg := &cozyv1alpha1.Package{}
|
||||
pkg.Name = packageName
|
||||
if err := k8sClient.Delete(ctx, pkg); err != nil {
|
||||
if client.IgnoreNotFound(err) == nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
fmt.Fprintf(os.Stderr, "⚠ Package %s not found, skipping\n", packageName)
|
||||
continue
|
||||
}
|
||||
@@ -113,6 +161,233 @@ Multiple -f flags can be specified, and they can point to files or directories.`
|
||||
},
|
||||
}
|
||||
|
||||
// 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)")
|
||||
|
||||
@@ -22,17 +22,17 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"github.com/emicklei/dot"
|
||||
"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/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"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
var dotCmdFlags struct {
|
||||
@@ -95,12 +95,12 @@ Specify packages as arguments or use -f flag to read from files.`,
|
||||
|
||||
// packagesOnly is inverse of components flag (if components=false, then packagesOnly=true)
|
||||
packagesOnly := !dotCmdFlags.components
|
||||
graph, allNodes, err := buildGraphFromCluster(ctx, dotCmdFlags.kubeconfig, packagesOnly, dotCmdFlags.installed, packageName, selectedPackages)
|
||||
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)
|
||||
dotGraph := generateDOTGraph(graph, allNodes, packagesOnly, edgeVariants, packageNames)
|
||||
dotGraph.Write(os.Stdout)
|
||||
|
||||
return nil
|
||||
@@ -110,7 +110,7 @@ Specify packages as arguments or use -f flag to read from files.`,
|
||||
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", true, "show component-level dependencies (default: true)")
|
||||
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)")
|
||||
}
|
||||
@@ -125,7 +125,8 @@ func init() {
|
||||
}
|
||||
|
||||
// buildGraphFromCluster builds a dependency graph from PackageSource resources in the cluster.
|
||||
func buildGraphFromCluster(ctx context.Context, kubeconfig string, packagesOnly bool, installedOnly bool, packageName string, selectedPackages []string) (map[string][]string, map[string]bool, error) {
|
||||
// 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
|
||||
@@ -134,27 +135,27 @@ func buildGraphFromCluster(ctx context.Context, kubeconfig string, packagesOnly
|
||||
// Load kubeconfig from explicit path
|
||||
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load kubeconfig from %s: %w", kubeconfig, err)
|
||||
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, fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
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, fmt.Errorf("failed to create k8s client: %w", err)
|
||||
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 || packageName != "" {
|
||||
if installedOnly {
|
||||
var packageList cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &packageList); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to list Packages: %w", err)
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
for _, pkg := range packageList.Items {
|
||||
installedPackages[pkg.Name] = true
|
||||
@@ -164,11 +165,31 @@ func buildGraphFromCluster(ctx context.Context, kubeconfig string, packagesOnly
|
||||
// List all PackageSource resources
|
||||
var packageSourceList cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &packageSourceList); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
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 {
|
||||
@@ -203,56 +224,245 @@ func buildGraphFromCluster(ctx context.Context, kubeconfig string, packagesOnly
|
||||
|
||||
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
|
||||
// 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
|
||||
}
|
||||
graph[psName] = append(graph[psName], dep)
|
||||
allNodes[dep] = true
|
||||
|
||||
// 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, ".") {
|
||||
graph[componentName] = append(graph[componentName], dep)
|
||||
allNodes[dep] = true
|
||||
// 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
|
||||
// Local component dependency (same package)
|
||||
// Mark that this component has local dependencies
|
||||
componentHasLocalDeps[componentName] = true
|
||||
|
||||
localDep := fmt.Sprintf("%s.%s", psName, dep)
|
||||
graph[componentName] = append(graph[componentName], localDep)
|
||||
allNodes[localDep] = true
|
||||
|
||||
// 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, nil
|
||||
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) *dot.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", "LR")
|
||||
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 && strings.Contains(node, ".") && !strings.HasPrefix(node, "cozystack.") {
|
||||
if packagesOnly && !isPackage(node) {
|
||||
// Skip component nodes when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
@@ -260,41 +470,95 @@ func generateDOTGraph(graph map[string][]string, allNodes map[string]bool, packa
|
||||
n := g.Node(node)
|
||||
|
||||
// Style nodes based on type
|
||||
if strings.Contains(node, ".") && !strings.HasPrefix(node, "cozystack.") {
|
||||
// Component node
|
||||
n.Attr("shape", "box")
|
||||
n.Attr("style", "rounded,filled")
|
||||
n.Attr("fillcolor", "lightyellow")
|
||||
n.Attr("label", strings.Split(node, ".")[len(strings.Split(node, "."))-1])
|
||||
} else {
|
||||
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 && strings.Contains(source, ".") && !strings.HasPrefix(source, "cozystack.") {
|
||||
if packagesOnly && !isPackage(source) {
|
||||
// Skip component edges when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
if packagesOnly && strings.Contains(target, ".") && !strings.HasPrefix(target, "cozystack.") {
|
||||
if packagesOnly && !isPackage(target) {
|
||||
// Skip component edges when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
|
||||
// Only add edge if both nodes exist
|
||||
if allNodes[source] && allNodes[target] {
|
||||
g.Edge(g.Node(source), g.Node(target))
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
@@ -89,49 +90,52 @@ func listPackageSources(ctx context.Context, k8sClient client.Client, showCompon
|
||||
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.Fprintf(os.Stdout, "%-50s %-30s %-10s %s\n", "NAME", "VARIANTS", "READY", "STATUS")
|
||||
fmt.Fprintf(os.Stdout, "%-50s %-30s %-10s %s\n", strings.Repeat("-", 50), strings.Repeat("-", 30), strings.Repeat("-", 10), strings.Repeat("-", 50))
|
||||
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] + "..."
|
||||
}
|
||||
// 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
|
||||
// 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(os.Stdout, "%-50s %-30s %-10s %s\n", ps.Name, variantsStr, ready, status)
|
||||
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(os.Stdout, " %-48s %-30s %-10s %s\n",
|
||||
fmt.Sprintf("%s.%s", ps.Name, component.Name),
|
||||
variant.Name, "", "")
|
||||
}
|
||||
// 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
|
||||
}
|
||||
@@ -142,9 +146,25 @@ func listPackages(ctx context.Context, k8sClient client.Client, showComponents b
|
||||
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.Fprintf(os.Stdout, "%-50s %-20s %-10s %s\n", "NAME", "VARIANT", "READY", "STATUS")
|
||||
fmt.Fprintf(os.Stdout, "%-50s %-20s %-10s %s\n", strings.Repeat("-", 50), strings.Repeat("-", 20), strings.Repeat("-", 10), strings.Repeat("-", 50))
|
||||
fmt.Fprintln(w, "NAME\tVARIANT\tREADY\tSTATUS")
|
||||
|
||||
// Print rows
|
||||
for _, pkg := range pkgList.Items {
|
||||
@@ -167,20 +187,19 @@ func listPackages(ctx context.Context, k8sClient client.Client, showComponents b
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stdout, "%-50s %-20s %-10s %s\n", pkg.Name, variant, ready, status)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", pkg.Name, variant, ready, status)
|
||||
|
||||
// Show components if requested
|
||||
if showComponents {
|
||||
// Get PackageSource to show components
|
||||
ps := &cozyv1alpha1.PackageSource{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: pkg.Name}, ps); err == nil {
|
||||
// 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(os.Stdout, " %-48s %-20s %-10s %s\n",
|
||||
fmt.Fprintf(w, " %s\t%s\t\t\n",
|
||||
fmt.Sprintf("%s.%s", pkg.Name, component.Name),
|
||||
variant, "", "")
|
||||
variant)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -200,22 +200,6 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controller.TenantHelmReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "TenantHelmReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controller.CozystackConfigReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "CozystackConfigReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cozyAPIKind := "DaemonSet"
|
||||
if reconcileDeployment {
|
||||
cozyAPIKind = "Deployment"
|
||||
@@ -229,6 +213,14 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controller.CozystackResourceDefinitionHelmReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionHelmReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dashboardManager := &dashboard.Manager{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
|
||||
@@ -18,10 +18,8 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -34,12 +32,16 @@ import (
|
||||
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"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
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"
|
||||
@@ -47,6 +49,7 @@ import (
|
||||
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/fluxinstall"
|
||||
"github.com/cozystack/cozystack/internal/operator"
|
||||
// +kubebuilder:scaffold:imports
|
||||
@@ -57,18 +60,6 @@ var (
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
// stringSliceFlag is a custom flag type that allows multiple values
|
||||
type stringSliceFlag []string
|
||||
|
||||
func (f *stringSliceFlag) String() string {
|
||||
return strings.Join(*f, ",")
|
||||
}
|
||||
|
||||
func (f *stringSliceFlag) Set(value string) error {
|
||||
*f = append(*f, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
|
||||
@@ -87,9 +78,12 @@ func main() {
|
||||
var enableHTTP2 bool
|
||||
var installFlux bool
|
||||
var cozystackVersion string
|
||||
var installFluxResources stringSliceFlag
|
||||
var platformSource 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.")
|
||||
@@ -101,11 +95,14 @@ func main() {
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
flag.BoolVar(&installFlux, "install-flux", false, "Install Flux components before starting reconcile loop")
|
||||
flag.Var(&installFluxResources, "install-flux-resource", "Install Flux resource (JSON format). Can be specified multiple times. Applied after Flux installation.")
|
||||
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
|
||||
"Version of Cozystack")
|
||||
flag.StringVar(&platformSource, "platform-source", "", "Platform source URL (oci:// or git://). If specified, generates OCIRepository or GitRepository resource.")
|
||||
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-packages", "Name for the generated platform source resource (default: cozystack-packages)")
|
||||
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,
|
||||
@@ -117,10 +114,36 @@ func main() {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Start the controller manager
|
||||
setupLog.Info("Starting 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,
|
||||
@@ -153,37 +176,22 @@ func main() {
|
||||
installCtx, installCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer installCancel()
|
||||
|
||||
// The namespace will be automatically extracted from the embedded manifests
|
||||
if err := fluxinstall.Install(installCtx, mgr.GetClient(), fluxinstall.WriteEmbeddedManifests); err != nil {
|
||||
setupLog.Error(err, "failed to install Flux, continuing anyway")
|
||||
// Don't exit - allow operator to start even if Flux install fails
|
||||
// This allows the operator to work in environments where Flux is already installed
|
||||
} else {
|
||||
setupLog.Info("Flux installation completed successfully")
|
||||
}
|
||||
}
|
||||
|
||||
// Install Flux resources after Flux installation
|
||||
if len(installFluxResources) > 0 {
|
||||
setupLog.Info("Installing Flux resources", "count", len(installFluxResources))
|
||||
installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer installCancel()
|
||||
|
||||
if err := installFluxResourcesFunc(installCtx, mgr.GetClient(), installFluxResources); err != nil {
|
||||
setupLog.Error(err, "failed to install Flux resources, continuing anyway")
|
||||
// Don't exit - allow operator to start even if resource installation fails
|
||||
} else {
|
||||
setupLog.Info("Flux resources installation completed successfully")
|
||||
// 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 platformSource != "" {
|
||||
setupLog.Info("Generating platform source resource", "source", platformSource, "name", platformSourceName)
|
||||
if platformSourceURL != "" {
|
||||
setupLog.Info("Generating platform source resource", "url", platformSourceURL, "name", platformSourceName, "ref", platformSourceRef)
|
||||
installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer installCancel()
|
||||
|
||||
if err := installPlatformSourceResource(installCtx, mgr.GetClient(), platformSource, platformSourceName); err != nil {
|
||||
// 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 {
|
||||
@@ -209,6 +217,18 @@ func main() {
|
||||
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 {
|
||||
@@ -228,120 +248,58 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// installFluxResourcesFunc installs Flux resources from JSON strings
|
||||
func installFluxResourcesFunc(ctx context.Context, k8sClient client.Client, resources []string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
for i, resourceJSON := range resources {
|
||||
logger.Info("Installing Flux resource", "index", i+1, "total", len(resources))
|
||||
|
||||
// Parse JSON into unstructured object
|
||||
var obj unstructured.Unstructured
|
||||
if err := json.Unmarshal([]byte(resourceJSON), &obj.Object); err != nil {
|
||||
return fmt.Errorf("failed to parse resource JSON at index %d: %w", i, err)
|
||||
}
|
||||
|
||||
// Validate that it has required fields
|
||||
if obj.GetAPIVersion() == "" {
|
||||
return fmt.Errorf("resource at index %d missing apiVersion", i)
|
||||
}
|
||||
if obj.GetKind() == "" {
|
||||
return fmt.Errorf("resource at index %d missing kind", i)
|
||||
}
|
||||
if obj.GetName() == "" {
|
||||
return fmt.Errorf("resource at index %d missing metadata.name", i)
|
||||
}
|
||||
|
||||
// Apply the resource (create or update)
|
||||
logger.Info("Applying Flux resource",
|
||||
"apiVersion", obj.GetAPIVersion(),
|
||||
"kind", obj.GetKind(),
|
||||
"name", obj.GetName(),
|
||||
"namespace", obj.GetNamespace(),
|
||||
)
|
||||
|
||||
// Use server-side apply or create/update
|
||||
existing := &unstructured.Unstructured{}
|
||||
existing.SetGroupVersionKind(obj.GroupVersionKind())
|
||||
key := client.ObjectKey{
|
||||
Name: obj.GetName(),
|
||||
Namespace: obj.GetNamespace(),
|
||||
}
|
||||
|
||||
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.GetKind(), obj.GetName(), err)
|
||||
}
|
||||
logger.Info("Created Flux resource", "kind", obj.GetKind(), "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.GetKind(), obj.GetName(), err)
|
||||
}
|
||||
logger.Info("Updated Flux resource", "kind", obj.GetKind(), "name", obj.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 string) error {
|
||||
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, ref, err := parsePlatformSource(sourceURL)
|
||||
sourceType, repoURL, err := parsePlatformSourceURL(sourceURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse platform source URL: %w", err)
|
||||
}
|
||||
|
||||
var obj *unstructured.Unstructured
|
||||
// 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, ref)
|
||||
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, ref)
|
||||
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 git://)", sourceType)
|
||||
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.GetAPIVersion(),
|
||||
"kind", obj.GetKind(),
|
||||
"apiVersion", obj.GetObjectKind().GroupVersionKind().GroupVersion().String(),
|
||||
"kind", obj.GetObjectKind().GroupVersionKind().Kind,
|
||||
"name", obj.GetName(),
|
||||
"namespace", obj.GetNamespace(),
|
||||
)
|
||||
|
||||
existing := &unstructured.Unstructured{}
|
||||
existing.SetGroupVersionKind(obj.GroupVersionKind())
|
||||
key := client.ObjectKey{
|
||||
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.GetKind(), obj.GetName(), err)
|
||||
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.GetKind(), "name", obj.GetName())
|
||||
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)
|
||||
}
|
||||
@@ -349,132 +307,195 @@ func installPlatformSourceResource(ctx context.Context, k8sClient client.Client,
|
||||
// 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.GetKind(), obj.GetName(), err)
|
||||
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.GetKind(), "name", obj.GetName())
|
||||
logger.Info("Updated platform source resource", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsePlatformSource parses the source URL and returns the type, repository URL, and reference
|
||||
// parsePlatformSourceURL parses the source URL and returns the source type and repository URL.
|
||||
// Supports formats:
|
||||
// - oci://registry.example.com/repo@sha256:digest
|
||||
// - oci://registry.example.com/repo (ref will be empty)
|
||||
// - git://github.com/user/repo@branch
|
||||
// - git://github.com/user/repo (ref will default to "main")
|
||||
// - https://github.com/user/repo@branch (treated as git)
|
||||
func parsePlatformSource(sourceURL string) (sourceType, repoURL, ref string, err error) {
|
||||
// Normalize the URL by trimming whitespace
|
||||
// - 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)
|
||||
|
||||
// Check for oci:// prefix
|
||||
if strings.HasPrefix(sourceURL, "oci://") {
|
||||
// Remove oci:// prefix
|
||||
rest := strings.TrimPrefix(sourceURL, "oci://")
|
||||
|
||||
// Check for @sha256: digest (look for @ followed by sha256:)
|
||||
// We need to find the last @ before sha256: to handle paths with @ symbols
|
||||
sha256Idx := strings.Index(rest, "@sha256:")
|
||||
if sha256Idx != -1 {
|
||||
repoURL = "oci://" + rest[:sha256Idx]
|
||||
ref = rest[sha256Idx+1:] // sha256:digest
|
||||
} else {
|
||||
// Check for @ without sha256: (might be a tag)
|
||||
if atIdx := strings.LastIndex(rest, "@"); atIdx != -1 {
|
||||
// Could be a tag, but for OCI we expect sha256: digest
|
||||
// For now, treat everything after @ as the ref
|
||||
repoURL = "oci://" + rest[:atIdx]
|
||||
ref = rest[atIdx+1:]
|
||||
} else {
|
||||
repoURL = "oci://" + rest
|
||||
ref = "" // No digest specified
|
||||
}
|
||||
}
|
||||
return "oci", repoURL, ref, nil
|
||||
return "oci", sourceURL, nil
|
||||
}
|
||||
|
||||
// Check for git:// prefix or treat as git for http/https
|
||||
if strings.HasPrefix(sourceURL, "git://") || strings.HasPrefix(sourceURL, "http://") || strings.HasPrefix(sourceURL, "https://") || strings.HasPrefix(sourceURL, "ssh://") {
|
||||
// Parse URL to extract ref if present
|
||||
parsedURL, err := url.Parse(sourceURL)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
|
||||
// Check for @ref in the path (e.g., git://host/path@branch)
|
||||
path := parsedURL.Path
|
||||
if idx := strings.LastIndex(path, "@"); idx != -1 {
|
||||
repoURL = fmt.Sprintf("%s://%s%s", parsedURL.Scheme, parsedURL.Host, path[:idx])
|
||||
if parsedURL.RawQuery != "" {
|
||||
repoURL += "?" + parsedURL.RawQuery
|
||||
}
|
||||
ref = path[idx+1:]
|
||||
} else {
|
||||
// Default to main branch if no ref specified
|
||||
repoURL = sourceURL
|
||||
ref = "main"
|
||||
}
|
||||
|
||||
// Normalize git:// to https:// for GitRepository
|
||||
if strings.HasPrefix(repoURL, "git://") {
|
||||
repoURL = strings.Replace(repoURL, "git://", "https://", 1)
|
||||
}
|
||||
|
||||
return "git", repoURL, ref, 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:// or git://): %s", sourceURL)
|
||||
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, digest string) (*unstructured.Unstructured, error) {
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetAPIVersion("source.toolkit.fluxcd.io/v1")
|
||||
obj.SetKind("OCIRepository")
|
||||
obj.SetName(name)
|
||||
obj.SetNamespace("cozy-system")
|
||||
|
||||
spec := map[string]interface{}{
|
||||
"interval": "5m0s",
|
||||
"url": repoURL,
|
||||
func generateOCIRepository(name, repoURL string, refMap map[string]string) (*sourcev1.OCIRepository, error) {
|
||||
if err := validateOCIRef(refMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if digest != "" {
|
||||
// Ensure digest starts with sha256:
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
digest = "sha256:" + digest
|
||||
}
|
||||
spec["ref"] = map[string]interface{}{
|
||||
"digest": digest,
|
||||
}
|
||||
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},
|
||||
},
|
||||
}
|
||||
|
||||
if err := unstructured.SetNestedField(obj.Object, spec, "spec"); err != nil {
|
||||
return nil, fmt.Errorf("failed to set spec: %w", err)
|
||||
// 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, ref string) (*unstructured.Unstructured, error) {
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetAPIVersion("source.toolkit.fluxcd.io/v1")
|
||||
obj.SetKind("GitRepository")
|
||||
obj.SetName(name)
|
||||
obj.SetNamespace("cozy-system")
|
||||
func generateGitRepository(name, repoURL string, refMap map[string]string) (*sourcev1.GitRepository, error) {
|
||||
if err := validateGitRef(refMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spec := map[string]interface{}{
|
||||
"interval": "5m0s",
|
||||
"url": repoURL,
|
||||
"ref": map[string]interface{}{
|
||||
"branch": ref,
|
||||
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},
|
||||
},
|
||||
}
|
||||
|
||||
if err := unstructured.SetNestedField(obj.Object, spec, "spec"); err != nil {
|
||||
return nil, fmt.Errorf("failed to set spec: %w", err)
|
||||
// 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
|
||||
|
||||
@@ -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, cozypkg, cozy-proxy) for tags during release period
|
||||
- [ ] **MANDATORY**: Check ALL optional repositories (talm, boot-to-talos, cozyhr, 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/cozypkg](https://github.com/cozystack/cozypkg)
|
||||
- [https://github.com/cozystack/cozyhr](https://github.com/cozystack/cozyhr)
|
||||
- [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, cozypkg, cozy-proxy). Do NOT skip any repository!**
|
||||
**⚠️ MANDATORY: You MUST check ALL optional repositories (talm, boot-to-talos, cozyhr, 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, cozypkg, cozy-proxy)
|
||||
- Check ALL optional repositories (talm, boot-to-talos, cozyhr, 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, cozypkg, cozy-proxy) checked for tags during release period
|
||||
- [ ] Step 6 completed: **ALL** optional repositories (talm, boot-to-talos, cozyhr, 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, cozypkg, 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, cozyhr, 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, 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.
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
@@ -155,6 +155,91 @@ 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
|
||||
|
||||
@@ -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 cozypkg tool
|
||||
- **Developer Experience**: Simplified local development with cozyhr tool
|
||||
|
||||
The platform exposes infrastructure services via the Kubernetes API with ready-made configs, built-in monitoring, and alerts.
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@ https://github.com/cozystack/cozystack/releases/tag/v0.36.2
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
## Security
|
||||
* [vm-disk] New SVG icon for VM disk application. (@kvaps and @kvapsova in https://github.com/cozystack/cozystack/pull/1435)
|
||||
|
||||
## 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
|
||||
|
||||
14
docs/changelogs/v0.38.3.md
Normal file
14
docs/changelogs/v0.38.3.md
Normal file
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
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)
|
||||
|
||||
18
docs/changelogs/v0.38.4.md
Normal file
18
docs/changelogs/v0.38.4.md
Normal file
@@ -0,0 +1,18 @@
|
||||
<!--
|
||||
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)
|
||||
|
||||
95
docs/changelogs/v0.39.0.md
Normal file
95
docs/changelogs/v0.39.0.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 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
|
||||
-->
|
||||
|
||||
12
docs/changelogs/v0.39.1.md
Normal file
12
docs/changelogs/v0.39.1.md
Normal file
@@ -0,0 +1,12 @@
|
||||
<!--
|
||||
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)
|
||||
|
||||
20
examples/desired-backup.yaml
Normal file
20
examples/desired-backup.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: backups.cozystack.io/v1alpha1
|
||||
kind: BackupJob
|
||||
metadata:
|
||||
name: desired-backup
|
||||
namespace: tenant-root
|
||||
labels:
|
||||
backups.cozystack.io/triggered-by: manual
|
||||
spec:
|
||||
applicationRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: VirtualMachine
|
||||
name: vm1
|
||||
storageRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: Bucket
|
||||
name: test-bucket
|
||||
strategyRef:
|
||||
apiGroup: strategy.backups.cozystack.io
|
||||
kind: Velero
|
||||
name: velero-strategy-default
|
||||
41
go.mod
41
go.mod
@@ -7,8 +7,8 @@ go 1.25.0
|
||||
require (
|
||||
github.com/emicklei/dot v1.10.0
|
||||
github.com/fluxcd/helm-controller/api v1.4.3
|
||||
github.com/fluxcd/source-controller/api v1.6.2
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.2
|
||||
github.com/fluxcd/source-controller/api v1.7.4
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/google/gofuzz v1.2.0
|
||||
@@ -17,19 +17,19 @@ require (
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/vmware-tanzu/velero v1.17.1
|
||||
go.uber.org/zap v1.27.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/apiserver v0.34.1
|
||||
k8s.io/client-go v0.34.1
|
||||
k8s.io/component-base v0.34.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
|
||||
sigs.k8s.io/controller-runtime v0.22.2
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0
|
||||
)
|
||||
|
||||
@@ -50,7 +50,7 @@ require (
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.13.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/meta v1.22.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/meta v1.23.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
@@ -81,8 +81,8 @@ require (
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
@@ -91,14 +91,14 @@ require (
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.6.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
@@ -106,21 +106,22 @@ require (
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/oauth2 v0.29.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
google.golang.org/grpc v1.72.1 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/grpc v1.73.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/kms v0.34.1 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
|
||||
78
go.sum
78
go.sum
@@ -43,12 +43,12 @@ github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2T
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.13.0 h1:GGf0UBVRIku+gebY944icVeEIhyg1P/KE3IrhOyJJnE=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.13.0/go.mod h1:TLKVqbtnzkhDuhWnAsN35977HvRfIjs+lgMuNro/LEc=
|
||||
github.com/fluxcd/pkg/apis/meta v1.22.0 h1:EHWQH5ZWml7i8eZ/AMjm1jxid3j/PQ31p+hIwCt6crM=
|
||||
github.com/fluxcd/pkg/apis/meta v1.22.0/go.mod h1:Kc1+bWe5p0doROzuV9XiTfV/oL3ddsemYXt8ZYWdVVg=
|
||||
github.com/fluxcd/source-controller/api v1.6.2 h1:UmodAeqLIeF29HdTqf2GiacZyO+hJydJlepDaYsMvhc=
|
||||
github.com/fluxcd/source-controller/api v1.6.2/go.mod h1:ZJcAi0nemsnBxjVgmJl0WQzNvB0rMETxQMTdoFosmMw=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.2 h1:fWSxsDqYN7My2AEpQwbP7O6Qjix8nGBX+UE/qWHtZfM=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.2/go.mod h1:Hs6ueayPt23jlkIr/d1pGPZ+OHiibQwWjxvU6xqljzg=
|
||||
github.com/fluxcd/pkg/apis/meta v1.23.0 h1:fLis5YcHnOsyKYptzBtituBm5EWNx13I0bXQsy0FG4s=
|
||||
github.com/fluxcd/pkg/apis/meta v1.23.0/go.mod h1:UWsIbBPCxYvoVklr2mV2uLFBf/n17dNAmKFjRfApdDo=
|
||||
github.com/fluxcd/source-controller/api v1.7.4 h1:+EOVnRA9LmLxOx7J273l7IOEU39m+Slt/nQGBy69ygs=
|
||||
github.com/fluxcd/source-controller/api v1.7.4/go.mod h1:ruf49LEgZRBfcP+eshl2n9SX1MfHayCcViAIGnZcaDY=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3 h1:SsVGAaMBxzvcgrOz/Kl6c2ybMHVqoiEFwtI+bDuSeSs=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3/go.mod h1:Nx3QZweVyuhaOtSNrw+oxifG+qrakPvjgNAN9qlUTb0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
@@ -144,10 +144,10 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
@@ -179,6 +179,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
|
||||
github.com/vmware-tanzu/velero v1.17.1 h1:ldKeiTuUwkThOw7zrUucNA1NwnLG66zl13YetWAoE0I=
|
||||
github.com/vmware-tanzu/velero v1.17.1/go.mod h1:3KTxuUN6Un38JzmYAX+8U6j2k6EexGoNNxa8jrJML8U=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk=
|
||||
@@ -201,24 +203,24 @@ go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
|
||||
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -246,8 +248,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -264,8 +266,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -278,14 +280,14 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -320,8 +322,8 @@ k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPG
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4=
|
||||
sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
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 v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
|
||||
@@ -48,7 +48,7 @@ echo " End: $RELEASE_END"
|
||||
echo ""
|
||||
|
||||
# Loop through ALL optional repositories
|
||||
for repo_name in talm boot-to-talos cozypkg cozy-proxy; do
|
||||
for repo_name in talm boot-to-talos cozyhr cozy-proxy; do
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Checking repository: $repo_name"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
226
hack/e2e-apps/backup.bats.disabled
Executable file
226
hack/e2e-apps/backup.bats.disabled
Executable file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
# Test variables - stored for teardown
|
||||
TEST_NAMESPACE='tenant-test'
|
||||
TEST_BUCKET_NAME='test-backup-bucket'
|
||||
TEST_VM_NAME='test-backup-vm'
|
||||
TEST_BACKUPJOB_NAME='test-backup-job'
|
||||
|
||||
teardown() {
|
||||
# Clean up resources (runs even if test fails)
|
||||
namespace="${TEST_NAMESPACE}"
|
||||
bucket_name="${TEST_BUCKET_NAME}"
|
||||
vm_name="${TEST_VM_NAME}"
|
||||
backupjob_name="${TEST_BACKUPJOB_NAME}"
|
||||
|
||||
# Clean up port-forward if still running
|
||||
pkill -f "kubectl.*port-forward.*seaweedfs-s3" 2>/dev/null || true
|
||||
|
||||
# Clean up Velero resources in cozy-velero namespace
|
||||
# Find Velero backup by pattern matching namespace-backupjob
|
||||
for backup in $(kubectl -n cozy-velero get backups.velero.io -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || true); do
|
||||
if echo "$backup" | grep -q "^${namespace}-${backupjob_name}-"; then
|
||||
kubectl -n cozy-velero delete backups.velero.io ${backup} --wait=false 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Clean up BackupStorageLocation and VolumeSnapshotLocation (named: namespace-backupjob)
|
||||
BSL_NAME="${namespace}-${backupjob_name}"
|
||||
kubectl -n cozy-velero delete backupstoragelocations.velero.io ${BSL_NAME} --wait=false 2>/dev/null || true
|
||||
kubectl -n cozy-velero delete volumesnapshotlocations.velero.io ${BSL_NAME} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up Velero credentials secret
|
||||
SECRET_NAME="backup-${namespace}-${backupjob_name}-s3-credentials"
|
||||
kubectl -n cozy-velero delete secret ${SECRET_NAME} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up BackupJob
|
||||
kubectl -n ${namespace} delete backupjob ${backupjob_name} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up Virtual Machine
|
||||
kubectl -n ${namespace} delete virtualmachines.apps.cozystack.io ${vm_name} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up Bucket
|
||||
kubectl -n ${namespace} delete bucket.apps.cozystack.io ${bucket_name} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up temporary files
|
||||
rm -f /tmp/bucket-backup-credentials.json
|
||||
}
|
||||
|
||||
print_log() {
|
||||
echo "# $1" >&3
|
||||
}
|
||||
|
||||
@test "Create Backup for Virtual Machine" {
|
||||
# Test variables
|
||||
bucket_name="${TEST_BUCKET_NAME}"
|
||||
vm_name="${TEST_VM_NAME}"
|
||||
backupjob_name="${TEST_BACKUPJOB_NAME}"
|
||||
namespace="${TEST_NAMESPACE}"
|
||||
|
||||
print_log "Step 0:Ensure BackupJob and Velero strategy CRDs are installed"
|
||||
kubectl apply -f packages/system/backup-controller/definitions/backups.cozystack.io_backupjobs.yaml
|
||||
kubectl apply -f packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_veleroes.yaml
|
||||
# Wait for CRDs to be ready
|
||||
kubectl wait --for condition=established --timeout=30s crd backupjobs.backups.cozystack.io
|
||||
kubectl wait --for condition=established --timeout=30s crd veleroes.strategy.backups.cozystack.io
|
||||
|
||||
# Ensure velero-strategy-default resource exists
|
||||
kubectl apply -f packages/system/backup-controller/templates/strategy.yaml
|
||||
|
||||
print_log "Step 1: Create the bucket resource"
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: apps.cozystack.io/v1alpha1
|
||||
kind: Bucket
|
||||
metadata:
|
||||
name: ${bucket_name}
|
||||
namespace: ${namespace}
|
||||
spec: {}
|
||||
EOF
|
||||
|
||||
print_log "Wait for the bucket to be ready"
|
||||
kubectl -n ${namespace} wait hr bucket-${bucket_name} --timeout=100s --for=condition=ready
|
||||
kubectl -n ${namespace} wait bucketclaims.objectstorage.k8s.io bucket-${bucket_name} --timeout=300s --for=jsonpath='{.status.bucketReady}'=true
|
||||
kubectl -n ${namespace} wait bucketaccesses.objectstorage.k8s.io bucket-${bucket_name} --timeout=300s --for=jsonpath='{.status.accessGranted}'=true
|
||||
|
||||
# Get bucket credentials for later S3 verification
|
||||
kubectl -n ${namespace} get secret bucket-${bucket_name} -ojsonpath='{.data.BucketInfo}' | base64 -d > /tmp/bucket-backup-credentials.json
|
||||
ACCESS_KEY=$(jq -r '.spec.secretS3.accessKeyID' /tmp/bucket-backup-credentials.json)
|
||||
SECRET_KEY=$(jq -r '.spec.secretS3.accessSecretKey' /tmp/bucket-backup-credentials.json)
|
||||
BUCKET_NAME=$(jq -r '.spec.bucketName' /tmp/bucket-backup-credentials.json)
|
||||
|
||||
print_log "Step 2: Create the Virtual Machine"
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: apps.cozystack.io/v1alpha1
|
||||
kind: VirtualMachine
|
||||
metadata:
|
||||
name: ${vm_name}
|
||||
namespace: ${namespace}
|
||||
spec:
|
||||
external: false
|
||||
externalMethod: PortList
|
||||
externalPorts:
|
||||
- 22
|
||||
instanceType: "u1.medium"
|
||||
instanceProfile: ubuntu
|
||||
systemDisk:
|
||||
image: ubuntu
|
||||
storage: 5Gi
|
||||
storageClass: replicated
|
||||
gpus: []
|
||||
resources: {}
|
||||
sshKeys:
|
||||
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF
|
||||
test@test
|
||||
cloudInit: |
|
||||
#cloud-config
|
||||
users:
|
||||
- name: test
|
||||
shell: /bin/bash
|
||||
sudo: ['ALL=(ALL) NOPASSWD: ALL']
|
||||
groups: sudo
|
||||
ssh_authorized_keys:
|
||||
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF test@test
|
||||
cloudInitSeed: ""
|
||||
EOF
|
||||
|
||||
print_log "Wait for VM to be ready"
|
||||
sleep 5
|
||||
kubectl -n ${namespace} wait hr virtual-machine-${vm_name} --timeout=10s --for=condition=ready
|
||||
kubectl -n ${namespace} wait dv virtual-machine-${vm_name} --timeout=150s --for=condition=ready
|
||||
kubectl -n ${namespace} wait pvc virtual-machine-${vm_name} --timeout=100s --for=jsonpath='{.status.phase}'=Bound
|
||||
kubectl -n ${namespace} wait vm virtual-machine-${vm_name} --timeout=100s --for=condition=ready
|
||||
|
||||
print_log "Step 3: Create BackupJob"
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: backups.cozystack.io/v1alpha1
|
||||
kind: BackupJob
|
||||
metadata:
|
||||
name: ${backupjob_name}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
backups.cozystack.io/triggered-by: e2e-test
|
||||
spec:
|
||||
applicationRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: VirtualMachine
|
||||
name: ${vm_name}
|
||||
storageRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: Bucket
|
||||
name: ${bucket_name}
|
||||
strategyRef:
|
||||
apiGroup: strategy.backups.cozystack.io
|
||||
kind: Velero
|
||||
name: velero-strategy-default
|
||||
EOF
|
||||
|
||||
print_log "Wait for BackupJob to start"
|
||||
kubectl -n ${namespace} wait backupjob ${backupjob_name} --timeout=60s --for=jsonpath='{.status.phase}'=Running
|
||||
|
||||
print_log "Wait for BackupJob to complete"
|
||||
kubectl -n ${namespace} wait backupjob ${backupjob_name} --timeout=300s --for=jsonpath='{.status.phase}'=Succeeded
|
||||
|
||||
print_log "Verify BackupJob status"
|
||||
PHASE=$(kubectl -n ${namespace} get backupjob ${backupjob_name} -o jsonpath='{.status.phase}')
|
||||
[ "$PHASE" = "Succeeded" ]
|
||||
|
||||
# Verify BackupJob has a backupRef
|
||||
BACKUP_REF=$(kubectl -n ${namespace} get backupjob ${backupjob_name} -o jsonpath='{.status.backupRef.name}')
|
||||
[ -n "$BACKUP_REF" ]
|
||||
|
||||
# Find the Velero backup by searching for backups matching the namespace-backupjob pattern
|
||||
# Format: namespace-backupjob-timestamp
|
||||
VELERO_BACKUP_NAME=""
|
||||
VELERO_BACKUP_PHASE=""
|
||||
|
||||
print_log "Wait a bit for the backup to be created and appear in the API"
|
||||
sleep 30
|
||||
|
||||
# Find backup by pattern matching namespace-backupjob
|
||||
for backup in $(kubectl -n cozy-velero get backups.velero.io -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
|
||||
if echo "$backup" | grep -q "^${namespace}-${backupjob_name}-"; then
|
||||
VELERO_BACKUP_NAME=$backup
|
||||
VELERO_BACKUP_PHASE=$(kubectl -n cozy-velero get backups.velero.io $backup -o jsonpath='{.status.phase}' 2>/dev/null || echo "")
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
print_log "Verify Velero Backup was found"
|
||||
[ -n "$VELERO_BACKUP_NAME" ]
|
||||
|
||||
echo '# Wait for Velero Backup to complete' >&3
|
||||
until kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' | grep -q 'Completed\|Failed'; do
|
||||
sleep 5
|
||||
done
|
||||
|
||||
print_log "Verify Velero Backup is Completed"
|
||||
timeout 90 sh -ec "until [ \"\$(kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' 2>/dev/null)\" = \"Completed\" ]; do sleep 30; done"
|
||||
|
||||
# Final verification
|
||||
VELERO_BACKUP_PHASE=$(kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' 2>/dev/null || echo "")
|
||||
[ "$VELERO_BACKUP_PHASE" = "Completed" ]
|
||||
|
||||
print_log "Step 4: Verify S3 has backup data"
|
||||
# Start port-forwarding to S3 service (with timeout to keep it alive)
|
||||
bash -c 'timeout 100s kubectl port-forward service/seaweedfs-s3 -n tenant-root 8333:8333 > /dev/null 2>&1 &'
|
||||
|
||||
# Wait for port-forward to be ready
|
||||
timeout 30 sh -ec "until nc -z localhost 8333; do sleep 1; done"
|
||||
|
||||
# Wait a bit for backup data to be written to S3
|
||||
sleep 30
|
||||
|
||||
# Set up MinIO client with insecure flag (use environment variable for all commands)
|
||||
export MC_INSECURE=1
|
||||
mc alias set local https://localhost:8333 $ACCESS_KEY $SECRET_KEY
|
||||
|
||||
# Verify backup directory exists in S3
|
||||
BACKUP_PATH="${BUCKET_NAME}/backups/${VELERO_BACKUP_NAME}"
|
||||
mc ls local/${BACKUP_PATH}/ 2>/dev/null
|
||||
[ $? -eq 0 ]
|
||||
|
||||
# Verify backup files exist (at least metadata files)
|
||||
BACKUP_FILES=$(mc ls local/${BACKUP_PATH}/ 2>/dev/null | wc -l || echo "0")
|
||||
[ "$BACKUP_FILES" -gt "0" ]
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ EOF
|
||||
kubectl wait --for=condition=TenantControlPlaneCreated kamajicontrolplane -n tenant-test kubernetes-${test_name} --timeout=4m
|
||||
|
||||
# Wait for Kubernetes resources to be ready (timeout after 2 minutes)
|
||||
kubectl wait tcp -n tenant-test kubernetes-${test_name} --timeout=2m --for=jsonpath='{.status.kubernetesResources.version.status}'=Ready
|
||||
kubectl wait tcp -n tenant-test kubernetes-${test_name} --timeout=5m --for=jsonpath='{.status.kubernetesResources.version.status}'=Ready
|
||||
|
||||
# Wait for all required deployments to be available (timeout after 4 minutes)
|
||||
kubectl wait deploy --timeout=4m --for=condition=available -n tenant-test kubernetes-${test_name} kubernetes-${test_name}-cluster-autoscaler kubernetes-${test_name}-kccm kubernetes-${test_name}-kcsi-controller
|
||||
@@ -87,7 +87,7 @@ EOF
|
||||
|
||||
|
||||
# Set up port forwarding to the Kubernetes API server for a 200 second timeout
|
||||
bash -c 'timeout 300s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &'
|
||||
bash -c 'timeout 500s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &'
|
||||
# Verify the Kubernetes version matches what we expect (retry for up to 20 seconds)
|
||||
timeout 20 sh -ec 'until kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' version 2>/dev/null | grep -Fq "Server Version: ${k8s_version}"; do sleep 5; done'
|
||||
|
||||
@@ -124,6 +124,100 @@ EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
kubectl --kubeconfig tenantkubeconfig-${test_name} apply -f - <<EOF
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: tenant-test
|
||||
EOF
|
||||
|
||||
# Backend 1
|
||||
kubectl apply --kubeconfig tenantkubeconfig-${test_name} -f- <<EOF
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: "${test_name}-backend"
|
||||
namespace: tenant-test
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: backend
|
||||
backend: "${test_name}-backend"
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: backend
|
||||
backend: "${test_name}-backend"
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- containerPort: 80
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 2
|
||||
periodSeconds: 2
|
||||
EOF
|
||||
|
||||
# LoadBalancer Service
|
||||
kubectl apply --kubeconfig tenantkubeconfig-${test_name} -f- <<EOF
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: "${test_name}-backend"
|
||||
namespace: tenant-test
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
selector:
|
||||
app: backend
|
||||
backend: "${test_name}-backend"
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 80
|
||||
EOF
|
||||
|
||||
# Wait for pods readiness
|
||||
kubectl wait deployment --kubeconfig tenantkubeconfig-${test_name} ${test_name}-backend -n tenant-test --for=condition=Available --timeout=90s
|
||||
|
||||
# Wait for LoadBalancer to be provisioned (IP or hostname)
|
||||
timeout 90 sh -ec "
|
||||
until kubectl get svc ${test_name}-backend --kubeconfig tenantkubeconfig-${test_name} -n tenant-test \
|
||||
-o jsonpath='{.status.loadBalancer.ingress[0]}' | grep -q .; do
|
||||
sleep 5
|
||||
done
|
||||
"
|
||||
|
||||
LB_ADDR=$(
|
||||
kubectl get svc --kubeconfig tenantkubeconfig-${test_name} "${test_name}-backend" \
|
||||
-n tenant-test \
|
||||
-o jsonpath='{.status.loadBalancer.ingress[0].ip}{.status.loadBalancer.ingress[0].hostname}'
|
||||
)
|
||||
|
||||
if [ -z "$LB_ADDR" ]; then
|
||||
echo "LoadBalancer address is empty" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for i in $(seq 1 20); do
|
||||
echo "Attempt $i"
|
||||
curl --silent --fail "http://${LB_ADDR}" && break
|
||||
sleep 3
|
||||
done
|
||||
|
||||
if [ "$i" -eq 20 ]; then
|
||||
echo "LoadBalancer not reachable" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
kubectl delete deployment --kubeconfig tenantkubeconfig-${test_name} "${test_name}-backend" -n tenant-test
|
||||
kubectl delete service --kubeconfig tenantkubeconfig-${test_name} "${test_name}-backend" -n tenant-test
|
||||
|
||||
# Wait for all machine deployment replicas to be ready (timeout after 10 minutes)
|
||||
kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=10m --for=jsonpath='{.status.v1beta2.readyReplicas}'=2
|
||||
|
||||
|
||||
@@ -24,9 +24,8 @@ API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${SCRIPT_ROOT}/api/api-ru
|
||||
UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}"
|
||||
CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4"
|
||||
TMPDIR=$(mktemp -d)
|
||||
OPERATOR_CRDDIR=packages/core/installer/crds
|
||||
BACKUPSTRATEGY_CRDDIR=packages/system/backupstrategy-controller/definitions
|
||||
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/crds
|
||||
OPERATOR_CRDDIR=packages/core/installer/definitions
|
||||
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/definitions
|
||||
COZY_RD_CRDDIR=packages/system/cozystack-resource-definition-crd/definition
|
||||
BACKUPS_CORE_CRDDIR=packages/system/backup-controller/definitions
|
||||
BACKUPSTRATEGY_CRDDIR=packages/system/backupstrategy-controller/definitions
|
||||
|
||||
@@ -8,7 +8,6 @@ need yq; need jq; need base64
|
||||
CHART_YAML="${CHART_YAML:-Chart.yaml}"
|
||||
VALUES_YAML="${VALUES_YAML:-values.yaml}"
|
||||
SCHEMA_JSON="${SCHEMA_JSON:-values.schema.json}"
|
||||
CRD_DIR="../../system/cozystack-resource-definitions/cozyrds"
|
||||
|
||||
[[ -f "$CHART_YAML" ]] || { echo "No $CHART_YAML found"; exit 1; }
|
||||
[[ -f "$SCHEMA_JSON" ]] || { echo "No $SCHEMA_JSON found"; exit 1; }
|
||||
@@ -22,6 +21,8 @@ if [[ -z "$NAME" ]]; then
|
||||
echo "Chart.yaml: .name is empty"; exit 1
|
||||
fi
|
||||
|
||||
CRD_DIR="../../system/${NAME}-rd/cozyrds"
|
||||
|
||||
# Resolve icon path
|
||||
# Accepts:
|
||||
# /logos/foo.svg -> ./logos/foo.svg
|
||||
|
||||
93
internal/backupcontroller/backupjob_controller.go
Normal file
93
internal/backupcontroller/backupjob_controller.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/record"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
)
|
||||
|
||||
// BackupVeleroStrategyReconciler reconciles BackupJob with a strategy referencing
|
||||
// Velero.strategy.backups.cozystack.io objects.
|
||||
type BackupJobReconciler struct {
|
||||
client.Client
|
||||
dynamic.Interface
|
||||
meta.RESTMapper
|
||||
Scheme *runtime.Scheme
|
||||
Recorder record.EventRecorder
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.Info("reconciling BackupJob", "namespace", req.Namespace, "name", req.Name)
|
||||
|
||||
j := &backupsv1alpha1.BackupJob{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: req.Name}, j)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
logger.V(1).Info("BackupJob not found, skipping")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
logger.Error(err, "failed to get BackupJob")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if j.Spec.StrategyRef.APIGroup == nil {
|
||||
logger.V(1).Info("BackupJob has nil StrategyRef.APIGroup, skipping", "backupjob", j.Name)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if *j.Spec.StrategyRef.APIGroup != strategyv1alpha1.GroupVersion.Group {
|
||||
logger.V(1).Info("BackupJob StrategyRef.APIGroup doesn't match, skipping",
|
||||
"backupjob", j.Name,
|
||||
"expected", strategyv1alpha1.GroupVersion.Group,
|
||||
"got", *j.Spec.StrategyRef.APIGroup)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
logger.Info("processing BackupJob", "backupjob", j.Name, "strategyKind", j.Spec.StrategyRef.Kind)
|
||||
switch j.Spec.StrategyRef.Kind {
|
||||
case strategyv1alpha1.JobStrategyKind:
|
||||
return r.reconcileJob(ctx, j)
|
||||
case strategyv1alpha1.VeleroStrategyKind:
|
||||
return r.reconcileVelero(ctx, j)
|
||||
default:
|
||||
logger.V(1).Info("BackupJob StrategyRef.Kind not supported, skipping",
|
||||
"backupjob", j.Name,
|
||||
"kind", j.Spec.StrategyRef.Kind,
|
||||
"supported", []string{strategyv1alpha1.JobStrategyKind, strategyv1alpha1.VeleroStrategyKind})
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetupWithManager registers our controller with the Manager and sets up watches.
|
||||
func (r *BackupJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
cfg := mgr.GetConfig()
|
||||
var err error
|
||||
if r.Interface, err = dynamic.NewForConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
var h *http.Client
|
||||
if h, err = rest.HTTPClientFor(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.RESTMapper, err = apiutil.NewDynamicRESTMapper(cfg, h); err != nil {
|
||||
return err
|
||||
}
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&backupsv1alpha1.BackupJob{}).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -3,29 +3,13 @@ package backupcontroller
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
)
|
||||
|
||||
// BackupJobStrategyReconciler reconciles BackupJob with a strategy referencing
|
||||
// Job.strategy.backups.cozystack.io objects.
|
||||
type BackupJobStrategyReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
func (r *BackupJobStrategyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
func (r *BackupJobReconciler) reconcileJob(ctx context.Context, j *backupsv1alpha1.BackupJob) (ctrl.Result, error) {
|
||||
_ = log.FromContext(ctx)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager registers our controller with the Manager and sets up watches.
|
||||
func (r *BackupJobStrategyReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&backupsv1alpha1.BackupJob{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
627
internal/backupcontroller/velerostrategy_controller.go
Normal file
627
internal/backupcontroller/velerostrategy_controller.go
Normal file
@@ -0,0 +1,627 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/template"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
func getLogger(ctx context.Context) loggerWithDebug {
|
||||
return loggerWithDebug{Logger: log.FromContext(ctx)}
|
||||
}
|
||||
|
||||
// loggerWithDebug wraps a logr.Logger and provides a Debug() method
|
||||
// that maps to V(1).Info() for convenience.
|
||||
type loggerWithDebug struct {
|
||||
logr.Logger
|
||||
}
|
||||
|
||||
// Debug logs at debug level (equivalent to V(1).Info())
|
||||
func (l loggerWithDebug) Debug(msg string, keysAndValues ...interface{}) {
|
||||
l.Logger.V(1).Info(msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// S3Credentials holds the discovered S3 credentials from a Bucket storageRef
|
||||
type S3Credentials struct {
|
||||
BucketName string
|
||||
Endpoint string
|
||||
Region string
|
||||
AccessKeyID string
|
||||
AccessSecretKey string
|
||||
}
|
||||
|
||||
// bucketInfo represents the structure of BucketInfo stored in the secret
|
||||
type bucketInfo struct {
|
||||
Spec struct {
|
||||
BucketName string `json:"bucketName"`
|
||||
SecretS3 struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
AccessKeyID string `json:"accessKeyID"`
|
||||
AccessSecretKey string `json:"accessSecretKey"`
|
||||
} `json:"secretS3"`
|
||||
} `json:"spec"`
|
||||
}
|
||||
|
||||
const (
|
||||
defaultRequeueAfter = 5 * time.Second
|
||||
defaultActiveJobPollingInterval = defaultRequeueAfter
|
||||
// Velero requires API objects and secrets to be in the cozy-velero namespace
|
||||
veleroNamespace = "cozy-velero"
|
||||
virtualMachinePrefix = "virtual-machine-"
|
||||
)
|
||||
|
||||
func storageS3SecretName(namespace, backupJobName string) string {
|
||||
return fmt.Sprintf("backup-%s-%s-s3-credentials", namespace, backupJobName)
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) reconcileVelero(ctx context.Context, j *backupsv1alpha1.BackupJob) (ctrl.Result, error) {
|
||||
logger := getLogger(ctx)
|
||||
logger.Debug("reconciling Velero strategy", "backupjob", j.Name, "phase", j.Status.Phase)
|
||||
|
||||
// If already completed, no need to reconcile
|
||||
if j.Status.Phase == backupsv1alpha1.BackupJobPhaseSucceeded ||
|
||||
j.Status.Phase == backupsv1alpha1.BackupJobPhaseFailed {
|
||||
logger.Debug("BackupJob already completed, skipping", "phase", j.Status.Phase)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Step 1: On first reconcile, set startedAt (but not phase yet - phase will be set after backup creation)
|
||||
logger.Debug("checking BackupJob status", "startedAt", j.Status.StartedAt, "phase", j.Status.Phase)
|
||||
if j.Status.StartedAt == nil {
|
||||
logger.Debug("setting BackupJob StartedAt")
|
||||
now := metav1.Now()
|
||||
j.Status.StartedAt = &now
|
||||
// Don't set phase to Running yet - will be set after Velero backup is successfully created
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("set BackupJob StartedAt", "startedAt", j.Status.StartedAt)
|
||||
} else {
|
||||
logger.Debug("BackupJob already started", "startedAt", j.Status.StartedAt, "phase", j.Status.Phase)
|
||||
}
|
||||
|
||||
// Step 2: Resolve inputs - Read Strategy, Storage, Application, optionally Plan
|
||||
logger.Debug("fetching Velero strategy", "strategyName", j.Spec.StrategyRef.Name)
|
||||
veleroStrategy := &strategyv1alpha1.Velero{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: j.Spec.StrategyRef.Name}, veleroStrategy); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logger.Error(err, "Velero strategy not found", "strategyName", j.Spec.StrategyRef.Name)
|
||||
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("Velero strategy not found: %s", j.Spec.StrategyRef.Name))
|
||||
}
|
||||
logger.Error(err, "failed to get Velero strategy")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("fetched Velero strategy", "strategyName", veleroStrategy.Name)
|
||||
|
||||
// Step 3: Execute backup logic
|
||||
// Check if we already created a Velero Backup
|
||||
// Use human-readable timestamp: YYYY-MM-DD-HH-MM-SS
|
||||
if j.Status.StartedAt == nil {
|
||||
logger.Error(nil, "StartedAt is nil after status update, this should not happen")
|
||||
return ctrl.Result{RequeueAfter: defaultRequeueAfter}, nil
|
||||
}
|
||||
logger.Debug("checking for existing Velero Backup", "namespace", veleroNamespace)
|
||||
veleroBackupList := &velerov1.BackupList{}
|
||||
opts := []client.ListOption{
|
||||
client.InNamespace(veleroNamespace),
|
||||
client.MatchingLabels{
|
||||
backupsv1alpha1.OwningJobNamespaceLabel: j.Namespace,
|
||||
backupsv1alpha1.OwningJobNameLabel: j.Name,
|
||||
},
|
||||
}
|
||||
|
||||
if err := r.List(ctx, veleroBackupList, opts...); err != nil {
|
||||
logger.Error(err, "failed to get Velero Backup")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if len(veleroBackupList.Items) == 0 {
|
||||
// Create Velero Backup
|
||||
logger.Debug("Velero Backup not found, creating new one")
|
||||
if err := r.createVeleroBackup(ctx, j, veleroStrategy); err != nil {
|
||||
logger.Error(err, "failed to create Velero Backup")
|
||||
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("failed to create Velero Backup: %v", err))
|
||||
}
|
||||
// After successful Velero backup creation, set phase to Running
|
||||
if j.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning {
|
||||
logger.Debug("setting BackupJob phase to Running after successful Velero backup creation")
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseRunning
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob phase to Running")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
logger.Debug("created Velero Backup, requeuing")
|
||||
// Requeue to check status
|
||||
return ctrl.Result{RequeueAfter: defaultRequeueAfter}, nil
|
||||
}
|
||||
|
||||
if len(veleroBackupList.Items) > 1 {
|
||||
logger.Error(fmt.Errorf("too many Velero backups for BackupJob"), "found more than one Velero Backup referencing a single BackupJob as owner")
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseFailed
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status")
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
veleroBackup := veleroBackupList.Items[0].DeepCopy()
|
||||
logger.Debug("found existing Velero Backup", "phase", veleroBackup.Status.Phase)
|
||||
|
||||
// If Velero backup exists but phase is not Running, set it to Running
|
||||
// This handles the case where the backup was created but phase wasn't set yet
|
||||
if j.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning {
|
||||
logger.Debug("setting BackupJob phase to Running (Velero backup already exists)")
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseRunning
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob phase to Running")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check Velero Backup status
|
||||
phase := string(veleroBackup.Status.Phase)
|
||||
if phase == "" {
|
||||
// Still in progress, requeue
|
||||
return ctrl.Result{RequeueAfter: defaultActiveJobPollingInterval}, nil
|
||||
}
|
||||
|
||||
// Step 4: On success - Create Backup resource and update status
|
||||
if phase == "Completed" {
|
||||
// Check if we already created the Backup resource
|
||||
if j.Status.BackupRef == nil {
|
||||
backup, err := r.createBackupResource(ctx, j, veleroBackup)
|
||||
if err != nil {
|
||||
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("failed to create Backup resource: %v", err))
|
||||
}
|
||||
|
||||
now := metav1.Now()
|
||||
j.Status.BackupRef = &corev1.LocalObjectReference{Name: backup.Name}
|
||||
j.Status.CompletedAt = &now
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseSucceeded
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("BackupJob succeeded", "backup", backup.Name)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Step 5: On failure
|
||||
if phase == "Failed" || phase == "PartiallyFailed" {
|
||||
message := fmt.Sprintf("Velero Backup failed with phase: %s", phase)
|
||||
if len(veleroBackup.Status.ValidationErrors) > 0 {
|
||||
message = fmt.Sprintf("%s: %v", message, veleroBackup.Status.ValidationErrors)
|
||||
}
|
||||
return r.markBackupJobFailed(ctx, j, message)
|
||||
}
|
||||
|
||||
// Still in progress (InProgress, New, etc.)
|
||||
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
|
||||
}
|
||||
|
||||
// resolveBucketStorageRef discovers S3 credentials from a Bucket storageRef
|
||||
// It follows this flow:
|
||||
// 1. Get the Bucket resource (apps.cozystack.io/v1alpha1)
|
||||
// 2. Find the BucketAccess that references this bucket
|
||||
// 3. Get the secret from BucketAccess.spec.credentialsSecretName
|
||||
// 4. Decode BucketInfo from secret.data.BucketInfo and extract S3 credentials
|
||||
func (r *BackupJobReconciler) resolveBucketStorageRef(ctx context.Context, storageRef corev1.TypedLocalObjectReference, namespace string) (*S3Credentials, error) {
|
||||
logger := getLogger(ctx)
|
||||
|
||||
// Step 1: Get the Bucket resource
|
||||
bucket := &unstructured.Unstructured{}
|
||||
bucket.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: *storageRef.APIGroup,
|
||||
Version: "v1alpha1",
|
||||
Kind: storageRef.Kind,
|
||||
})
|
||||
|
||||
if *storageRef.APIGroup != "apps.cozystack.io" {
|
||||
return nil, fmt.Errorf("Unsupported storage APIGroup: %v, expected apps.cozystack.io", storageRef.APIGroup)
|
||||
}
|
||||
bucketKey := client.ObjectKey{Namespace: namespace, Name: storageRef.Name}
|
||||
|
||||
if err := r.Get(ctx, bucketKey, bucket); err != nil {
|
||||
return nil, fmt.Errorf("failed to get Bucket %s: %w", storageRef.Name, err)
|
||||
}
|
||||
|
||||
// Step 2: Determine the bucket claim name
|
||||
// For apps.cozystack.io Bucket, the BucketClaim name is typically the same as the Bucket name
|
||||
// or follows a pattern. Based on the templates, it's usually the Release.Name which equals the Bucket name
|
||||
bucketName := storageRef.Name
|
||||
|
||||
// Step 3: Get BucketAccess by name (assuming BucketAccess name matches bucketName)
|
||||
bucketAccess := &unstructured.Unstructured{}
|
||||
bucketAccess.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: "objectstorage.k8s.io",
|
||||
Version: "v1alpha1",
|
||||
Kind: "BucketAccess",
|
||||
})
|
||||
|
||||
bucketAccessKey := client.ObjectKey{Name: "bucket-" + bucketName, Namespace: namespace}
|
||||
if err := r.Get(ctx, bucketAccessKey, bucketAccess); err != nil {
|
||||
return nil, fmt.Errorf("failed to get BucketAccess %s in namespace %s: %w", bucketName, namespace, err)
|
||||
}
|
||||
|
||||
// Step 4: Get the secret name from BucketAccess
|
||||
secretName, found, err := unstructured.NestedString(bucketAccess.Object, "spec", "credentialsSecretName")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get credentialsSecretName from BucketAccess: %w", err)
|
||||
}
|
||||
if !found || secretName == "" {
|
||||
return nil, fmt.Errorf("credentialsSecretName not found in BucketAccess %s", bucketAccessKey.Name)
|
||||
}
|
||||
|
||||
// Step 5: Get the secret
|
||||
secret := &corev1.Secret{}
|
||||
secretKey := client.ObjectKey{Namespace: namespace, Name: secretName}
|
||||
if err := r.Get(ctx, secretKey, secret); err != nil {
|
||||
return nil, fmt.Errorf("failed to get secret %s: %w", secretName, err)
|
||||
}
|
||||
|
||||
// Step 6: Decode BucketInfo from secret.data.BucketInfo
|
||||
bucketInfoData, found := secret.Data["BucketInfo"]
|
||||
if !found {
|
||||
return nil, fmt.Errorf("BucketInfo key not found in secret %s", secretName)
|
||||
}
|
||||
|
||||
// Parse JSON value
|
||||
var info bucketInfo
|
||||
if err := json.Unmarshal(bucketInfoData, &info); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal BucketInfo from secret %s: %w", secretName, err)
|
||||
}
|
||||
|
||||
// Step 7: Extract and return S3 credentials
|
||||
creds := &S3Credentials{
|
||||
BucketName: info.Spec.BucketName,
|
||||
Endpoint: info.Spec.SecretS3.Endpoint,
|
||||
Region: info.Spec.SecretS3.Region,
|
||||
AccessKeyID: info.Spec.SecretS3.AccessKeyID,
|
||||
AccessSecretKey: info.Spec.SecretS3.AccessSecretKey,
|
||||
}
|
||||
|
||||
logger.Debug("resolved S3 credentials from Bucket storageRef",
|
||||
"bucket", storageRef.Name,
|
||||
"bucketName", creds.BucketName,
|
||||
"endpoint", creds.Endpoint)
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
// createS3CredsForVelero creates or updates a Kubernetes Secret containing
|
||||
// Velero S3 credentials in the format expected by Velero's cloud-credentials plugin.
|
||||
func (r *BackupJobReconciler) createS3CredsForVelero(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, creds *S3Credentials) error {
|
||||
logger := getLogger(ctx)
|
||||
secretName := storageS3SecretName(backupJob.Namespace, backupJob.Name)
|
||||
secretNamespace := veleroNamespace
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: secretNamespace,
|
||||
},
|
||||
Type: corev1.SecretTypeOpaque,
|
||||
StringData: map[string]string{
|
||||
"cloud": fmt.Sprintf(`[default]
|
||||
aws_access_key_id=%s
|
||||
aws_secret_access_key=%s
|
||||
|
||||
services = seaweed-s3
|
||||
[services seaweed-s3]
|
||||
s3 =
|
||||
endpoint_url = %s
|
||||
`, creds.AccessKeyID, creds.AccessSecretKey, creds.Endpoint),
|
||||
},
|
||||
}
|
||||
|
||||
foundSecret := &corev1.Secret{}
|
||||
secretKey := client.ObjectKey{Name: secretName, Namespace: secretNamespace}
|
||||
err := r.Get(ctx, secretKey, foundSecret)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// Create the Secret
|
||||
if err := r.Create(ctx, secret); err != nil {
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "SecretCreationFailed",
|
||||
fmt.Sprintf("Failed to create Velero credentials secret %s/%s: %v", secretNamespace, secretName, err))
|
||||
return fmt.Errorf("failed to create Velero credentials secret: %w", err)
|
||||
}
|
||||
logger.Debug("created Velero credentials secret", "secret", secretName)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "SecretCreated",
|
||||
fmt.Sprintf("Created Velero credentials secret %s/%s", secretNamespace, secretName))
|
||||
} else if err == nil {
|
||||
// Update if necessary - only update if the secret data has actually changed
|
||||
// Compare the new secret data with existing secret data
|
||||
existingData := foundSecret.Data
|
||||
if existingData == nil {
|
||||
existingData = make(map[string][]byte)
|
||||
}
|
||||
newData := make(map[string][]byte)
|
||||
for k, v := range secret.StringData {
|
||||
newData[k] = []byte(v)
|
||||
}
|
||||
|
||||
// Check if data has changed
|
||||
dataChanged := false
|
||||
if len(existingData) != len(newData) {
|
||||
dataChanged = true
|
||||
} else {
|
||||
for k, newVal := range newData {
|
||||
existingVal, exists := existingData[k]
|
||||
if !exists || !reflect.DeepEqual(existingVal, newVal) {
|
||||
dataChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dataChanged {
|
||||
foundSecret.StringData = secret.StringData
|
||||
foundSecret.Data = nil // Clear .Data so .StringData will be used
|
||||
if err := r.Update(ctx, foundSecret); err != nil {
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "SecretUpdateFailed",
|
||||
fmt.Sprintf("Failed to update Velero credentials secret %s/%s: %v", secretNamespace, secretName, err))
|
||||
return fmt.Errorf("failed to update Velero credentials secret: %w", err)
|
||||
}
|
||||
logger.Debug("updated Velero credentials secret", "secret", secretName)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "SecretUpdated",
|
||||
fmt.Sprintf("Updated Velero credentials secret %s/%s", secretNamespace, secretName))
|
||||
} else {
|
||||
logger.Debug("Velero credentials secret data unchanged, skipping update", "secret", secretName)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error checking for existing Velero credentials secret: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createBackupStorageLocation creates or updates a Velero BackupStorageLocation resource.
|
||||
func (r *BackupJobReconciler) createBackupStorageLocation(ctx context.Context, bsl *velerov1.BackupStorageLocation) error {
|
||||
logger := getLogger(ctx)
|
||||
foundBSL := &velerov1.BackupStorageLocation{}
|
||||
bslKey := client.ObjectKey{Name: bsl.Name, Namespace: bsl.Namespace}
|
||||
|
||||
err := r.Get(ctx, bslKey, foundBSL)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// Create the BackupStorageLocation
|
||||
if err := r.Create(ctx, bsl); err != nil {
|
||||
return fmt.Errorf("failed to create BackupStorageLocation: %w", err)
|
||||
}
|
||||
logger.Debug("created BackupStorageLocation", "name", bsl.Name, "namespace", bsl.Namespace)
|
||||
} else if err == nil {
|
||||
// Update if necessary - use patch to avoid conflicts with Velero's status updates
|
||||
// Only update if the spec has actually changed
|
||||
if !reflect.DeepEqual(foundBSL.Spec, bsl.Spec) {
|
||||
// Retry on conflict since Velero may be updating status concurrently
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := r.Get(ctx, bslKey, foundBSL); err != nil {
|
||||
return fmt.Errorf("failed to get BackupStorageLocation for update: %w", err)
|
||||
}
|
||||
foundBSL.Spec = bsl.Spec
|
||||
if err := r.Update(ctx, foundBSL); err != nil {
|
||||
if errors.IsConflict(err) && i < 2 {
|
||||
logger.Debug("conflict updating BackupStorageLocation, retrying", "attempt", i+1)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to update BackupStorageLocation: %w", err)
|
||||
}
|
||||
logger.Debug("updated BackupStorageLocation", "name", bsl.Name, "namespace", bsl.Namespace)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
logger.Debug("BackupStorageLocation spec unchanged, skipping update", "name", bsl.Name, "namespace", bsl.Namespace)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error checking for existing BackupStorageLocation: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createVolumeSnapshotLocation creates or updates a Velero VolumeSnapshotLocation resource.
|
||||
func (r *BackupJobReconciler) createVolumeSnapshotLocation(ctx context.Context, vsl *velerov1.VolumeSnapshotLocation) error {
|
||||
logger := getLogger(ctx)
|
||||
foundVSL := &velerov1.VolumeSnapshotLocation{}
|
||||
vslKey := client.ObjectKey{Name: vsl.Name, Namespace: vsl.Namespace}
|
||||
|
||||
err := r.Get(ctx, vslKey, foundVSL)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// Create the VolumeSnapshotLocation
|
||||
if err := r.Create(ctx, vsl); err != nil {
|
||||
return fmt.Errorf("failed to create VolumeSnapshotLocation: %w", err)
|
||||
}
|
||||
logger.Debug("created VolumeSnapshotLocation", "name", vsl.Name, "namespace", vsl.Namespace)
|
||||
} else if err == nil {
|
||||
// Update if necessary - only update if the spec has actually changed
|
||||
if !reflect.DeepEqual(foundVSL.Spec, vsl.Spec) {
|
||||
// Retry on conflict since Velero may be updating status concurrently
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := r.Get(ctx, vslKey, foundVSL); err != nil {
|
||||
return fmt.Errorf("failed to get VolumeSnapshotLocation for update: %w", err)
|
||||
}
|
||||
foundVSL.Spec = vsl.Spec
|
||||
if err := r.Update(ctx, foundVSL); err != nil {
|
||||
if errors.IsConflict(err) && i < 2 {
|
||||
logger.Debug("conflict updating VolumeSnapshotLocation, retrying", "attempt", i+1)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to update VolumeSnapshotLocation: %w", err)
|
||||
}
|
||||
logger.Debug("updated VolumeSnapshotLocation", "name", vsl.Name, "namespace", vsl.Namespace)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
logger.Debug("VolumeSnapshotLocation spec unchanged, skipping update", "name", vsl.Name, "namespace", vsl.Namespace)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error checking for existing VolumeSnapshotLocation: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) markBackupJobFailed(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, message string) (ctrl.Result, error) {
|
||||
logger := getLogger(ctx)
|
||||
now := metav1.Now()
|
||||
backupJob.Status.CompletedAt = &now
|
||||
backupJob.Status.Phase = backupsv1alpha1.BackupJobPhaseFailed
|
||||
backupJob.Status.Message = message
|
||||
|
||||
// Add condition
|
||||
backupJob.Status.Conditions = append(backupJob.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "BackupFailed",
|
||||
Message: message,
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
if err := r.Status().Update(ctx, backupJob); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status to Failed")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("BackupJob failed", "message", message)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) createVeleroBackup(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, strategy *strategyv1alpha1.Velero) error {
|
||||
logger := getLogger(ctx)
|
||||
logger.Debug("createVeleroBackup called", "strategy", strategy.Name)
|
||||
|
||||
mapping, err := r.RESTMapping(schema.GroupKind{Group: *backupJob.Spec.ApplicationRef.APIGroup, Kind: backupJob.Spec.ApplicationRef.Kind})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ns := backupJob.Namespace
|
||||
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
|
||||
ns = ""
|
||||
}
|
||||
app, err := r.Resource(mapping.Resource).Namespace(ns).Get(ctx, backupJob.Spec.ApplicationRef.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
veleroBackupSpec, err := template.Template(&strategy.Spec.Template.Spec, app.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
veleroBackup := &velerov1.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: fmt.Sprintf("%s.%s-", backupJob.Namespace, backupJob.Name),
|
||||
Namespace: veleroNamespace,
|
||||
Labels: map[string]string{
|
||||
backupsv1alpha1.OwningJobNameLabel: backupJob.Name,
|
||||
backupsv1alpha1.OwningJobNamespaceLabel: backupJob.Namespace,
|
||||
},
|
||||
},
|
||||
Spec: *veleroBackupSpec,
|
||||
}
|
||||
name := veleroBackup.GenerateName
|
||||
if err := r.Create(ctx, veleroBackup); err != nil {
|
||||
if veleroBackup.Name != "" {
|
||||
name = veleroBackup.Name
|
||||
}
|
||||
logger.Error(err, "failed to create Velero Backup", "name", veleroBackup.Name)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "VeleroBackupCreationFailed",
|
||||
fmt.Sprintf("Failed to create Velero Backup %s/%s: %v", veleroNamespace, name, err))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("created Velero Backup", "name", veleroBackup.Name, "namespace", veleroBackup.Namespace)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "VeleroBackupCreated",
|
||||
fmt.Sprintf("Created Velero Backup %s/%s", veleroNamespace, name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) createBackupResource(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, veleroBackup *velerov1.Backup) (*backupsv1alpha1.Backup, error) {
|
||||
logger := getLogger(ctx)
|
||||
// Extract artifact information from Velero Backup
|
||||
// Create a basic artifact referencing the Velero backup
|
||||
artifact := &backupsv1alpha1.BackupArtifact{
|
||||
URI: fmt.Sprintf("velero://%s/%s", backupJob.Namespace, veleroBackup.Name),
|
||||
}
|
||||
|
||||
// Get takenAt from Velero Backup creation timestamp or status
|
||||
takenAt := metav1.Now()
|
||||
if veleroBackup.Status.StartTimestamp != nil {
|
||||
takenAt = *veleroBackup.Status.StartTimestamp
|
||||
} else if !veleroBackup.CreationTimestamp.IsZero() {
|
||||
takenAt = veleroBackup.CreationTimestamp
|
||||
}
|
||||
|
||||
// Extract driver metadata (e.g., Velero backup name)
|
||||
driverMetadata := map[string]string{
|
||||
"velero.io/backup-name": veleroBackup.Name,
|
||||
"velero.io/backup-namespace": veleroBackup.Namespace,
|
||||
}
|
||||
|
||||
backup := &backupsv1alpha1.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s", backupJob.Name),
|
||||
Namespace: backupJob.Namespace,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: backupJob.APIVersion,
|
||||
Kind: backupJob.Kind,
|
||||
Name: backupJob.Name,
|
||||
UID: backupJob.UID,
|
||||
Controller: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: backupsv1alpha1.BackupSpec{
|
||||
ApplicationRef: backupJob.Spec.ApplicationRef,
|
||||
StorageRef: backupJob.Spec.StorageRef,
|
||||
StrategyRef: backupJob.Spec.StrategyRef,
|
||||
TakenAt: takenAt,
|
||||
DriverMetadata: driverMetadata,
|
||||
},
|
||||
Status: backupsv1alpha1.BackupStatus{
|
||||
Phase: backupsv1alpha1.BackupPhaseReady,
|
||||
},
|
||||
}
|
||||
|
||||
if backupJob.Spec.PlanRef != nil {
|
||||
backup.Spec.PlanRef = backupJob.Spec.PlanRef
|
||||
}
|
||||
|
||||
if artifact != nil {
|
||||
backup.Status.Artifact = artifact
|
||||
}
|
||||
|
||||
if err := r.Create(ctx, backup); err != nil {
|
||||
logger.Error(err, "failed to create Backup resource")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debug("created Backup resource", "name", backup.Name)
|
||||
return backup, nil
|
||||
}
|
||||
@@ -5,13 +5,11 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -39,11 +37,8 @@ type CozystackResourceDefinitionReconciler struct {
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
if err := r.reconcileCozyRDAndUpdateHelmReleases(ctx); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Continue with debounced restart logic
|
||||
// Only handle debounced restart logic
|
||||
// HelmRelease reconciliation is handled by CozystackResourceDefinitionHelmReconciler
|
||||
return r.debouncedRestart(ctx)
|
||||
}
|
||||
|
||||
@@ -192,138 +187,3 @@ func sortCozyRDs(a, b cozyv1alpha1.CozystackResourceDefinition) int {
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// reconcileCozyRDAndUpdateHelmReleases reconciles all CozystackResourceDefinitions and updates HelmReleases from them
|
||||
func (r *CozystackResourceDefinitionReconciler) reconcileCozyRDAndUpdateHelmReleases(ctx context.Context) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// List all CozystackResourceDefinitions
|
||||
crdList := &cozyv1alpha1.CozystackResourceDefinitionList{}
|
||||
if err := r.List(ctx, crdList); err != nil {
|
||||
logger.Error(err, "failed to list CozystackResourceDefinitions")
|
||||
return err
|
||||
}
|
||||
|
||||
// Update HelmReleases for each CRD
|
||||
for i := range crdList.Items {
|
||||
crd := &crdList.Items[i]
|
||||
if err := r.updateHelmReleasesForCRD(ctx, crd); err != nil {
|
||||
logger.Error(err, "failed to update HelmReleases for CRD", "crd", crd.Name)
|
||||
// Continue with other CRDs even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateHelmReleasesForCRD updates all HelmReleases that match the application labels from CozystackResourceDefinition
|
||||
func (r *CozystackResourceDefinitionReconciler) updateHelmReleasesForCRD(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Use application labels to find HelmReleases
|
||||
// Labels: apps.cozystack.io/application.kind and apps.cozystack.io/application.group
|
||||
applicationKind := crd.Spec.Application.Kind
|
||||
|
||||
// Validate that applicationKind is non-empty
|
||||
if applicationKind == "" {
|
||||
logger.Error(fmt.Errorf("Application.Kind is empty"), "Skipping HelmRelease update: invalid CozystackResourceDefinition", "crd", crd.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
applicationGroup := "apps.cozystack.io" // All applications use this group
|
||||
|
||||
// Build label selector for HelmReleases
|
||||
// Only reconcile HelmReleases with cozystack.io/ui=true label
|
||||
labelSelector := client.MatchingLabels{
|
||||
"apps.cozystack.io/application.kind": applicationKind,
|
||||
"apps.cozystack.io/application.group": applicationGroup,
|
||||
"cozystack.io/ui": "true",
|
||||
}
|
||||
|
||||
// List all HelmReleases with matching labels
|
||||
hrList := &helmv2.HelmReleaseList{}
|
||||
if err := r.List(ctx, hrList, labelSelector); err != nil {
|
||||
logger.Error(err, "failed to list HelmReleases", "kind", applicationKind, "group", applicationGroup)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.V(4).Info("Found HelmReleases to update", "crd", crd.Name, "kind", applicationKind, "count", len(hrList.Items))
|
||||
|
||||
// Update each HelmRelease
|
||||
for i := range hrList.Items {
|
||||
hr := &hrList.Items[i]
|
||||
if err := r.updateHelmReleaseChart(ctx, hr, crd); err != nil {
|
||||
logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateHelmReleaseChart updates the chart in HelmRelease based on CozystackResourceDefinition
|
||||
func (r *CozystackResourceDefinitionReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
logger := log.FromContext(ctx)
|
||||
hrCopy := hr.DeepCopy()
|
||||
updated := false
|
||||
|
||||
// Validate Chart configuration exists
|
||||
if crd.Spec.Release.Chart.Name == "" {
|
||||
logger.V(4).Info("Skipping HelmRelease chart update: Chart.Name is empty", "crd", crd.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate SourceRef fields
|
||||
if crd.Spec.Release.Chart.SourceRef.Kind == "" ||
|
||||
crd.Spec.Release.Chart.SourceRef.Name == "" ||
|
||||
crd.Spec.Release.Chart.SourceRef.Namespace == "" {
|
||||
logger.Error(fmt.Errorf("invalid SourceRef in CRD"), "Skipping HelmRelease chart update: SourceRef fields are incomplete",
|
||||
"crd", crd.Name,
|
||||
"kind", crd.Spec.Release.Chart.SourceRef.Kind,
|
||||
"name", crd.Spec.Release.Chart.SourceRef.Name,
|
||||
"namespace", crd.Spec.Release.Chart.SourceRef.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get version and reconcileStrategy from CRD or use defaults
|
||||
version := ">= 0.0.0-0"
|
||||
reconcileStrategy := "Revision"
|
||||
// TODO: Add Version and ReconcileStrategy fields to CozystackResourceDefinitionChart if needed
|
||||
|
||||
// Build expected SourceRef
|
||||
expectedSourceRef := helmv2.CrossNamespaceObjectReference{
|
||||
Kind: crd.Spec.Release.Chart.SourceRef.Kind,
|
||||
Name: crd.Spec.Release.Chart.SourceRef.Name,
|
||||
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
|
||||
}
|
||||
|
||||
if hrCopy.Spec.Chart == nil {
|
||||
// Need to create Chart spec
|
||||
hrCopy.Spec.Chart = &helmv2.HelmChartTemplate{
|
||||
Spec: helmv2.HelmChartTemplateSpec{
|
||||
Chart: crd.Spec.Release.Chart.Name,
|
||||
Version: version,
|
||||
ReconcileStrategy: reconcileStrategy,
|
||||
SourceRef: expectedSourceRef,
|
||||
},
|
||||
}
|
||||
updated = true
|
||||
} else {
|
||||
// Update existing Chart spec
|
||||
if hrCopy.Spec.Chart.Spec.Chart != crd.Spec.Release.Chart.Name ||
|
||||
hrCopy.Spec.Chart.Spec.SourceRef != expectedSourceRef {
|
||||
hrCopy.Spec.Chart.Spec.Chart = crd.Spec.Release.Chart.Name
|
||||
hrCopy.Spec.Chart.Spec.SourceRef = expectedSourceRef
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
if updated {
|
||||
logger.V(4).Info("Updating HelmRelease chart", "name", hr.Name, "namespace", hr.Namespace)
|
||||
if err := r.Update(ctx, hrCopy); err != nil {
|
||||
return fmt.Errorf("failed to update HelmRelease: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=cozystackresourcedefinitions,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;update;patch
|
||||
|
||||
// CozystackResourceDefinitionHelmReconciler reconciles CozystackResourceDefinitions
|
||||
// and updates related HelmReleases when a CozyRD changes.
|
||||
// This controller does NOT watch HelmReleases to avoid mutual reconciliation storms
|
||||
// with Flux's helm-controller.
|
||||
type CozystackResourceDefinitionHelmReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionHelmReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Get the CozystackResourceDefinition that triggered this reconciliation
|
||||
crd := &cozyv1alpha1.CozystackResourceDefinition{}
|
||||
if err := r.Get(ctx, req.NamespacedName, crd); err != nil {
|
||||
logger.Error(err, "failed to get CozystackResourceDefinition", "name", req.Name)
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// Update HelmReleases related to this specific CozyRD
|
||||
if err := r.updateHelmReleasesForCRD(ctx, crd); err != nil {
|
||||
logger.Error(err, "failed to update HelmReleases for CRD", "crd", crd.Name)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionHelmReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystackresourcedefinition-helm-reconciler").
|
||||
For(&cozyv1alpha1.CozystackResourceDefinition{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
// updateHelmReleasesForCRD updates all HelmReleases that match the application labels from CozystackResourceDefinition
|
||||
func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleasesForCRD(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Use application labels to find HelmReleases
|
||||
// Labels: apps.cozystack.io/application.kind and apps.cozystack.io/application.group
|
||||
applicationKind := crd.Spec.Application.Kind
|
||||
|
||||
// Validate that applicationKind is non-empty
|
||||
if applicationKind == "" {
|
||||
logger.V(4).Info("Skipping HelmRelease update: Application.Kind is empty", "crd", crd.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
applicationGroup := "apps.cozystack.io" // All applications use this group
|
||||
|
||||
// Build label selector for HelmReleases
|
||||
// Only reconcile HelmReleases with cozystack.io/ui=true label
|
||||
labelSelector := client.MatchingLabels{
|
||||
"apps.cozystack.io/application.kind": applicationKind,
|
||||
"apps.cozystack.io/application.group": applicationGroup,
|
||||
"cozystack.io/ui": "true",
|
||||
}
|
||||
|
||||
// List all HelmReleases with matching labels
|
||||
hrList := &helmv2.HelmReleaseList{}
|
||||
if err := r.List(ctx, hrList, labelSelector); err != nil {
|
||||
logger.Error(err, "failed to list HelmReleases", "kind", applicationKind, "group", applicationGroup)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.V(4).Info("Found HelmReleases to update", "crd", crd.Name, "kind", applicationKind, "count", len(hrList.Items))
|
||||
|
||||
// Update each HelmRelease
|
||||
for i := range hrList.Items {
|
||||
hr := &hrList.Items[i]
|
||||
if err := r.updateHelmReleaseChart(ctx, hr, crd); err != nil {
|
||||
logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// expectedValuesFrom returns the expected valuesFrom configuration for HelmReleases
|
||||
func expectedValuesFrom() []helmv2.ValuesReference {
|
||||
return []helmv2.ValuesReference{
|
||||
{
|
||||
Kind: "Secret",
|
||||
Name: "cozystack-values",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// valuesFromEqual compares two ValuesReference slices
|
||||
func valuesFromEqual(a, b []helmv2.ValuesReference) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i].Kind != b[i].Kind ||
|
||||
a[i].Name != b[i].Name ||
|
||||
a[i].ValuesKey != b[i].ValuesKey ||
|
||||
a[i].TargetPath != b[i].TargetPath ||
|
||||
a[i].Optional != b[i].Optional {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// updateHelmReleaseChart updates the chart and valuesFrom in HelmRelease based on CozystackResourceDefinition
|
||||
func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
logger := log.FromContext(ctx)
|
||||
hrCopy := hr.DeepCopy()
|
||||
updated := false
|
||||
|
||||
// Validate ChartRef configuration exists
|
||||
if crd.Spec.Release.ChartRef == nil ||
|
||||
crd.Spec.Release.ChartRef.Kind == "" ||
|
||||
crd.Spec.Release.ChartRef.Name == "" ||
|
||||
crd.Spec.Release.ChartRef.Namespace == "" {
|
||||
logger.Error(fmt.Errorf("invalid ChartRef in CRD"), "Skipping HelmRelease chartRef update: ChartRef is nil or incomplete",
|
||||
"crd", crd.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use ChartRef directly from CRD
|
||||
expectedChartRef := crd.Spec.Release.ChartRef
|
||||
|
||||
// Check if chartRef needs to be updated
|
||||
if hrCopy.Spec.ChartRef == nil {
|
||||
hrCopy.Spec.ChartRef = expectedChartRef
|
||||
// Clear the old chart field when switching to chartRef
|
||||
hrCopy.Spec.Chart = nil
|
||||
updated = true
|
||||
} else if hrCopy.Spec.ChartRef.Kind != expectedChartRef.Kind ||
|
||||
hrCopy.Spec.ChartRef.Name != expectedChartRef.Name ||
|
||||
hrCopy.Spec.ChartRef.Namespace != expectedChartRef.Namespace {
|
||||
hrCopy.Spec.ChartRef = expectedChartRef
|
||||
updated = true
|
||||
}
|
||||
|
||||
// Check and update valuesFrom configuration
|
||||
expected := expectedValuesFrom()
|
||||
if !valuesFromEqual(hrCopy.Spec.ValuesFrom, expected) {
|
||||
logger.V(4).Info("Updating HelmRelease valuesFrom", "name", hr.Name, "namespace", hr.Namespace)
|
||||
hrCopy.Spec.ValuesFrom = expected
|
||||
updated = true
|
||||
}
|
||||
|
||||
if updated {
|
||||
logger.V(4).Info("Updating HelmRelease chartRef", "name", hr.Name, "namespace", hr.Namespace)
|
||||
if err := r.Update(ctx, hrCopy); err != nil {
|
||||
return fmt.Errorf("failed to update HelmRelease: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
)
|
||||
|
||||
type CozystackConfigReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
var configMapNames = []string{"cozystack", "cozystack-branding", "cozystack-scheduling"}
|
||||
|
||||
const configMapNamespace = "cozy-system"
|
||||
const digestAnnotation = "cozystack.io/cozy-config-digest"
|
||||
const forceReconcileKey = "reconcile.fluxcd.io/forceAt"
|
||||
const requestedAt = "reconcile.fluxcd.io/requestedAt"
|
||||
|
||||
func (r *CozystackConfigReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) {
|
||||
log := log.FromContext(ctx)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
digest, err := r.computeDigest(ctx)
|
||||
if err != nil {
|
||||
log.Error(err, "failed to compute config digest")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
var helmList helmv2.HelmReleaseList
|
||||
if err := r.List(ctx, &helmList); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("failed to list HelmReleases: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now().Format(time.RFC3339Nano)
|
||||
updated := 0
|
||||
|
||||
for _, hr := range helmList.Items {
|
||||
isSystemApp := hr.Labels["cozystack.io/system-app"] == "true"
|
||||
isTenantRoot := hr.Namespace == "tenant-root" && hr.Name == "tenant-root"
|
||||
if !isSystemApp && !isTenantRoot {
|
||||
continue
|
||||
}
|
||||
patchTarget := hr.DeepCopy()
|
||||
|
||||
if hr.Annotations == nil {
|
||||
hr.Annotations = map[string]string{}
|
||||
}
|
||||
|
||||
if hr.Annotations[digestAnnotation] == digest {
|
||||
continue
|
||||
}
|
||||
patchTarget.Annotations[digestAnnotation] = digest
|
||||
patchTarget.Annotations[forceReconcileKey] = now
|
||||
patchTarget.Annotations[requestedAt] = now
|
||||
|
||||
patch := client.MergeFrom(hr.DeepCopy())
|
||||
if err := r.Patch(ctx, patchTarget, patch); err != nil {
|
||||
log.Error(err, "failed to patch HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
|
||||
continue
|
||||
}
|
||||
updated++
|
||||
log.Info("patched HelmRelease with new config digest", "name", hr.Name, "namespace", hr.Namespace)
|
||||
}
|
||||
|
||||
log.Info("finished reconciliation", "updatedHelmReleases", updated)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *CozystackConfigReconciler) computeDigest(ctx context.Context) (string, error) {
|
||||
hash := sha256.New()
|
||||
|
||||
for _, name := range configMapNames {
|
||||
var cm corev1.ConfigMap
|
||||
err := r.Get(ctx, client.ObjectKey{Namespace: configMapNamespace, Name: name}, &cm)
|
||||
if err != nil {
|
||||
if kerrors.IsNotFound(err) {
|
||||
continue // ignore missing
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Sort keys for consistent hashing
|
||||
var keys []string
|
||||
for k := range cm.Data {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
v := cm.Data[k]
|
||||
fmt.Fprintf(hash, "%s:%s=%s\n", name, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func (r *CozystackConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
WithEventFilter(predicate.Funcs{
|
||||
UpdateFunc: func(e event.UpdateEvent) bool {
|
||||
cm, ok := e.ObjectNew.(*corev1.ConfigMap)
|
||||
return ok && cm.Namespace == configMapNamespace && contains(configMapNames, cm.Name)
|
||||
},
|
||||
CreateFunc: func(e event.CreateEvent) bool {
|
||||
cm, ok := e.Object.(*corev1.ConfigMap)
|
||||
return ok && cm.Namespace == configMapNamespace && contains(configMapNames, cm.Name)
|
||||
},
|
||||
DeleteFunc: func(e event.DeleteEvent) bool {
|
||||
cm, ok := e.Object.(*corev1.ConfigMap)
|
||||
return ok && cm.Namespace == configMapNamespace && contains(configMapNames, cm.Name)
|
||||
},
|
||||
}).
|
||||
For(&corev1.ConfigMap{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func contains(slice []string, val string) bool {
|
||||
for _, s := range slice {
|
||||
if s == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
e "errors"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
"gopkg.in/yaml.v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
type TenantHelmReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
func (r *TenantHelmReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
hr := &helmv2.HelmRelease{}
|
||||
if err := r.Get(ctx, req.NamespacedName, hr); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
logger.Error(err, "unable to fetch HelmRelease")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(hr.Name, "tenant-") {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if len(hr.Status.Conditions) == 0 || hr.Status.Conditions[0].Type != "Ready" {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if len(hr.Status.History) == 0 {
|
||||
logger.Info("no history in HelmRelease status", "name", hr.Name)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if hr.Status.History[0].Status != "deployed" {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
newDigest := hr.Status.History[0].Digest
|
||||
var hrList helmv2.HelmReleaseList
|
||||
childNamespace := getChildNamespace(hr.Namespace, hr.Name)
|
||||
if childNamespace == "tenant-root" && hr.Name == "tenant-root" {
|
||||
if hr.Spec.Values == nil {
|
||||
logger.Error(e.New("hr.Spec.Values is nil"), "cant annotate tenant-root ns")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
err := annotateTenantRootNs(*hr.Spec.Values, r.Client)
|
||||
if err != nil {
|
||||
logger.Error(err, "cant annotate tenant-root ns")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
logger.Info("namespace 'tenant-root' annotated")
|
||||
}
|
||||
|
||||
if err := r.List(ctx, &hrList, client.InNamespace(childNamespace)); err != nil {
|
||||
logger.Error(err, "unable to list HelmReleases in namespace", "namespace", hr.Name)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
for _, item := range hrList.Items {
|
||||
if item.Name == hr.Name {
|
||||
continue
|
||||
}
|
||||
oldDigest := item.GetAnnotations()["cozystack.io/tenant-config-digest"]
|
||||
if oldDigest == newDigest {
|
||||
continue
|
||||
}
|
||||
patchTarget := item.DeepCopy()
|
||||
|
||||
if patchTarget.Annotations == nil {
|
||||
patchTarget.Annotations = map[string]string{}
|
||||
}
|
||||
ts := time.Now().Format(time.RFC3339Nano)
|
||||
|
||||
patchTarget.Annotations["cozystack.io/tenant-config-digest"] = newDigest
|
||||
patchTarget.Annotations["reconcile.fluxcd.io/forceAt"] = ts
|
||||
patchTarget.Annotations["reconcile.fluxcd.io/requestedAt"] = ts
|
||||
|
||||
patch := client.MergeFrom(item.DeepCopy())
|
||||
if err := r.Patch(ctx, patchTarget, patch); err != nil {
|
||||
logger.Error(err, "failed to patch HelmRelease", "name", patchTarget.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("patched HelmRelease with new digest", "name", patchTarget.Name, "digest", newDigest, "version", hr.Status.History[0].Version)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *TenantHelmReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&helmv2.HelmRelease{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func getChildNamespace(currentNamespace, hrName string) string {
|
||||
tenantName := strings.TrimPrefix(hrName, "tenant-")
|
||||
|
||||
switch {
|
||||
case currentNamespace == "tenant-root" && hrName == "tenant-root":
|
||||
// 1) root tenant inside root namespace
|
||||
return "tenant-root"
|
||||
|
||||
case currentNamespace == "tenant-root":
|
||||
// 2) any other tenant in root namespace
|
||||
return fmt.Sprintf("tenant-%s", tenantName)
|
||||
|
||||
default:
|
||||
// 3) tenant in a dedicated namespace
|
||||
return fmt.Sprintf("%s-%s", currentNamespace, tenantName)
|
||||
}
|
||||
}
|
||||
|
||||
func annotateTenantRootNs(values apiextensionsv1.JSON, c client.Client) error {
|
||||
var data map[string]interface{}
|
||||
if err := yaml.Unmarshal(values.Raw, &data); err != nil {
|
||||
return fmt.Errorf("failed to parse HelmRelease values: %w", err)
|
||||
}
|
||||
|
||||
host, ok := data["host"].(string)
|
||||
if !ok || host == "" {
|
||||
return fmt.Errorf("host field not found or not a string")
|
||||
}
|
||||
|
||||
var ns corev1.Namespace
|
||||
if err := c.Get(context.TODO(), client.ObjectKey{Name: "tenant-root"}, &ns); err != nil {
|
||||
return fmt.Errorf("failed to get namespace tenant-root: %w", err)
|
||||
}
|
||||
|
||||
if ns.Annotations == nil {
|
||||
ns.Annotations = map[string]string{}
|
||||
}
|
||||
ns.Annotations["namespace.cozystack.io/host"] = host
|
||||
|
||||
if err := c.Update(context.TODO(), &ns); err != nil {
|
||||
return fmt.Errorf("failed to update namespace: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
272
internal/cozyvaluesreplicator/cozyvaluesreplicator.go
Normal file
272
internal/cozyvaluesreplicator/cozyvaluesreplicator.go
Normal file
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
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 cozyvaluesreplicator
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
// SecretReplicatorReconciler replicates a source secret to namespaces matching a label selector.
|
||||
type SecretReplicatorReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
|
||||
// Source of truth:
|
||||
SourceNamespace string
|
||||
SecretName string
|
||||
|
||||
// Namespaces to replicate into:
|
||||
// (e.g. labels.SelectorFromSet(labels.Set{"tenant":"true"}), or metav1.LabelSelectorAsSelector(...))
|
||||
TargetNamespaceSelector labels.Selector
|
||||
}
|
||||
|
||||
func (r *SecretReplicatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
// 1) Primary watch for requirement (b):
|
||||
// Reconcile any Secret named r.SecretName in any namespace (includes source too).
|
||||
// This keeps Secrets in cache and causes "copy changed -> reconcile it" to happen.
|
||||
secretNameOnly := predicate.NewPredicateFuncs(func(obj client.Object) bool {
|
||||
return obj.GetName() == r.SecretName
|
||||
})
|
||||
|
||||
// 2) Secondary watch for requirement (c):
|
||||
// When the *source* Secret changes, fan-out reconcile requests to every matching namespace.
|
||||
onlySourceSecret := predicate.Funcs{
|
||||
CreateFunc: func(e event.CreateEvent) bool { return isSourceSecret(e.Object, r) },
|
||||
UpdateFunc: func(e event.UpdateEvent) bool { return isSourceSecret(e.ObjectNew, r) },
|
||||
DeleteFunc: func(e event.DeleteEvent) bool { return isSourceSecret(e.Object, r) },
|
||||
GenericFunc: func(e event.GenericEvent) bool {
|
||||
return isSourceSecret(e.Object, r)
|
||||
},
|
||||
}
|
||||
|
||||
// Fan-out mapper for source Secret events -> one request per matching target namespace.
|
||||
fanOutOnSourceSecret := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, _ client.Object) []reconcile.Request {
|
||||
// List namespaces *from the cache* (because we also watch Namespaces below).
|
||||
var nsList corev1.NamespaceList
|
||||
if err := r.List(ctx, &nsList); err != nil {
|
||||
// If list fails, best-effort: return nothing; reconcile will be retried by next event.
|
||||
return nil
|
||||
}
|
||||
|
||||
reqs := make([]reconcile.Request, 0, len(nsList.Items))
|
||||
for i := range nsList.Items {
|
||||
ns := &nsList.Items[i]
|
||||
if ns.Name == r.SourceNamespace {
|
||||
continue
|
||||
}
|
||||
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)) {
|
||||
continue
|
||||
}
|
||||
reqs = append(reqs, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: ns.Name,
|
||||
Name: r.SecretName,
|
||||
},
|
||||
})
|
||||
}
|
||||
return reqs
|
||||
})
|
||||
|
||||
// 3) Namespace watch for requirement (a):
|
||||
// When a namespace is created/updated to match selector, enqueue reconcile for the Secret copy in that namespace.
|
||||
enqueueOnNamespaceMatch := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
ns, ok := obj.(*corev1.Namespace)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if ns.Name == r.SourceNamespace {
|
||||
return nil
|
||||
}
|
||||
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)) {
|
||||
return nil
|
||||
}
|
||||
return []reconcile.Request{{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: ns.Name,
|
||||
Name: r.SecretName,
|
||||
},
|
||||
}}
|
||||
})
|
||||
|
||||
// Only trigger from namespace events where the label match may be (or become) true.
|
||||
// (You can keep this simple; it's fine if it fires on any update—your Reconcile should be idempotent.)
|
||||
namespaceMayMatter := predicate.Funcs{
|
||||
CreateFunc: func(e event.CreateEvent) bool {
|
||||
ns, ok := e.Object.(*corev1.Namespace)
|
||||
return ok && (r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)))
|
||||
},
|
||||
UpdateFunc: func(e event.UpdateEvent) bool {
|
||||
oldNS, okOld := e.ObjectOld.(*corev1.Namespace)
|
||||
newNS, okNew := e.ObjectNew.(*corev1.Namespace)
|
||||
if !okOld || !okNew {
|
||||
return false
|
||||
}
|
||||
// Fire if it matches now OR matched before (covers transitions both ways; reconcile can decide what to do).
|
||||
oldMatch := r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(oldNS.Labels))
|
||||
newMatch := r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(newNS.Labels))
|
||||
return oldMatch || newMatch
|
||||
},
|
||||
DeleteFunc: func(event.DeleteEvent) bool { return false }, // nothing to do on namespace delete
|
||||
GenericFunc: func(event.GenericEvent) bool { return false },
|
||||
}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
// (b) Watch all Secrets with the chosen name; this also ensures Secret objects are cached.
|
||||
For(&corev1.Secret{}, builder.WithPredicates(secretNameOnly)).
|
||||
|
||||
// (c) Add a second watch on Secret, but only for the source secret, and fan-out to all namespaces.
|
||||
Watches(
|
||||
&corev1.Secret{},
|
||||
fanOutOnSourceSecret,
|
||||
builder.WithPredicates(onlySourceSecret),
|
||||
).
|
||||
|
||||
// (a) Watch Namespaces so they're cached and so "namespace appears / starts matching" enqueues reconcile.
|
||||
Watches(
|
||||
&corev1.Namespace{},
|
||||
enqueueOnNamespaceMatch,
|
||||
builder.WithPredicates(namespaceMayMatter),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
func isSourceSecret(obj client.Object, r *SecretReplicatorReconciler) bool {
|
||||
if obj == nil {
|
||||
return false
|
||||
}
|
||||
return obj.GetNamespace() == r.SourceNamespace && obj.GetName() == r.SecretName
|
||||
}
|
||||
|
||||
func (r *SecretReplicatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Ignore requests that don't match our secret name or are for the source namespace
|
||||
if req.Name != r.SecretName || req.Namespace == r.SourceNamespace {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Verify the target namespace still exists and matches the selector
|
||||
targetNamespace := &corev1.Namespace{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: req.Namespace}, targetNamespace); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Namespace doesn't exist, nothing to do
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
logger.Error(err, "Failed to get target namespace", "namespace", req.Namespace)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Check if namespace still matches the selector
|
||||
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(targetNamespace.Labels)) {
|
||||
// Namespace no longer matches selector, delete the replicated secret if it exists
|
||||
replicatedSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: req.Namespace,
|
||||
Name: req.Name,
|
||||
},
|
||||
}
|
||||
if err := r.Delete(ctx, replicatedSecret); err != nil && !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "Failed to delete replicated secret from non-matching namespace",
|
||||
"namespace", req.Namespace, "secret", req.Name)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Get the source secret
|
||||
originalSecret := &corev1.Secret{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Namespace: r.SourceNamespace, Name: r.SecretName}, originalSecret); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Source secret doesn't exist, delete the replicated secret if it exists
|
||||
replicatedSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: req.Namespace,
|
||||
Name: req.Name,
|
||||
},
|
||||
}
|
||||
if err := r.Delete(ctx, replicatedSecret); err != nil && !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "Failed to delete replicated secret after source secret deletion",
|
||||
"namespace", req.Namespace, "secret", req.Name)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
logger.Error(err, "Failed to get source secret",
|
||||
"namespace", r.SourceNamespace, "secret", r.SecretName)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Create or update the replicated secret
|
||||
replicatedSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: req.Namespace,
|
||||
Name: req.Name,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, replicatedSecret, func() error {
|
||||
// Copy the secret data and type from the source
|
||||
replicatedSecret.Data = make(map[string][]byte)
|
||||
for k, v := range originalSecret.Data {
|
||||
replicatedSecret.Data[k] = v
|
||||
}
|
||||
replicatedSecret.Type = originalSecret.Type
|
||||
|
||||
// Copy labels and annotations from source (if any)
|
||||
if originalSecret.Labels != nil {
|
||||
if replicatedSecret.Labels == nil {
|
||||
replicatedSecret.Labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range originalSecret.Labels {
|
||||
replicatedSecret.Labels[k] = v
|
||||
}
|
||||
}
|
||||
if originalSecret.Annotations != nil {
|
||||
if replicatedSecret.Annotations == nil {
|
||||
replicatedSecret.Annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range originalSecret.Annotations {
|
||||
replicatedSecret.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to create or update replicated secret",
|
||||
"namespace", req.Namespace, "secret", req.Name)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
@@ -28,8 +28,6 @@ import (
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
|
||||
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
@@ -160,7 +158,6 @@ func readYAMLObjects(reader io.Reader) ([]*unstructured.Unstructured, error) {
|
||||
// applyManifests applies Kubernetes objects using server-side apply.
|
||||
func applyManifests(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured) error {
|
||||
logger := log.FromContext(ctx)
|
||||
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
||||
|
||||
// Separate CRDs and namespaces from other resources
|
||||
var stageOne []*unstructured.Unstructured // CRDs and Namespaces
|
||||
@@ -177,7 +174,7 @@ func applyManifests(ctx context.Context, k8sClient client.Client, objects []*uns
|
||||
// Apply stage one (CRDs and Namespaces) first
|
||||
if len(stageOne) > 0 {
|
||||
logger.Info("Applying cluster definitions", "count", len(stageOne))
|
||||
if err := applyObjects(ctx, k8sClient, decoder, stageOne); err != nil {
|
||||
if err := applyObjects(ctx, k8sClient, stageOne); err != nil {
|
||||
return fmt.Errorf("failed to apply cluster definitions: %w", err)
|
||||
}
|
||||
|
||||
@@ -188,7 +185,7 @@ func applyManifests(ctx context.Context, k8sClient client.Client, objects []*uns
|
||||
// Apply stage two (everything else)
|
||||
if len(stageTwo) > 0 {
|
||||
logger.Info("Applying resources", "count", len(stageTwo))
|
||||
if err := applyObjects(ctx, k8sClient, decoder, stageTwo); err != nil {
|
||||
if err := applyObjects(ctx, k8sClient, stageTwo); err != nil {
|
||||
return fmt.Errorf("failed to apply resources: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -197,7 +194,7 @@ func applyManifests(ctx context.Context, k8sClient client.Client, objects []*uns
|
||||
}
|
||||
|
||||
// applyObjects applies a list of objects using server-side apply.
|
||||
func applyObjects(ctx context.Context, k8sClient client.Client, decoder runtime.Decoder, objects []*unstructured.Unstructured) error {
|
||||
func applyObjects(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured) error {
|
||||
for _, obj := range objects {
|
||||
// Use server-side apply with force ownership and field manager
|
||||
// FieldManager is required for apply patch operations
|
||||
@@ -237,6 +234,7 @@ func isClusterDefinition(obj *unstructured.Unstructured) bool {
|
||||
// injectKubernetesServiceEnv injects KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT
|
||||
// environment variables into all containers of Deployment, StatefulSet, and DaemonSet objects
|
||||
// if these variables are set in the operator's environment.
|
||||
// Errors are logged but do not stop processing of other objects.
|
||||
func injectKubernetesServiceEnv(objects []*unstructured.Unstructured) error {
|
||||
kubernetesHost := os.Getenv("KUBERNETES_SERVICE_HOST")
|
||||
kubernetesPort := os.Getenv("KUBERNETES_SERVICE_PORT")
|
||||
@@ -246,6 +244,7 @@ func injectKubernetesServiceEnv(objects []*unstructured.Unstructured) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var firstErr error
|
||||
for _, obj := range objects {
|
||||
kind := obj.GetKind()
|
||||
if kind != "Deployment" && kind != "StatefulSet" && kind != "DaemonSet" {
|
||||
@@ -254,35 +253,62 @@ func injectKubernetesServiceEnv(objects []*unstructured.Unstructured) error {
|
||||
|
||||
// Navigate to spec.template.spec.containers
|
||||
spec, found, err := unstructured.NestedMap(obj.Object, "spec", "template", "spec")
|
||||
if !found || err != nil {
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to get spec for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Update containers
|
||||
containers, found, err := unstructured.NestedSlice(spec, "containers")
|
||||
if found && err == nil {
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to get containers for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if found {
|
||||
containers = updateContainersEnv(containers, kubernetesHost, kubernetesPort)
|
||||
if err := unstructured.SetNestedSlice(spec, containers, "containers"); err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to set containers for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Update initContainers
|
||||
initContainers, found, err := unstructured.NestedSlice(spec, "initContainers")
|
||||
if found && err == nil {
|
||||
if err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to get initContainers for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if found {
|
||||
initContainers = updateContainersEnv(initContainers, kubernetesHost, kubernetesPort)
|
||||
if err := unstructured.SetNestedSlice(spec, initContainers, "initContainers"); err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to set initContainers for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Update spec in the object
|
||||
if err := unstructured.SetNestedMap(obj.Object, spec, "spec", "template", "spec"); err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("failed to update spec for %s/%s: %w", kind, obj.GetName(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// updateContainersEnv updates environment variables for a slice of containers.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,11 +31,20 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
const (
|
||||
// AnnotationSkipCozystackValues disables injection of cozystack-values secret into HelmRelease
|
||||
// This annotation should be placed on PackageSource
|
||||
AnnotationSkipCozystackValues = "operator.cozystack.io/skip-cozystack-values"
|
||||
// SecretCozystackValues is the name of the secret containing cluster and namespace configuration
|
||||
SecretCozystackValues = "cozystack-values"
|
||||
)
|
||||
|
||||
// PackageReconciler reconciles Package resources
|
||||
type PackageReconciler struct {
|
||||
client.Client
|
||||
@@ -113,15 +122,21 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Update dependencies status
|
||||
if err := r.updateDependenciesStatus(ctx, pkg, variant); err != nil {
|
||||
logger.Error(err, "failed to update dependencies status")
|
||||
// Don't return error, continue with reconciliation
|
||||
}
|
||||
|
||||
// Validate variant dependencies before creating HelmReleases
|
||||
// If dependencies are missing, we don't create new HelmReleases but don't delete existing ones
|
||||
if err := r.validateVariantDependencies(ctx, pkg, variant); err != nil {
|
||||
logger.Info("variant dependencies not ready, skipping HelmRelease creation", "package", pkg.Name, "error", err)
|
||||
// Check if all dependencies are ready based on status
|
||||
if !r.areDependenciesReady(pkg, variant) {
|
||||
logger.Info("variant dependencies not ready, skipping HelmRelease creation", "package", pkg.Name)
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "DependenciesNotReady",
|
||||
Message: fmt.Sprintf("Variant dependencies not ready: %v", err),
|
||||
Message: "One or more dependencies are not ready",
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
@@ -152,10 +167,20 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
strings.ReplaceAll(variantName, ".", "-"),
|
||||
strings.ReplaceAll(component.Name, ".", "-"))
|
||||
|
||||
// Determine namespace (from Install or default to cozy-system)
|
||||
// Namespace must be set
|
||||
namespace := component.Install.Namespace
|
||||
if namespace == "" {
|
||||
namespace = "cozy-system"
|
||||
logger.Error(fmt.Errorf("component %s has empty namespace in Install section", component.Name), "namespace validation failed")
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "InvalidConfiguration",
|
||||
Message: fmt.Sprintf("Component %s has empty namespace in Install section", component.Name),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("component %s has empty namespace in Install section", component.Name)
|
||||
}
|
||||
|
||||
// Determine release name (from Install or use component name)
|
||||
@@ -198,11 +223,35 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
},
|
||||
}
|
||||
|
||||
// Add valuesFrom for cozystack-values secret unless disabled by annotation on PackageSource
|
||||
if packageSource.GetAnnotations()[AnnotationSkipCozystackValues] != "true" {
|
||||
hr.Spec.ValuesFrom = []helmv2.ValuesReference{
|
||||
{
|
||||
Kind: "Secret",
|
||||
Name: SecretCozystackValues,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Set ownerReference
|
||||
gvk, err := apiutil.GVKForObject(pkg, r.Scheme)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to get GVK for Package")
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "InternalError",
|
||||
Message: fmt.Sprintf("Failed to get GVK for Package: %v", err),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, fmt.Errorf("failed to get GVK for Package: %w", err)
|
||||
}
|
||||
hr.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: pkg.APIVersion,
|
||||
Kind: pkg.Kind,
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
Kind: gvk.Kind,
|
||||
Name: pkg.Name,
|
||||
UID: pkg.UID,
|
||||
Controller: func() *bool { b := true; return &b }(),
|
||||
@@ -227,12 +276,21 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
// Return nil to stop reconciliation, error is recorded in status
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if len(dependsOn) > 0 {
|
||||
hr.Spec.DependsOn = dependsOn
|
||||
}
|
||||
|
||||
// Set valuesFiles annotation
|
||||
if len(component.ValuesFiles) > 0 {
|
||||
if hr.Annotations == nil {
|
||||
hr.Annotations = make(map[string]string)
|
||||
}
|
||||
hr.Annotations["cozyhr.cozystack.io/values-files"] = strings.Join(component.ValuesFiles, ",")
|
||||
}
|
||||
|
||||
if err := r.createOrUpdateHelmRelease(ctx, hr); err != nil {
|
||||
logger.Error(err, "failed to reconcile HelmRelease", "name", releaseName, "namespace", namespace)
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
@@ -272,12 +330,16 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
||||
|
||||
logger.Info("reconciled Package", "name", pkg.Name, "helmReleaseCount", helmReleaseCount)
|
||||
|
||||
// Trigger reconcile for Packages that depend on this Package
|
||||
if err := r.triggerDependentPackages(ctx, pkg.Name); err != nil {
|
||||
logger.Error(err, "failed to trigger dependent Packages", "package", pkg.Name)
|
||||
// Update dependencies status for Packages that depend on this Package
|
||||
// This ensures they get re-enqueued when their dependency becomes ready
|
||||
if err := r.updateDependentPackagesDependencies(ctx, pkg.Name); err != nil {
|
||||
logger.V(1).Error(err, "failed to update dependent packages dependencies", "package", pkg.Name)
|
||||
// Don't return error, this is best-effort
|
||||
}
|
||||
|
||||
// Dependent Packages will be automatically enqueued by the watch handler
|
||||
// when this Package's status is updated (see SetupWithManager watch handler)
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
@@ -332,6 +394,47 @@ func (r *PackageReconciler) createOrUpdateHelmRelease(ctx context.Context, hr *h
|
||||
return r.Update(ctx, existing)
|
||||
}
|
||||
|
||||
// getVariantForPackage retrieves the Variant for a given Package
|
||||
// Returns the Variant and an error if not found
|
||||
// If c is nil, uses the reconciler's client
|
||||
func (r *PackageReconciler) getVariantForPackage(ctx context.Context, pkg *cozyv1alpha1.Package, c client.Client) (*cozyv1alpha1.Variant, error) {
|
||||
// Use provided client or fall back to reconciler's client
|
||||
cl := c
|
||||
if cl == nil {
|
||||
cl = r.Client
|
||||
}
|
||||
|
||||
// Determine variant name (default to "default" if not specified)
|
||||
variantName := pkg.Spec.Variant
|
||||
if variantName == "" {
|
||||
variantName = "default"
|
||||
}
|
||||
|
||||
// Get the PackageSource
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := cl.Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("PackageSource %s not found", pkg.Name)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get PackageSource %s: %w", pkg.Name, err)
|
||||
}
|
||||
|
||||
// Find the variant in PackageSource
|
||||
var variant *cozyv1alpha1.Variant
|
||||
for i := range packageSource.Spec.Variants {
|
||||
if packageSource.Spec.Variants[i].Name == variantName {
|
||||
variant = &packageSource.Spec.Variants[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if variant == nil {
|
||||
return nil, fmt.Errorf("variant %s not found in PackageSource %s", variantName, pkg.Name)
|
||||
}
|
||||
|
||||
return variant, nil
|
||||
}
|
||||
|
||||
// buildDependsOn builds DependsOn list for a component
|
||||
// Includes:
|
||||
// 1. Dependencies from component.Install.DependsOn (with namespace from referenced component)
|
||||
@@ -351,7 +454,7 @@ func (r *PackageReconciler) buildDependsOn(ctx context.Context, pkg *cozyv1alpha
|
||||
}
|
||||
compNamespace := comp.Install.Namespace
|
||||
if compNamespace == "" {
|
||||
compNamespace = "cozy-system"
|
||||
return nil, fmt.Errorf("component %s has empty namespace in Install section", comp.Name)
|
||||
}
|
||||
compReleaseName := comp.Install.ReleaseName
|
||||
if compReleaseName == "" {
|
||||
@@ -407,31 +510,9 @@ func (r *PackageReconciler) buildDependsOn(ctx context.Context, pkg *cozyv1alpha
|
||||
}
|
||||
|
||||
// Get the variant from dependent Package
|
||||
depVariantName := depPackage.Spec.Variant
|
||||
if depVariantName == "" {
|
||||
depVariantName = "default"
|
||||
}
|
||||
|
||||
// Get the PackageSource
|
||||
depPackageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("dependent PackageSource %s not found", depPackageName)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get dependent PackageSource %s: %w", depPackageName, err)
|
||||
}
|
||||
|
||||
// Find the variant in PackageSource
|
||||
var depVariant *cozyv1alpha1.Variant
|
||||
for i := range depPackageSource.Spec.Variants {
|
||||
if depPackageSource.Spec.Variants[i].Name == depVariantName {
|
||||
depVariant = &depPackageSource.Spec.Variants[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if depVariant == nil {
|
||||
return nil, fmt.Errorf("dependent variant %s not found in PackageSource %s", depVariantName, depPackageName)
|
||||
depVariant, err := r.getVariantForPackage(ctx, depPackage, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get variant for dependent Package %s: %w", depPackageName, err)
|
||||
}
|
||||
|
||||
// Add all components with Install from dependent variant
|
||||
@@ -449,7 +530,7 @@ func (r *PackageReconciler) buildDependsOn(ctx context.Context, pkg *cozyv1alpha
|
||||
|
||||
depCompNamespace := depComp.Install.Namespace
|
||||
if depCompNamespace == "" {
|
||||
depCompNamespace = "cozy-system"
|
||||
return nil, fmt.Errorf("component %s in dependent Package %s has empty namespace in Install section", depComp.Name, depPackageName)
|
||||
}
|
||||
depCompReleaseName := depComp.Install.ReleaseName
|
||||
if depCompReleaseName == "" {
|
||||
@@ -468,13 +549,86 @@ func (r *PackageReconciler) buildDependsOn(ctx context.Context, pkg *cozyv1alpha
|
||||
return dependsOn, nil
|
||||
}
|
||||
|
||||
// validateVariantDependencies validates that all variant dependencies exist
|
||||
// Returns error if any dependency is missing
|
||||
func (r *PackageReconciler) validateVariantDependencies(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
|
||||
// updateDependenciesStatus updates the dependencies status in Package status
|
||||
// It checks the readiness of each dependency and updates pkg.Status.Dependencies
|
||||
// Old dependency keys that are no longer in the dependency list are removed
|
||||
func (r *PackageReconciler) updateDependenciesStatus(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Initialize dependencies map if nil
|
||||
if pkg.Status.Dependencies == nil {
|
||||
pkg.Status.Dependencies = make(map[string]cozyv1alpha1.DependencyStatus)
|
||||
}
|
||||
|
||||
// Build set of current dependencies (excluding ignored ones)
|
||||
currentDeps := make(map[string]bool)
|
||||
if len(variant.DependsOn) > 0 {
|
||||
for _, depPackageName := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == depPackageName {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
logger.V(1).Info("ignoring dependency", "package", pkg.Name, "dependency", depPackageName)
|
||||
continue
|
||||
}
|
||||
currentDeps[depPackageName] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old dependencies that are no longer in the list
|
||||
for depName := range pkg.Status.Dependencies {
|
||||
if !currentDeps[depName] {
|
||||
delete(pkg.Status.Dependencies, depName)
|
||||
logger.V(1).Info("removed old dependency from status", "package", pkg.Name, "dependency", depName)
|
||||
}
|
||||
}
|
||||
|
||||
// Update status for each current dependency
|
||||
for depPackageName := range currentDeps {
|
||||
// Get the Package
|
||||
depPackage := &cozyv1alpha1.Package{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackage); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Dependency not found, mark as not ready
|
||||
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
|
||||
Ready: false,
|
||||
}
|
||||
logger.V(1).Info("dependency not found, marking as not ready", "package", pkg.Name, "dependency", depPackageName)
|
||||
continue
|
||||
}
|
||||
// Error getting dependency, keep existing status or mark as not ready
|
||||
if _, exists := pkg.Status.Dependencies[depPackageName]; !exists {
|
||||
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
|
||||
Ready: false,
|
||||
}
|
||||
}
|
||||
logger.V(1).Error(err, "failed to get dependency, keeping existing status", "package", pkg.Name, "dependency", depPackageName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check Ready condition
|
||||
readyCondition := meta.FindStatusCondition(depPackage.Status.Conditions, "Ready")
|
||||
isReady := readyCondition != nil && readyCondition.Status == metav1.ConditionTrue
|
||||
|
||||
// Update dependency status
|
||||
pkg.Status.Dependencies[depPackageName] = cozyv1alpha1.DependencyStatus{
|
||||
Ready: isReady,
|
||||
}
|
||||
logger.V(1).Info("updated dependency status", "package", pkg.Name, "dependency", depPackageName, "ready", isReady)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// areDependenciesReady checks if all dependencies are ready based on status
|
||||
func (r *PackageReconciler) areDependenciesReady(pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) bool {
|
||||
if len(variant.DependsOn) == 0 {
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
|
||||
for _, depPackageName := range variant.DependsOn {
|
||||
@@ -487,45 +641,92 @@ func (r *PackageReconciler) validateVariantDependencies(ctx context.Context, pkg
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
logger.V(1).Info("ignoring dependency", "package", pkg.Name, "dependency", depPackageName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the Package
|
||||
depPackage := &cozyv1alpha1.Package{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackage); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("dependent Package %s not found", depPackageName)
|
||||
// Check dependency status
|
||||
depStatus, exists := pkg.Status.Dependencies[depPackageName]
|
||||
if !exists || !depStatus.Ready {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// updateDependentPackagesDependencies updates dependencies status for all Packages that depend on the given Package
|
||||
// This ensures dependent packages get re-enqueued when their dependency status changes
|
||||
func (r *PackageReconciler) updateDependentPackagesDependencies(ctx context.Context, packageName string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Get all Packages
|
||||
packageList := &cozyv1alpha1.PackageList{}
|
||||
if err := r.List(ctx, packageList); err != nil {
|
||||
return fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
|
||||
// Get the updated Package to check its readiness
|
||||
updatedPkg := &cozyv1alpha1.Package{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: packageName}, updatedPkg); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil // Package not found, nothing to update
|
||||
}
|
||||
return fmt.Errorf("failed to get Package %s: %w", packageName, err)
|
||||
}
|
||||
|
||||
// Check Ready condition of the updated Package
|
||||
readyCondition := meta.FindStatusCondition(updatedPkg.Status.Conditions, "Ready")
|
||||
isReady := readyCondition != nil && readyCondition.Status == metav1.ConditionTrue
|
||||
|
||||
// For each Package, check if it depends on the given Package
|
||||
for _, pkg := range packageList.Items {
|
||||
// Skip the Package itself
|
||||
if pkg.Name == packageName {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get variant
|
||||
variant, err := r.getVariantForPackage(ctx, &pkg, nil)
|
||||
if err != nil {
|
||||
// Continue if PackageSource or variant not found (best-effort operation)
|
||||
logger.V(1).Info("skipping package, failed to get variant", "package", pkg.Name, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this Package depends on the given Package
|
||||
dependsOn := false
|
||||
for _, dep := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == dep {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to get dependent Package %s: %w", depPackageName, err)
|
||||
}
|
||||
|
||||
// Get the PackageSource
|
||||
depPackageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("dependent PackageSource %s not found", depPackageName)
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to get dependent PackageSource %s: %w", depPackageName, err)
|
||||
}
|
||||
|
||||
// Get the variant from dependent Package
|
||||
depVariantName := depPackage.Spec.Variant
|
||||
if depVariantName == "" {
|
||||
depVariantName = "default"
|
||||
}
|
||||
|
||||
// Find the variant in PackageSource
|
||||
var depVariant *cozyv1alpha1.Variant
|
||||
for i := range depPackageSource.Spec.Variants {
|
||||
if depPackageSource.Spec.Variants[i].Name == depVariantName {
|
||||
depVariant = &depPackageSource.Spec.Variants[i]
|
||||
if dep == packageName {
|
||||
dependsOn = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if depVariant == nil {
|
||||
return fmt.Errorf("dependent variant %s not found in PackageSource %s", depVariantName, depPackageName)
|
||||
if dependsOn {
|
||||
// Update the dependency status in this Package
|
||||
if pkg.Status.Dependencies == nil {
|
||||
pkg.Status.Dependencies = make(map[string]cozyv1alpha1.DependencyStatus)
|
||||
}
|
||||
pkg.Status.Dependencies[packageName] = cozyv1alpha1.DependencyStatus{
|
||||
Ready: isReady,
|
||||
}
|
||||
if err := r.Status().Update(ctx, &pkg); err != nil {
|
||||
logger.V(1).Error(err, "failed to update dependency status for dependent Package", "package", pkg.Name, "dependency", packageName)
|
||||
continue
|
||||
}
|
||||
logger.V(1).Info("updated dependency status for dependent Package", "package", pkg.Name, "dependency", packageName, "ready", isReady)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,46 +810,16 @@ func (r *PackageReconciler) reconcileNamespaces(ctx context.Context, pkg *cozyv1
|
||||
return nil
|
||||
}
|
||||
|
||||
// createOrUpdateNamespace creates or updates a namespace
|
||||
// createOrUpdateNamespace creates or updates a namespace using server-side apply
|
||||
func (r *PackageReconciler) createOrUpdateNamespace(ctx context.Context, namespace *corev1.Namespace) error {
|
||||
existing := &corev1.Namespace{}
|
||||
key := types.NamespacedName{Name: namespace.Name}
|
||||
|
||||
err := r.Get(ctx, key, existing)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return r.Create(ctx, namespace)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preserve resource version
|
||||
namespace.SetResourceVersion(existing.GetResourceVersion())
|
||||
|
||||
// Merge labels
|
||||
labels := namespace.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetLabels() {
|
||||
if _, ok := labels[k]; !ok {
|
||||
labels[k] = v
|
||||
}
|
||||
}
|
||||
namespace.SetLabels(labels)
|
||||
|
||||
// Merge annotations
|
||||
annotations := namespace.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetAnnotations() {
|
||||
if _, ok := annotations[k]; !ok {
|
||||
annotations[k] = v
|
||||
}
|
||||
}
|
||||
namespace.SetAnnotations(annotations)
|
||||
|
||||
return r.Update(ctx, namespace)
|
||||
// Ensure TypeMeta is set for server-side apply
|
||||
namespace.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Namespace"))
|
||||
|
||||
// Use server-side apply with field manager
|
||||
// This is atomic and avoids race conditions from Get/Create/Update pattern
|
||||
// Labels and annotations will be merged automatically by the server
|
||||
// Each label/annotation key is treated as a separate field, so existing ones are preserved
|
||||
return r.Patch(ctx, namespace, client.Apply, client.FieldOwner("cozystack-package-controller"))
|
||||
}
|
||||
|
||||
// cleanupOrphanedHelmReleases removes HelmReleases that are no longer needed
|
||||
@@ -671,7 +842,8 @@ func (r *PackageReconciler) cleanupOrphanedHelmReleases(ctx context.Context, pkg
|
||||
|
||||
namespace := component.Install.Namespace
|
||||
if namespace == "" {
|
||||
namespace = "cozy-system"
|
||||
// Skip components with empty namespace (they shouldn't exist anyway)
|
||||
continue
|
||||
}
|
||||
|
||||
releaseName := component.Install.ReleaseName
|
||||
@@ -710,100 +882,12 @@ func (r *PackageReconciler) cleanupOrphanedHelmReleases(ctx context.Context, pkg
|
||||
return nil
|
||||
}
|
||||
|
||||
// triggerDependentPackages triggers reconcile for all Packages that depend on the given Package
|
||||
func (r *PackageReconciler) triggerDependentPackages(ctx context.Context, packageName string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Get all Packages
|
||||
packageList := &cozyv1alpha1.PackageList{}
|
||||
if err := r.List(ctx, packageList); err != nil {
|
||||
return fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
|
||||
// For each Package, check if it depends on the given Package
|
||||
for _, pkg := range packageList.Items {
|
||||
// Skip the Package itself
|
||||
if pkg.Name == packageName {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get PackageSource
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
logger.V(1).Error(err, "failed to get PackageSource", "package", pkg.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine variant
|
||||
variantName := pkg.Spec.Variant
|
||||
if variantName == "" {
|
||||
variantName = "default"
|
||||
}
|
||||
|
||||
// Find variant
|
||||
var variant *cozyv1alpha1.Variant
|
||||
for i := range packageSource.Spec.Variants {
|
||||
if packageSource.Spec.Variants[i].Name == variantName {
|
||||
variant = &packageSource.Spec.Variants[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if variant == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this Package depends on the given Package
|
||||
dependsOn := false
|
||||
for _, dep := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == dep {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
|
||||
if dep == packageName {
|
||||
dependsOn = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if dependsOn {
|
||||
logger.V(1).Info("triggering reconcile for dependent Package", "package", pkg.Name, "dependency", packageName)
|
||||
// Trigger reconcile by updating the Package (add annotation or just requeue)
|
||||
// We can't directly requeue from here, but we can update the Package to trigger reconcile
|
||||
// Actually, we can use the client to trigger an update, but that might cause infinite loop
|
||||
// Better approach: use event handler or just log and let the watch handle it
|
||||
// For now, we'll just log - the watch on PackageSource should handle it
|
||||
// But we need a way to trigger reconcile...
|
||||
// Let's add an annotation to trigger reconcile
|
||||
if pkg.Annotations == nil {
|
||||
pkg.Annotations = make(map[string]string)
|
||||
}
|
||||
pkg.Annotations["cozystack.io/trigger-reconcile"] = fmt.Sprintf("%d", metav1.Now().Unix())
|
||||
if err := r.Update(ctx, &pkg); err != nil {
|
||||
logger.V(1).Error(err, "failed to trigger reconcile for dependent Package", "package", pkg.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *PackageReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystack-package").
|
||||
For(&cozyv1alpha1.Package{}).
|
||||
Owns(&helmv2.HelmRelease{}).
|
||||
Watches(
|
||||
&cozyv1alpha1.PackageSource{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
@@ -843,41 +927,31 @@ func (r *PackageReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
if pkg.Name == updatedPkg.Name {
|
||||
continue // Skip the Package itself
|
||||
}
|
||||
// Get PackageSource to check dependencies
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := mgr.GetClient().Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
|
||||
// Get variant to check dependencies
|
||||
variant, err := r.getVariantForPackage(ctx, &pkg, mgr.GetClient())
|
||||
if err != nil {
|
||||
// Continue if PackageSource or variant not found
|
||||
continue
|
||||
}
|
||||
// Determine variant
|
||||
variantName := pkg.Spec.Variant
|
||||
if variantName == "" {
|
||||
variantName = "default"
|
||||
}
|
||||
// Find variant
|
||||
for _, variant := range packageSource.Spec.Variants {
|
||||
if variant.Name == variantName {
|
||||
// Check if this variant depends on updatedPkg
|
||||
for _, dep := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == dep {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
if dep == updatedPkg.Name {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: pkg.Name,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
// Check if this variant depends on updatedPkg
|
||||
for _, dep := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == dep {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
if dep == updatedPkg.Name {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: pkg.Name,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,9 +30,8 @@ import (
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
// PackageSourceReconciler reconciles PackageSource resources
|
||||
@@ -84,11 +83,21 @@ func (r *PackageSourceReconciler) reconcileArtifactGenerators(ctx context.Contex
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build library map from all variants
|
||||
// Map key is the library name (from lib.Name or extracted from path)
|
||||
// This allows components to reference libraries by name
|
||||
libraryMap := make(map[string]cozyv1alpha1.Library)
|
||||
// Namespace is always cozy-system
|
||||
namespace := "cozy-system"
|
||||
// ArtifactGenerator name is the package source name
|
||||
agName := packageSource.Name
|
||||
|
||||
// Collect all OutputArtifacts
|
||||
outputArtifacts := []sourcewatcherv1beta1.OutputArtifact{}
|
||||
|
||||
// Process all variants and their components
|
||||
for _, variant := range packageSource.Spec.Variants {
|
||||
// Build library map for this variant
|
||||
// Map key is the library name (from lib.Name or extracted from path)
|
||||
// This allows components in this variant to reference libraries by name
|
||||
// Libraries are scoped per variant to avoid conflicts between variants
|
||||
libraryMap := make(map[string]cozyv1alpha1.Library)
|
||||
for _, lib := range variant.Libraries {
|
||||
libName := lib.Name
|
||||
if libName == "" {
|
||||
@@ -100,18 +109,7 @@ func (r *PackageSourceReconciler) reconcileArtifactGenerators(ctx context.Contex
|
||||
libraryMap[libName] = lib
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace is always cozy-system
|
||||
namespace := "cozy-system"
|
||||
// ArtifactGenerator name is the package source name
|
||||
agName := packageSource.Name
|
||||
|
||||
// Collect all OutputArtifacts
|
||||
outputArtifacts := []sourcewatcherv1beta1.OutputArtifact{}
|
||||
|
||||
// Process all variants and their components
|
||||
for _, variant := range packageSource.Spec.Variants {
|
||||
for _, component := range variant.Components {
|
||||
// Skip components without path
|
||||
if component.Path == "" {
|
||||
@@ -209,10 +207,14 @@ func (r *PackageSourceReconciler) reconcileArtifactGenerators(ctx context.Contex
|
||||
}
|
||||
|
||||
// Set ownerReference
|
||||
gvk, err := apiutil.GVKForObject(packageSource, r.Scheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get GVK for PackageSource: %w", err)
|
||||
}
|
||||
ag.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: packageSource.APIVersion,
|
||||
Kind: packageSource.Kind,
|
||||
APIVersion: gvk.GroupVersion().String(),
|
||||
Kind: gvk.Kind,
|
||||
Name: packageSource.Name,
|
||||
UID: packageSource.UID,
|
||||
Controller: func() *bool { b := true; return &b }(),
|
||||
@@ -306,61 +308,23 @@ func (r *PackageSourceReconciler) buildSourceFilePath(sourceName, basePath, path
|
||||
return fmt.Sprintf("@%s/%s", sourceName, fullPath)
|
||||
}
|
||||
|
||||
// createOrUpdate creates or updates a resource
|
||||
// createOrUpdate creates or updates a resource using server-side apply
|
||||
func (r *PackageSourceReconciler) createOrUpdate(ctx context.Context, obj client.Object) error {
|
||||
existing := obj.DeepCopyObject().(client.Object)
|
||||
key := client.ObjectKeyFromObject(obj)
|
||||
|
||||
err := r.Get(ctx, key, existing)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return r.Create(ctx, obj)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preserve resource version
|
||||
obj.SetResourceVersion(existing.GetResourceVersion())
|
||||
// Merge labels and annotations
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetLabels() {
|
||||
if _, ok := labels[k]; !ok {
|
||||
labels[k] = v
|
||||
}
|
||||
}
|
||||
obj.SetLabels(labels)
|
||||
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetAnnotations() {
|
||||
if _, ok := annotations[k]; !ok {
|
||||
annotations[k] = v
|
||||
}
|
||||
}
|
||||
obj.SetAnnotations(annotations)
|
||||
|
||||
// For ArtifactGenerator, explicitly update Spec (OutputArtifacts and Sources)
|
||||
if ag, ok := obj.(*sourcewatcherv1beta1.ArtifactGenerator); ok {
|
||||
if existingAG, ok := existing.(*sourcewatcherv1beta1.ArtifactGenerator); ok {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.V(1).Info("updating ArtifactGenerator Spec", "name", ag.Name, "namespace", ag.Namespace,
|
||||
"outputArtifactCount", len(ag.Spec.OutputArtifacts))
|
||||
// Update Spec from obj (which contains the desired state with all OutputArtifacts)
|
||||
existingAG.Spec = ag.Spec
|
||||
// Preserve metadata updates we made above
|
||||
existingAG.SetLabels(ag.GetLabels())
|
||||
existingAG.SetAnnotations(ag.GetAnnotations())
|
||||
existingAG.SetOwnerReferences(ag.GetOwnerReferences())
|
||||
// Use existingAG for Update
|
||||
obj = existingAG
|
||||
// Ensure TypeMeta is set for server-side apply
|
||||
// Use type assertion to set GVK if the object supports it
|
||||
if runtimeObj, ok := obj.(runtime.Object); ok {
|
||||
gvk, err := apiutil.GVKForObject(obj, r.Scheme)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get GVK for object: %w", err)
|
||||
}
|
||||
runtimeObj.GetObjectKind().SetGroupVersionKind(gvk)
|
||||
}
|
||||
|
||||
return r.Update(ctx, obj)
|
||||
// Use server-side apply with field manager
|
||||
// This is atomic and avoids race conditions from Get/Create/Update pattern
|
||||
// Labels, annotations, and spec will be merged automatically by the server
|
||||
// Each field is treated separately, so existing ones are preserved
|
||||
return r.Patch(ctx, obj, client.Apply, client.FieldOwner("cozystack-packagesource-controller"))
|
||||
}
|
||||
|
||||
// updateStatus updates PackageSource status (variants and conditions from ArtifactGenerator)
|
||||
@@ -443,26 +407,7 @@ func (r *PackageSourceReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystack-packagesource").
|
||||
For(&cozyv1alpha1.PackageSource{}).
|
||||
Watches(
|
||||
&sourcewatcherv1beta1.ArtifactGenerator{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
ag, ok := obj.(*sourcewatcherv1beta1.ArtifactGenerator)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Find the PackageSource that owns this ArtifactGenerator by ownerReference
|
||||
for _, ownerRef := range ag.OwnerReferences {
|
||||
if ownerRef.Kind == "PackageSource" {
|
||||
return []reconcile.Request{{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: ownerRef.Name,
|
||||
},
|
||||
}}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
).
|
||||
Owns(&sourcewatcherv1beta1.ArtifactGenerator{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
|
||||
68
internal/template/template.go
Normal file
68
internal/template/template.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
tmpl "text/template"
|
||||
)
|
||||
|
||||
func Template[T any](obj *T, templateContext map[string]any) (*T, error) {
|
||||
b, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var unstructured any
|
||||
err = json.Unmarshal(b, &unstructured)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
templateFunc := func(in string) string {
|
||||
out, err := template(in, templateContext)
|
||||
if err != nil {
|
||||
return in
|
||||
}
|
||||
return out
|
||||
}
|
||||
unstructured = mapAtStrings(unstructured, templateFunc)
|
||||
b, err = json.Marshal(unstructured)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out T
|
||||
err = json.Unmarshal(b, &out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func mapAtStrings(v any, f func(string) string) any {
|
||||
switch x := v.(type) {
|
||||
case map[string]any:
|
||||
for k, val := range x {
|
||||
x[k] = mapAtStrings(val, f)
|
||||
}
|
||||
return x
|
||||
case []any:
|
||||
for i, val := range x {
|
||||
x[i] = mapAtStrings(val, f)
|
||||
}
|
||||
return x
|
||||
case string:
|
||||
return f(x)
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func template(in string, templateContext map[string]any) (string, error) {
|
||||
tpl, err := tmpl.New("this").Parse(in)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, templateContext); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
68
internal/template/template_test.go
Normal file
68
internal/template/template_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestTemplate_PodTemplateSpec(t *testing.T) {
|
||||
original := corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-pod",
|
||||
Labels: map[string]string{
|
||||
"app": "demo",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"note": "hello",
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "{{ .Release.Name }}",
|
||||
Image: "nginx:1.21",
|
||||
Args: []string{"--flag={{ .Values.value }}"},
|
||||
Env: []corev1.EnvVar{
|
||||
{
|
||||
Name: "FOO",
|
||||
Value: "{{ .Release.Namespace }}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
templateContext := map[string]any{
|
||||
"Release": map[string]any{
|
||||
"Name": "foo",
|
||||
"Namespace": "notdefault",
|
||||
},
|
||||
"Values": map[string]any{
|
||||
"value": 3,
|
||||
},
|
||||
}
|
||||
reference := *original.DeepCopy()
|
||||
reference.Spec.Containers[0].Name = "foo"
|
||||
reference.Spec.Containers[0].Args[0] = "--flag=3"
|
||||
reference.Spec.Containers[0].Env[0].Value = "notdefault"
|
||||
got, err := Template(&original, templateContext)
|
||||
if err != nil {
|
||||
t.Fatalf("Template returned error: %v", err)
|
||||
}
|
||||
b1, err := json.Marshal(reference)
|
||||
t.Logf("reference:\n%s", string(b1))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal reference value: %v", err)
|
||||
}
|
||||
b2, err := json.Marshal(got)
|
||||
t.Logf("got:\n%s", string(b2))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal transformed value: %v", err)
|
||||
}
|
||||
if string(b1) != string(b2) {
|
||||
t.Fatalf("transformed value not equal to reference value, expected: %s, got: %s", string(b1), string(b2))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
|
||||
{{- $seaweedfs := index $myNS.metadata.annotations "namespace.cozystack.io/seaweedfs" }}
|
||||
{{- $seaweedfs := .Values._namespace.seaweedfs }}
|
||||
apiVersion: objectstorage.k8s.io/v1alpha1
|
||||
kind: BucketClaim
|
||||
metadata:
|
||||
|
||||
@@ -3,10 +3,15 @@ kind: HelmRelease
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-system
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-bucket-application-default-bucket-system
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-bucket
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
@@ -16,5 +21,8 @@ spec:
|
||||
force: true
|
||||
remediation:
|
||||
retries: -1
|
||||
valuesFrom:
|
||||
- kind: Secret
|
||||
name: cozystack-values
|
||||
values:
|
||||
bucketName: {{ .Release.Name }}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
|
||||
{{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }}
|
||||
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
|
||||
|
||||
{{- if .Values.clickhouseKeeper.enabled }}
|
||||
apiVersion: "clickhouse-keeper.altinity.com/v1"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
|
||||
{{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }}
|
||||
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
|
||||
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }}
|
||||
{{- $passwords := dict }}
|
||||
{{- $users := .Values.users }}
|
||||
|
||||
@@ -50,9 +50,8 @@ spec:
|
||||
postgresUID: 999
|
||||
postgresGID: 999
|
||||
enableSuperuserAccess: true
|
||||
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
|
||||
{{- if $configMap }}
|
||||
{{- $rawConstraints := get $configMap.data "globalAppTopologySpreadConstraints" }}
|
||||
{{- if .Values._cluster.scheduling }}
|
||||
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
|
||||
{{- if $rawConstraints }}
|
||||
{{- $rawConstraints | fromYaml | toYaml | nindent 2 }}
|
||||
labelSelector:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" | default (dict "data" (dict)) }}
|
||||
{{- $clusterDomain := index $cozyConfig.data "cluster-domain" | default "cozy.local" }}
|
||||
{{- $clusterDomain := index .Values._cluster "cluster-domain" | default "cozy.local" }}
|
||||
---
|
||||
apiVersion: apps.foundationdb.org/v1beta2
|
||||
kind: FoundationDBCluster
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/pkg/controller/kubevirteps/kubevirteps_controller.go b/pkg/controller/kubevirteps/kubevirteps_controller.go
|
||||
index 53388eb8e..28644236f 100644
|
||||
index 53388eb8e..873060251 100644
|
||||
--- a/pkg/controller/kubevirteps/kubevirteps_controller.go
|
||||
+++ b/pkg/controller/kubevirteps/kubevirteps_controller.go
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
@@ -10,12 +10,17 @@ index 53388eb8e..28644236f 100644
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
@@ -669,35 +668,50 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
@@ -666,38 +665,62 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
// for extracting the nodes it does not matter what type of address we are dealing with
|
||||
// all nodes with an endpoint for a corresponding slice will be selected.
|
||||
nodeSet := sets.Set[string]{}
|
||||
+ hasEndpointsWithoutNodeName := false
|
||||
for _, slice := range tenantSlices {
|
||||
for _, endpoint := range slice.Endpoints {
|
||||
// find all unique nodes that correspond to an endpoint in a tenant slice
|
||||
+ if endpoint.NodeName == nil {
|
||||
+ klog.Warningf("Skipping endpoint without NodeName in slice %s/%s", slice.Namespace, slice.Name)
|
||||
+ hasEndpointsWithoutNodeName = true
|
||||
+ continue
|
||||
+ }
|
||||
nodeSet.Insert(*endpoint.NodeName)
|
||||
@@ -23,6 +28,13 @@ index 53388eb8e..28644236f 100644
|
||||
}
|
||||
|
||||
- klog.Infof("Desired nodes for service %s in namespace %s: %v", service.Name, service.Namespace, sets.List(nodeSet))
|
||||
+ // Fallback: if no endpoints with NodeName were found, but there are endpoints without NodeName,
|
||||
+ // distribute traffic to all VMIs (similar to ExternalTrafficPolicy=Cluster behavior)
|
||||
+ if nodeSet.Len() == 0 && hasEndpointsWithoutNodeName {
|
||||
+ klog.Infof("No endpoints with NodeName found for service %s/%s, falling back to all VMIs", service.Namespace, service.Name)
|
||||
+ return c.getAllVMIEndpoints()
|
||||
+ }
|
||||
+
|
||||
+ klog.Infof("Desired nodes for service %s/%s: %v", service.Namespace, service.Name, sets.List(nodeSet))
|
||||
|
||||
for _, node := range sets.List(nodeSet) {
|
||||
@@ -68,7 +80,7 @@ index 53388eb8e..28644236f 100644
|
||||
desiredEndpoints = append(desiredEndpoints, &discovery.Endpoint{
|
||||
Addresses: []string{i.IP},
|
||||
Conditions: discovery.EndpointConditions{
|
||||
@@ -705,9 +719,9 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
@@ -705,9 +728,9 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
Serving: &serving,
|
||||
Terminating: &terminating,
|
||||
},
|
||||
@@ -80,6 +92,71 @@ index 53388eb8e..28644236f 100644
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -716,6 +739,64 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
return desiredEndpoints
|
||||
}
|
||||
|
||||
+// getAllVMIEndpoints returns endpoints for all VMIs in the infra namespace.
|
||||
+// This is used as a fallback when tenant endpoints don't have NodeName specified,
|
||||
+// similar to ExternalTrafficPolicy=Cluster behavior where traffic is distributed to all nodes.
|
||||
+func (c *Controller) getAllVMIEndpoints() []*discovery.Endpoint {
|
||||
+ var endpoints []*discovery.Endpoint
|
||||
+
|
||||
+ // List all VMIs in the infra namespace
|
||||
+ vmiList, err := c.infraDynamic.
|
||||
+ Resource(kubevirtv1.VirtualMachineInstanceGroupVersionKind.GroupVersion().WithResource("virtualmachineinstances")).
|
||||
+ Namespace(c.infraNamespace).
|
||||
+ List(context.TODO(), metav1.ListOptions{})
|
||||
+ if err != nil {
|
||||
+ klog.Errorf("Failed to list VMIs in namespace %q: %v", c.infraNamespace, err)
|
||||
+ return endpoints
|
||||
+ }
|
||||
+
|
||||
+ for _, obj := range vmiList.Items {
|
||||
+ vmi := &kubevirtv1.VirtualMachineInstance{}
|
||||
+ err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, vmi)
|
||||
+ if err != nil {
|
||||
+ klog.Errorf("Failed to convert Unstructured to VirtualMachineInstance: %v", err)
|
||||
+ continue
|
||||
+ }
|
||||
+
|
||||
+ if vmi.Status.NodeName == "" {
|
||||
+ klog.Warningf("Skipping VMI %s/%s: NodeName is empty", vmi.Namespace, vmi.Name)
|
||||
+ continue
|
||||
+ }
|
||||
+ nodeNamePtr := &vmi.Status.NodeName
|
||||
+
|
||||
+ ready := vmi.Status.Phase == kubevirtv1.Running
|
||||
+ serving := vmi.Status.Phase == kubevirtv1.Running
|
||||
+ terminating := vmi.Status.Phase == kubevirtv1.Failed || vmi.Status.Phase == kubevirtv1.Succeeded
|
||||
+
|
||||
+ for _, i := range vmi.Status.Interfaces {
|
||||
+ if i.Name == "default" {
|
||||
+ if i.IP == "" {
|
||||
+ klog.Warningf("VMI %s/%s interface %q has no IP, skipping", vmi.Namespace, vmi.Name, i.Name)
|
||||
+ continue
|
||||
+ }
|
||||
+ endpoints = append(endpoints, &discovery.Endpoint{
|
||||
+ Addresses: []string{i.IP},
|
||||
+ Conditions: discovery.EndpointConditions{
|
||||
+ Ready: &ready,
|
||||
+ Serving: &serving,
|
||||
+ Terminating: &terminating,
|
||||
+ },
|
||||
+ NodeName: nodeNamePtr,
|
||||
+ })
|
||||
+ break
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ klog.Infof("Fallback: created %d endpoints from all VMIs in namespace %s", len(endpoints), c.infraNamespace)
|
||||
+ return endpoints
|
||||
+}
|
||||
+
|
||||
func (c *Controller) ensureEndpointSliceLabels(slice *discovery.EndpointSlice, svc *v1.Service) (map[string]string, bool) {
|
||||
labels := make(map[string]string)
|
||||
labelsChanged := false
|
||||
diff --git a/pkg/controller/kubevirteps/kubevirteps_controller_test.go b/pkg/controller/kubevirteps/kubevirteps_controller_test.go
|
||||
index 1c97035b4..d205d0bed 100644
|
||||
--- a/pkg/controller/kubevirteps/kubevirteps_controller_test.go
|
||||
@@ -1,7 +1,6 @@
|
||||
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
|
||||
{{- $etcd := index $myNS.metadata.annotations "namespace.cozystack.io/etcd" }}
|
||||
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
|
||||
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
|
||||
{{- $etcd := .Values._namespace.etcd }}
|
||||
{{- $ingress := .Values._namespace.ingress }}
|
||||
{{- $host := .Values._namespace.host }}
|
||||
{{- $kubevirtmachinetemplateNames := list }}
|
||||
{{- define "kubevirtmachinetemplate" -}}
|
||||
spec:
|
||||
@@ -31,9 +30,8 @@ spec:
|
||||
{{- end }}
|
||||
cluster.x-k8s.io/deployment-name: {{ $.Release.Name }}-{{ .groupName }}
|
||||
spec:
|
||||
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
|
||||
{{- if $configMap }}
|
||||
{{- $rawConstraints := get $configMap.data "globalAppTopologySpreadConstraints" }}
|
||||
{{- if .Values._cluster.scheduling }}
|
||||
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
|
||||
{{- if $rawConstraints }}
|
||||
{{- $rawConstraints | fromYaml | toYaml | nindent 10 }}
|
||||
labelSelector:
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cert-manager-crds
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-cert-manager-crds
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-cert-manager-crds
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cert-manager
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-cert-manager
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-cert-manager
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -22,10 +22,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cilium
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-cilium
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-cilium
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -13,10 +13,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: coredns
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-coredns
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-coredns
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
spec:
|
||||
interval: 5m
|
||||
releaseName: csi
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-kubevirt-csi-node
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-kubevirt-csi-node
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: fluxcd-operator
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-fluxcd-operator
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-fluxcd-operator
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
@@ -51,10 +56,15 @@ metadata:
|
||||
spec:
|
||||
interval: 5m
|
||||
releaseName: fluxcd
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-fluxcd
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-fluxcd
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: gateway-api-crds
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-gateway-api-crds
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-gateway-api-crds
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: gpu-operator
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-gpu-operator
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-gpu-operator
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -27,10 +27,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: ingress-nginx
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-ingress-nginx
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-ingress-nginx
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -7,10 +7,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: metrics-server
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-metrics-server
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-metrics-server
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
|
||||
{{- $targetTenant := index $myNS.metadata.annotations "namespace.cozystack.io/monitoring" }}
|
||||
{{- $targetTenant := .Values._namespace.monitoring }}
|
||||
{{- if .Values.addons.monitoringAgents.enabled }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
@@ -10,10 +9,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cozy-monitoring-agents
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-monitoring-agents
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-monitoring-agents
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -7,10 +7,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: prometheus-operator-crds
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-prometheus-operator-crds
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-prometheus-operator-crds
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: velero
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-velero
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-velero
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -9,10 +9,15 @@ metadata:
|
||||
spec:
|
||||
interval: 5m
|
||||
releaseName: vertical-pod-autoscaler-crds
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-vertical-pod-autoscaler-crds
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-vertical-pod-autoscaler-crds
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{{- define "cozystack.defaultVPAValues" -}}
|
||||
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
|
||||
{{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }}
|
||||
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
|
||||
{{- $targetTenant := index $myNS.metadata.annotations "namespace.cozystack.io/monitoring" }}
|
||||
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
|
||||
{{- $targetTenant := .Values._namespace.monitoring }}
|
||||
vpaForVPA: false
|
||||
vertical-pod-autoscaler:
|
||||
recommender:
|
||||
@@ -36,10 +34,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: vertical-pod-autoscaler
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-vertical-pod-autoscaler
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-vertical-pod-autoscaler
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cozy-victoria-metrics-operator
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-victoria-metrics-operator
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-victoria-metrics-operator
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -7,10 +7,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: vsnap-crd
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-volumesnapshot-crd
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-vsnap-crd
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
|
||||
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
|
||||
{{- $ingress := .Values._namespace.ingress }}
|
||||
{{- if and (eq .Values.addons.ingressNginx.exposeMethod "Proxied") .Values.addons.ingressNginx.hosts }}
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
|
||||
{{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }}
|
||||
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
|
||||
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }}
|
||||
{{- $passwords := dict }}
|
||||
|
||||
@@ -35,10 +34,15 @@ kind: HelmRelease
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-system
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-nats-application-default-nats-system
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-nats
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
@@ -48,6 +52,9 @@ spec:
|
||||
force: true
|
||||
remediation:
|
||||
retries: -1
|
||||
valuesFrom:
|
||||
- kind: Secret
|
||||
name: cozystack-values
|
||||
values:
|
||||
nats:
|
||||
container:
|
||||
|
||||
@@ -46,9 +46,8 @@ spec:
|
||||
|
||||
imageName: ghcr.io/cloudnative-pg/postgresql:{{ include "postgres.versionMap" $ | trimPrefix "v" }}
|
||||
enableSuperuserAccess: true
|
||||
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
|
||||
{{- if $configMap }}
|
||||
{{- $rawConstraints := get $configMap.data "globalAppTopologySpreadConstraints" }}
|
||||
{{- if .Values._cluster.scheduling }}
|
||||
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
|
||||
{{- if $rawConstraints }}
|
||||
{{- $rawConstraints | fromYaml | toYaml | nindent 2 }}
|
||||
labelSelector:
|
||||
|
||||
@@ -3,7 +3,7 @@ apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "tenant.name" . }}-cleanup
|
||||
namespace: {{ include "tenant.name" . }}
|
||||
namespace: cozy-system
|
||||
annotations:
|
||||
helm.sh/hook: pre-delete
|
||||
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
|
||||
@@ -39,13 +39,13 @@ roleRef:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: {{ include "tenant.name" . }}-cleanup
|
||||
namespace: {{ include "tenant.name" . }}
|
||||
namespace: cozy-system
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "tenant.name" . }}-cleanup
|
||||
namespace: {{ include "tenant.name" . }}
|
||||
namespace: cozy-system
|
||||
annotations:
|
||||
helm.sh/hook: pre-delete
|
||||
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
|
||||
|
||||
@@ -13,10 +13,15 @@ metadata:
|
||||
apps.cozystack.io/application.group: apps.cozystack.io
|
||||
apps.cozystack.io/application.name: etcd
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-etcd-application-default-etcd
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: etcd
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-extra
|
||||
namespace: cozy-public
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
@@ -26,4 +31,7 @@ spec:
|
||||
force: true
|
||||
remediation:
|
||||
retries: -1
|
||||
valuesFrom:
|
||||
- kind: Secret
|
||||
name: cozystack-values
|
||||
{{- end }}
|
||||
|
||||
@@ -12,10 +12,15 @@ metadata:
|
||||
apps.cozystack.io/application.group: apps.cozystack.io
|
||||
apps.cozystack.io/application.name: info
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-info-application-default-info
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: info
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-extra
|
||||
namespace: cozy-public
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
@@ -25,3 +30,6 @@ spec:
|
||||
force: true
|
||||
remediation:
|
||||
retries: -1
|
||||
valuesFrom:
|
||||
- kind: Secret
|
||||
name: cozystack-values
|
||||
|
||||
@@ -13,10 +13,15 @@ metadata:
|
||||
apps.cozystack.io/application.group: apps.cozystack.io
|
||||
apps.cozystack.io/application.name: ingress
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-ingress-application-default-ingress
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: ingress
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-extra
|
||||
namespace: cozy-public
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
@@ -26,4 +31,7 @@ spec:
|
||||
force: true
|
||||
remediation:
|
||||
retries: -1
|
||||
valuesFrom:
|
||||
- kind: Secret
|
||||
name: cozystack-values
|
||||
{{- end }}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
|
||||
{{- $oidcEnabled := index $cozyConfig.data "oidc-enabled" }}
|
||||
{{- $oidcEnabled := index .Values._cluster "oidc-enabled" }}
|
||||
{{- if eq $oidcEnabled "true" }}
|
||||
{{- if .Capabilities.APIVersions.Has "v1.edp.epam.com/v1" }}
|
||||
apiVersion: v1.edp.epam.com/v1
|
||||
|
||||
@@ -13,10 +13,15 @@ metadata:
|
||||
apps.cozystack.io/application.group: apps.cozystack.io
|
||||
apps.cozystack.io/application.name: monitoring
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-monitoring-application-default-monitoring
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: monitoring
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-extra
|
||||
namespace: cozy-public
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
@@ -26,4 +31,7 @@ spec:
|
||||
force: true
|
||||
remediation:
|
||||
retries: -1
|
||||
valuesFrom:
|
||||
- kind: Secret
|
||||
name: cozystack-values
|
||||
{{- end }}
|
||||
|
||||
@@ -1,46 +1,63 @@
|
||||
{{- define "cozystack.namespace-anotations" }}
|
||||
{{- $context := index . 0 }}
|
||||
{{- $existingNS := index . 1 }}
|
||||
{{- range $x := list "etcd" "monitoring" "ingress" "seaweedfs" }}
|
||||
{{- if (index $context.Values $x) }}
|
||||
namespace.cozystack.io/{{ $x }}: "{{ include "tenant.name" $context }}"
|
||||
{{- else }}
|
||||
namespace.cozystack.io/{{ $x }}: "{{ index $existingNS.metadata.annotations (printf "namespace.cozystack.io/%s" $x) | required (printf "namespace %s has no namespace.cozystack.io/%s annotation" $context.Release.Namespace $x) }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Lookup for namespace uid (needed for ownerReferences) */}}
|
||||
{{- $existingNS := lookup "v1" "Namespace" "" .Release.Namespace }}
|
||||
{{- if not $existingNS }}
|
||||
{{- fail (printf "error lookup existing namespace: %s" .Release.Namespace) }}
|
||||
{{- end }}
|
||||
|
||||
{{- if ne (include "tenant.name" .) "tenant-root" }}
|
||||
{{/* Compute namespace values once for use in both Secret and labels */}}
|
||||
{{- $tenantName := include "tenant.name" . }}
|
||||
{{- $parentNamespace := .Values._namespace | default dict }}
|
||||
{{- $parentHost := $parentNamespace.host | default "" }}
|
||||
|
||||
{{/* Compute host */}}
|
||||
{{- $computedHost := "" }}
|
||||
{{- if .Values.host }}
|
||||
{{- $computedHost = .Values.host }}
|
||||
{{- else if $parentHost }}
|
||||
{{- $computedHost = printf "%s.%s" (splitList "-" $tenantName | last) $parentHost }}
|
||||
{{- end }}
|
||||
|
||||
{{/* Compute service references */}}
|
||||
{{- $etcd := $parentNamespace.etcd | default "" }}
|
||||
{{- if .Values.etcd }}
|
||||
{{- $etcd = $tenantName }}
|
||||
{{- end }}
|
||||
|
||||
{{- $ingress := $parentNamespace.ingress | default "" }}
|
||||
{{- if .Values.ingress }}
|
||||
{{- $ingress = $tenantName }}
|
||||
{{- end }}
|
||||
|
||||
{{- $monitoring := $parentNamespace.monitoring | default "" }}
|
||||
{{- if .Values.monitoring }}
|
||||
{{- $monitoring = $tenantName }}
|
||||
{{- end }}
|
||||
|
||||
{{- $seaweedfs := $parentNamespace.seaweedfs | default "" }}
|
||||
{{- if .Values.seaweedfs }}
|
||||
{{- $seaweedfs = $tenantName }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: {{ include "tenant.name" . }}
|
||||
name: {{ $tenantName }}
|
||||
{{- if hasPrefix "tenant-" .Release.Namespace }}
|
||||
annotations:
|
||||
{{- if .Values.host }}
|
||||
namespace.cozystack.io/host: "{{ .Values.host }}"
|
||||
{{- else }}
|
||||
{{ $parentHost := index $existingNS.metadata.annotations "namespace.cozystack.io/host" | required (printf "namespace %s has no namespace.cozystack.io/host annotation" .Release.Namespace) }}
|
||||
namespace.cozystack.io/host: "{{ splitList "-" (include "tenant.name" .) | last }}.{{ $parentHost }}"
|
||||
{{- end }}
|
||||
{{- include "cozystack.namespace-anotations" (list . $existingNS) | nindent 4 }}
|
||||
labels:
|
||||
tenant.cozystack.io/{{ include "tenant.name" $ }}: ""
|
||||
{{- if hasPrefix "tenant-" .Release.Namespace }}
|
||||
tenant.cozystack.io/{{ $tenantName }}: ""
|
||||
{{- $parts := splitList "-" .Release.Namespace }}
|
||||
{{- range $i, $v := $parts }}
|
||||
{{- if ne $i 0 }}
|
||||
tenant.cozystack.io/{{ join "-" (slice $parts 0 (add $i 1)) }}: ""
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- include "cozystack.namespace-anotations" (list $ $existingNS) | nindent 4 }}
|
||||
{{/* Labels for network policies */}}
|
||||
namespace.cozystack.io/etcd: {{ $etcd | quote }}
|
||||
namespace.cozystack.io/ingress: {{ $ingress | quote }}
|
||||
namespace.cozystack.io/monitoring: {{ $monitoring | quote }}
|
||||
namespace.cozystack.io/seaweedfs: {{ $seaweedfs | quote }}
|
||||
namespace.cozystack.io/host: {{ $computedHost | quote }}
|
||||
alpha.kubevirt.io/auto-memory-limits-ratio: "1.0"
|
||||
ownerReferences:
|
||||
- apiVersion: v1
|
||||
@@ -50,4 +67,23 @@ metadata:
|
||||
name: {{ .Release.Namespace }}
|
||||
uid: {{ $existingNS.metadata.uid }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: cozystack-values
|
||||
namespace: {{ $tenantName }}
|
||||
labels:
|
||||
reconcile.fluxcd.io/watch: Enabled
|
||||
type: Opaque
|
||||
stringData:
|
||||
values.yaml: |
|
||||
_cluster:
|
||||
{{- .Values._cluster | toYaml | nindent 6 }}
|
||||
_namespace:
|
||||
etcd: {{ $etcd | quote }}
|
||||
ingress: {{ $ingress | quote }}
|
||||
monitoring: {{ $monitoring | quote }}
|
||||
seaweedfs: {{ $seaweedfs | quote }}
|
||||
host: {{ $computedHost | quote }}
|
||||
{{- end }}
|
||||
|
||||
@@ -67,6 +67,19 @@ spec:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if ne (include "tenant.name" .) "tenant-root" }}
|
||||
- toEndpoints:
|
||||
{{- if hasPrefix "tenant-" .Release.Namespace }}
|
||||
{{- $parts := splitList "-" .Release.Namespace }}
|
||||
{{- range $i, $v := $parts }}
|
||||
{{- if ne $i 0 }}
|
||||
- matchLabels:
|
||||
cozystack.io/service: ingress
|
||||
"k8s:io.kubernetes.pod.namespace": {{ join "-" (slice $parts 0 (add $i 1)) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: cilium.io/v2
|
||||
kind: CiliumClusterwideNetworkPolicy
|
||||
|
||||
@@ -13,10 +13,15 @@ metadata:
|
||||
apps.cozystack.io/application.group: apps.cozystack.io
|
||||
apps.cozystack.io/application.name: seaweedfs
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-seaweedfs-application-default-seaweedfs
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: seaweedfs
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-extra
|
||||
namespace: cozy-public
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
@@ -26,4 +31,7 @@ spec:
|
||||
force: true
|
||||
remediation:
|
||||
retries: -1
|
||||
valuesFrom:
|
||||
- kind: Secret
|
||||
name: cozystack-values
|
||||
{{- end }}
|
||||
|
||||
@@ -69,3 +69,35 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade.
|
||||
{{- end }}
|
||||
{{- $uuid }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Node Affinity for Windows VMs
|
||||
*/}}
|
||||
{{- define "virtual-machine.nodeAffinity" -}}
|
||||
{{- if .Values._cluster.scheduling -}}
|
||||
{{- $dedicatedNodesForWindowsVMs := get .Values._cluster.scheduling "dedicatedNodesForWindowsVMs" -}}
|
||||
{{- if eq $dedicatedNodesForWindowsVMs "true" -}}
|
||||
{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}}
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
{{- if $isWindows }}
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: In
|
||||
values:
|
||||
- "true"
|
||||
{{- else }}
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: NotIn
|
||||
values:
|
||||
- "true"
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{{- if .Values.external }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@@ -7,17 +6,24 @@ metadata:
|
||||
labels:
|
||||
apps.cozystack.io/user-service: "true"
|
||||
{{- include "virtual-machine.labels" . | nindent 4 }}
|
||||
{{- if .Values.external }}
|
||||
annotations:
|
||||
networking.cozystack.io/wholeIP: "true"
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }}
|
||||
{{- if .Values.external }}
|
||||
externalTrafficPolicy: Local
|
||||
{{- if ((include "cozy-lib.network.disableLoadBalancerNodePorts" $) | fromYaml) }}
|
||||
allocateLoadBalancerNodePorts: false
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
clusterIP: None
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "virtual-machine.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
{{- if .Values.external }}
|
||||
{{- if and (eq .Values.externalMethod "WholeIP") (not .Values.externalPorts) }}
|
||||
- port: 65535
|
||||
{{- else }}
|
||||
@@ -27,4 +33,6 @@ spec:
|
||||
targetPort: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
- port: 65535
|
||||
{{- end }}
|
||||
|
||||
@@ -124,6 +124,8 @@ spec:
|
||||
|
||||
terminationGracePeriodSeconds: 30
|
||||
|
||||
{{- include "virtual-machine.nodeAffinity" . | nindent 6 }}
|
||||
|
||||
volumes:
|
||||
- name: systemdisk
|
||||
dataVolume:
|
||||
|
||||
@@ -69,3 +69,35 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade.
|
||||
{{- end }}
|
||||
{{- $uuid }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Node Affinity for Windows VMs
|
||||
*/}}
|
||||
{{- define "virtual-machine.nodeAffinity" -}}
|
||||
{{- if .Values._cluster.scheduling -}}
|
||||
{{- $dedicatedNodesForWindowsVMs := get .Values._cluster.scheduling "dedicatedNodesForWindowsVMs" -}}
|
||||
{{- if eq $dedicatedNodesForWindowsVMs "true" -}}
|
||||
{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}}
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
{{- if $isWindows }}
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: In
|
||||
values:
|
||||
- "true"
|
||||
{{- else }}
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: NotIn
|
||||
values:
|
||||
- "true"
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{{- if .Values.external }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@@ -7,17 +6,24 @@ metadata:
|
||||
labels:
|
||||
apps.cozystack.io/user-service: "true"
|
||||
{{- include "virtual-machine.labels" . | nindent 4 }}
|
||||
{{- if .Values.external }}
|
||||
annotations:
|
||||
networking.cozystack.io/wholeIP: "true"
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }}
|
||||
{{- if .Values.external }}
|
||||
externalTrafficPolicy: Local
|
||||
{{- if ((include "cozy-lib.network.disableLoadBalancerNodePorts" $) | fromYaml) }}
|
||||
allocateLoadBalancerNodePorts: false
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
clusterIP: None
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "virtual-machine.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
{{- if .Values.external }}
|
||||
{{- if and (eq .Values.externalMethod "WholeIP") (not .Values.externalPorts) }}
|
||||
- port: 65535
|
||||
{{- else }}
|
||||
@@ -27,4 +33,6 @@ spec:
|
||||
targetPort: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
- port: 65535
|
||||
{{- end }}
|
||||
|
||||
@@ -95,6 +95,9 @@ spec:
|
||||
noCloud: {}
|
||||
{{- end }}
|
||||
terminationGracePeriodSeconds: 30
|
||||
|
||||
{{- include "virtual-machine.nodeAffinity" . | nindent 6 }}
|
||||
|
||||
volumes:
|
||||
{{- range .Values.disks }}
|
||||
- name: disk-{{ .name }}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
|
||||
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
|
||||
{{- $host := .Values._namespace.host }}
|
||||
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-vpn" .Release.Name) }}
|
||||
{{- $accessKeys := list }}
|
||||
{{- $passwords := dict }}
|
||||
|
||||
@@ -4,18 +4,30 @@ NAMESPACE=cozy-$(NAME)
|
||||
include ../../../scripts/common-envs.mk
|
||||
|
||||
show:
|
||||
cozypkg show -n $(NAMESPACE) $(NAME) --plain
|
||||
cozyhr show -n $(NAMESPACE) $(NAME) --plain
|
||||
|
||||
apply:
|
||||
cozypkg show -n $(NAMESPACE) $(NAME) --plain | kubectl apply -f- --server-side --force-conflicts
|
||||
cozyhr show -n $(NAMESPACE) $(NAME) --plain | kubectl apply -f- --server-side --force-conflicts
|
||||
|
||||
diff:
|
||||
cozypkg show -n $(NAMESPACE) $(NAME) --plain | kubectl diff -f-
|
||||
cozyhr show -n $(NAMESPACE) $(NAME) --plain | kubectl diff -f-
|
||||
|
||||
update:
|
||||
timoni bundle build -f flux-aio.cue > manifests/fluxcd.yaml
|
||||
yq eval '(select(.kind == "Namespace") | .metadata.labels."pod-security.kubernetes.io/enforce") = "privileged"' -i manifests/fluxcd.yaml
|
||||
sed -i manifests/fluxcd.yaml \
|
||||
update: update-old update-new
|
||||
|
||||
# TODO: remove old manifest after migration to cozystack-operator
|
||||
update-old:
|
||||
timoni bundle build -f flux-aio.cue > templates/fluxcd.yaml
|
||||
yq eval '(select(.kind == "Namespace") | .metadata.labels."pod-security.kubernetes.io/enforce") = "privileged"' -i templates/fluxcd.yaml
|
||||
sed -i templates/fluxcd.yaml \
|
||||
-e '/timoni/d' \
|
||||
-e 's|\.cluster\.local\.,||g' -e 's|\.cluster\.local\,||g' -e 's|\.cluster\.local\.||g' \
|
||||
-e '/value: .svc/a \ {{- include "cozy.kubernetes_envs" . | nindent 12 }}' \
|
||||
-e '/hostNetwork: true/i \ dnsPolicy: ClusterFirstWithHostNet'
|
||||
|
||||
update-new:
|
||||
timoni bundle build -f flux-aio.cue > ../../../internal/fluxinstall/manifests/fluxcd.yaml
|
||||
yq eval '(select(.kind == "Namespace") | .metadata.labels."pod-security.kubernetes.io/enforce") = "privileged"' -i ../../../internal/fluxinstall/manifests/fluxcd.yaml
|
||||
sed -i ../../../internal/fluxinstall/manifests/fluxcd.yaml \
|
||||
-e '/timoni/d' \
|
||||
-e 's|\.cluster\.local\.,||g' -e 's|\.cluster\.local\,||g' -e 's|\.cluster\.local\.||g'
|
||||
# TODO: solve dns issue with hostNetwork for installing helmreleases in tenant k8s clusters
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user