mirror of
https://github.com/cozystack/cozystack.git
synced 2026-03-04 05:58:53 +00:00
Compare commits
48 Commits
v0.41.1
...
refactor-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f0e042eac | ||
|
|
66ab048612 | ||
|
|
cc52c69922 | ||
|
|
4270d66376 | ||
|
|
2ca68eda69 | ||
|
|
9db99f7233 | ||
|
|
a89dd819ff | ||
|
|
657bddaeb9 | ||
|
|
51d0001589 | ||
|
|
e0ec967120 | ||
|
|
b77791a5fe | ||
|
|
3d9cfee401 | ||
|
|
e046206d2b | ||
|
|
c69756de51 | ||
|
|
15a9180b67 | ||
|
|
451ef73172 | ||
|
|
2077b0e515 | ||
|
|
aaf2d1326a | ||
|
|
ea1d0363d1 | ||
|
|
45bd323c6e | ||
|
|
b328124be7 | ||
|
|
35086bc362 | ||
|
|
7b28139ad9 | ||
|
|
5883fbf7ea | ||
|
|
167e85004c | ||
|
|
7fc458d136 | ||
|
|
bb220647ad | ||
|
|
a4cb9ae30b | ||
|
|
982727ac91 | ||
|
|
6c3a7b7efb | ||
|
|
923dbd209d | ||
|
|
c23826efac | ||
|
|
36119cec45 | ||
|
|
f98b429ad2 | ||
|
|
8a0935fb37 | ||
|
|
5dc9f590cf | ||
|
|
17286ad213 | ||
|
|
ea9d44b4af | ||
|
|
7c2bec197b | ||
|
|
4b1525a5f8 | ||
|
|
2113d17a54 | ||
|
|
4f97aef04c | ||
|
|
4b5d777b81 | ||
|
|
75197c6d25 | ||
|
|
c808ed6f24 | ||
|
|
222b582b68 | ||
|
|
2a87c83043 | ||
|
|
e5b65e8e77 |
188
.github/workflows/auto-release.yaml
vendored
188
.github/workflows/auto-release.yaml
vendored
@@ -1,188 +0,0 @@
|
||||
name: Auto Patch Release
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at 2:00 AM CET (1:00 UTC in winter, 0:00 UTC in summer)
|
||||
# Using 1:00 UTC to approximate 2:00 AM CET
|
||||
- cron: '0 1 * * *'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
concurrency:
|
||||
group: auto-release-${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
auto-release:
|
||||
name: Auto Patch Release
|
||||
runs-on: [self-hosted]
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Configure git
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
git config user.name "cozystack-bot"
|
||||
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
|
||||
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
|
||||
git config --unset-all http.https://github.com/.extraheader || true
|
||||
|
||||
- name: Process release branches
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_PAT }}
|
||||
script: |
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Configure git to use PAT for authentication
|
||||
execSync('git config user.name "cozystack-bot"', { encoding: 'utf8' });
|
||||
execSync('git config user.email "217169706+cozystack-bot@users.noreply.github.com"', { encoding: 'utf8' });
|
||||
execSync(`git remote set-url origin https://cozystack-bot:${process.env.GH_PAT}@github.com/${process.env.GITHUB_REPOSITORY}`, { encoding: 'utf8' });
|
||||
// Remove GITHUB_TOKEN extraheader to ensure PAT is used (needed to trigger other workflows)
|
||||
execSync('git config --unset-all http.https://github.com/.extraheader || true', { encoding: 'utf8' });
|
||||
|
||||
// Get all release-X.Y branches
|
||||
const branches = execSync('git branch -r | grep -E "origin/release-[0-9]+\\.[0-9]+$" | sed "s|origin/||" | tr -d " "', { encoding: 'utf8' })
|
||||
.split('\n')
|
||||
.filter(b => b.trim())
|
||||
.filter(b => /^release-\d+\.\d+$/.test(b));
|
||||
|
||||
console.log(`Found ${branches.length} release branches: ${branches.join(', ')}`);
|
||||
|
||||
// Get all published releases (not draft)
|
||||
const allReleases = await github.rest.repos.listReleases({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
// Filter to only published releases (not draft) with tags matching vX.Y.Z (no suffixes)
|
||||
const publishedReleases = allReleases.data
|
||||
.filter(r => !r.draft)
|
||||
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name));
|
||||
|
||||
console.log(`Found ${publishedReleases.length} published releases without suffixes`);
|
||||
|
||||
for (const branch of branches) {
|
||||
console.log(`\n=== Processing branch: ${branch} ===`);
|
||||
|
||||
// Extract X.Y from branch name (release-X.Y)
|
||||
const match = branch.match(/^release-(\d+\.\d+)$/);
|
||||
if (!match) {
|
||||
console.log(` ⚠️ Branch ${branch} doesn't match pattern, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [major, minor] = match[1].split('.');
|
||||
const versionPrefix = `v${major}.${minor}.`;
|
||||
|
||||
console.log(` Looking for releases with prefix: ${versionPrefix}`);
|
||||
|
||||
// Find the latest published release for this branch (vX.Y.Z without suffixes)
|
||||
const branchReleases = publishedReleases
|
||||
.filter(r => r.tag_name.startsWith(versionPrefix))
|
||||
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name)); // Ensure no suffixes
|
||||
|
||||
if (branchReleases.length === 0) {
|
||||
console.log(` ⚠️ No published releases found for ${branch}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sort by version (descending) to get the latest
|
||||
branchReleases.sort((a, b) => {
|
||||
const aVersion = a.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
|
||||
const bVersion = b.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!aVersion || !bVersion) return 0;
|
||||
|
||||
const aNum = parseInt(aVersion[1]) * 10000 + parseInt(aVersion[2]) * 100 + parseInt(aVersion[3]);
|
||||
const bNum = parseInt(bVersion[1]) * 10000 + parseInt(bVersion[2]) * 100 + parseInt(bVersion[3]);
|
||||
return bNum - aNum;
|
||||
});
|
||||
|
||||
const latestRelease = branchReleases[0];
|
||||
console.log(` ✅ Latest published release: ${latestRelease.tag_name}`);
|
||||
|
||||
// Get the commit SHA for this release tag
|
||||
let releaseCommitSha;
|
||||
try {
|
||||
releaseCommitSha = execSync(`git rev-list -n 1 ${latestRelease.tag_name}`, { encoding: 'utf8' }).trim();
|
||||
console.log(` Release commit SHA: ${releaseCommitSha}`);
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Could not find commit for tag ${latestRelease.tag_name}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Checkout the branch
|
||||
execSync(`git fetch origin ${branch}:${branch}`, { encoding: 'utf8' });
|
||||
execSync(`git checkout ${branch}`, { encoding: 'utf8' });
|
||||
|
||||
// Get the latest commit on the branch
|
||||
const latestBranchCommit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
|
||||
console.log(` Latest branch commit: ${latestBranchCommit}`);
|
||||
|
||||
// Check if there are new commits after the release
|
||||
const commitsAfterRelease = execSync(
|
||||
`git rev-list ${releaseCommitSha}..HEAD --oneline`,
|
||||
{ encoding: 'utf8' }
|
||||
).trim();
|
||||
|
||||
if (!commitsAfterRelease) {
|
||||
console.log(` ℹ️ No new commits after ${latestRelease.tag_name}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(` ✅ Found new commits after release:`);
|
||||
console.log(commitsAfterRelease);
|
||||
|
||||
// Calculate next version (Z+1)
|
||||
const versionMatch = latestRelease.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!versionMatch) {
|
||||
console.log(` ❌ Could not parse version from ${latestRelease.tag_name}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextPatch = parseInt(versionMatch[3]) + 1;
|
||||
const nextTag = `v${versionMatch[1]}.${versionMatch[2]}.${nextPatch}`;
|
||||
|
||||
console.log(` 🏷️ Creating new tag: ${nextTag} on commit ${latestBranchCommit}`);
|
||||
|
||||
// Create and push the tag with base_ref for workflow triggering
|
||||
try {
|
||||
// Delete local tag if exists to force update
|
||||
try {
|
||||
execSync(`git tag -d ${nextTag}`, { encoding: 'utf8' });
|
||||
} catch (e) {
|
||||
// Tag doesn't exist locally, that's fine
|
||||
}
|
||||
|
||||
// Delete remote tag if exists
|
||||
try {
|
||||
execSync(`git push origin :refs/tags/${nextTag}`, { encoding: 'utf8' });
|
||||
} catch (e) {
|
||||
// Tag doesn't exist remotely, that's fine
|
||||
}
|
||||
|
||||
// Create tag locally
|
||||
execSync(`git tag ${nextTag} ${latestBranchCommit}`, { encoding: 'utf8' });
|
||||
|
||||
// Push tag with HEAD reference to preserve base_ref
|
||||
execSync(`git push origin HEAD:refs/tags/${nextTag}`, { encoding: 'utf8' });
|
||||
console.log(` ✅ Successfully created and pushed tag ${nextTag}`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error creating/pushing tag ${nextTag}: ${error.message}`);
|
||||
core.setFailed(`Failed to create tag ${nextTag} for branch ${branch}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Finished processing all release branches`);
|
||||
|
||||
104
.github/workflows/backport.yaml
vendored
104
.github/workflows/backport.yaml
vendored
@@ -2,7 +2,7 @@ name: Automatic Backport
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed, labeled] # fires when PR is closed (merged) or labeled
|
||||
types: [closed] # fires when PR is closed (merged)
|
||||
|
||||
concurrency:
|
||||
group: backport-${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
@@ -13,46 +13,22 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
# Determine which backports are needed
|
||||
prepare:
|
||||
backport:
|
||||
if: |
|
||||
github.event.pull_request.merged == true &&
|
||||
(
|
||||
contains(github.event.pull_request.labels.*.name, 'backport') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'backport-previous') ||
|
||||
(github.event.action == 'labeled' && (github.event.label.name == 'backport' || github.event.label.name == 'backport-previous'))
|
||||
)
|
||||
contains(github.event.pull_request.labels.*.name, 'backport')
|
||||
runs-on: [self-hosted]
|
||||
outputs:
|
||||
backport_current: ${{ steps.labels.outputs.backport }}
|
||||
backport_previous: ${{ steps.labels.outputs.backport_previous }}
|
||||
current_branch: ${{ steps.branches.outputs.current_branch }}
|
||||
previous_branch: ${{ steps.branches.outputs.previous_branch }}
|
||||
|
||||
steps:
|
||||
- name: Check which labels are present
|
||||
id: labels
|
||||
# 1. Decide which maintenance branch should receive the back‑port
|
||||
- name: Determine target maintenance branch
|
||||
id: target
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
const labels = pr.labels.map(l => l.name);
|
||||
const isBackport = labels.includes('backport');
|
||||
const isBackportPrevious = labels.includes('backport-previous');
|
||||
|
||||
core.setOutput('backport', isBackport ? 'true' : 'false');
|
||||
core.setOutput('backport_previous', isBackportPrevious ? 'true' : 'false');
|
||||
|
||||
console.log(`backport label: ${isBackport}, backport-previous label: ${isBackportPrevious}`);
|
||||
|
||||
- name: Determine target branches
|
||||
id: branches
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// Get latest release
|
||||
let latestRelease;
|
||||
let rel;
|
||||
try {
|
||||
latestRelease = await github.rest.repos.getLatestRelease({
|
||||
rel = await github.rest.repos.getLatestRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
@@ -60,70 +36,18 @@ jobs:
|
||||
core.setFailed('No existing releases found; cannot determine backport target.');
|
||||
return;
|
||||
}
|
||||
|
||||
const [maj, min] = latestRelease.data.tag_name.replace(/^v/, '').split('.');
|
||||
const currentBranch = `release-${maj}.${min}`;
|
||||
const prevMin = parseInt(min) - 1;
|
||||
const previousBranch = prevMin >= 0 ? `release-${maj}.${prevMin}` : '';
|
||||
|
||||
core.setOutput('current_branch', currentBranch);
|
||||
core.setOutput('previous_branch', previousBranch);
|
||||
|
||||
console.log(`Current branch: ${currentBranch}, Previous branch: ${previousBranch || 'N/A'}`);
|
||||
|
||||
// Verify previous branch exists if we need it
|
||||
if (previousBranch && '${{ steps.labels.outputs.backport_previous }}' === 'true') {
|
||||
try {
|
||||
await github.rest.repos.getBranch({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
branch: previousBranch
|
||||
});
|
||||
console.log(`Previous branch ${previousBranch} exists`);
|
||||
} catch (e) {
|
||||
core.setFailed(`Previous branch ${previousBranch} does not exist.`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
backport:
|
||||
needs: prepare
|
||||
if: |
|
||||
github.event.pull_request.merged == true &&
|
||||
(needs.prepare.outputs.backport_current == 'true' || needs.prepare.outputs.backport_previous == 'true')
|
||||
runs-on: [self-hosted]
|
||||
strategy:
|
||||
matrix:
|
||||
backport_type: [current, previous]
|
||||
steps:
|
||||
# 1. Determine target branch based on matrix
|
||||
- name: Set target branch
|
||||
id: target
|
||||
if: |
|
||||
(matrix.backport_type == 'current' && needs.prepare.outputs.backport_current == 'true') ||
|
||||
(matrix.backport_type == 'previous' && needs.prepare.outputs.backport_previous == 'true')
|
||||
run: |
|
||||
if [ "${{ matrix.backport_type }}" == "current" ]; then
|
||||
echo "branch=${{ needs.prepare.outputs.current_branch }}" >> $GITHUB_OUTPUT
|
||||
echo "Target branch: ${{ needs.prepare.outputs.current_branch }}"
|
||||
else
|
||||
echo "branch=${{ needs.prepare.outputs.previous_branch }}" >> $GITHUB_OUTPUT
|
||||
echo "Target branch: ${{ needs.prepare.outputs.previous_branch }}"
|
||||
fi
|
||||
|
||||
const [maj, min] = rel.data.tag_name.replace(/^v/, '').split('.');
|
||||
const branch = `release-${maj}.${min}`;
|
||||
core.setOutput('branch', branch);
|
||||
console.log(`Latest release ${rel.data.tag_name}; backporting to ${branch}`);
|
||||
# 2. Checkout (required by backport‑action)
|
||||
- name: Checkout repository
|
||||
if: steps.target.outcome == 'success'
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 3. Create the back‑port pull request
|
||||
- name: Create back‑port PR
|
||||
id: backport
|
||||
if: steps.target.outcome == 'success'
|
||||
uses: korthout/backport-action@v3.2.1
|
||||
uses: korthout/backport-action@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
label_pattern: '' # don't read labels for targets
|
||||
target_branches: ${{ steps.target.outputs.branch }}
|
||||
merge_commits: skip
|
||||
conflict_resolution: draft_commit_conflicts
|
||||
|
||||
2
.github/workflows/pre-commit.yml
vendored
2
.github/workflows/pre-commit.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
|
||||
- name: Install generate
|
||||
run: |
|
||||
curl -sSL https://github.com/cozystack/cozyvalues-gen/releases/download/v1.0.6/cozyvalues-gen-linux-amd64.tar.gz | tar -xzvf- -C /usr/local/bin/ cozyvalues-gen
|
||||
curl -sSL https://github.com/cozystack/cozyvalues-gen/releases/download/v1.0.5/cozyvalues-gen-linux-amd64.tar.gz | tar -xzvf- -C /usr/local/bin/ cozyvalues-gen
|
||||
|
||||
- name: Run pre-commit hooks
|
||||
run: |
|
||||
|
||||
127
.github/workflows/pull-requests-release.yaml
vendored
127
.github/workflows/pull-requests-release.yaml
vendored
@@ -46,12 +46,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create tag on merge commit
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
git config user.name "cozystack-bot"
|
||||
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
|
||||
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
|
||||
git tag -f ${{ steps.get_tag.outputs.tag }} ${{ github.sha }}
|
||||
git push -f origin ${{ steps.get_tag.outputs.tag }}
|
||||
|
||||
@@ -110,95 +105,67 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
# Publish draft release and ensure correct latest flag
|
||||
- name: Publish draft release
|
||||
# Get the latest published release
|
||||
- name: Get the latest published release
|
||||
id: latest_release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const tag = '${{ steps.get_tag.outputs.tag }}';
|
||||
try {
|
||||
const rel = await github.rest.repos.getLatestRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
core.setOutput('tag', rel.data.tag_name);
|
||||
} catch (_) {
|
||||
core.setOutput('tag', '');
|
||||
}
|
||||
|
||||
# Compare current tag vs latest using semver-utils
|
||||
- name: Semver compare
|
||||
id: semver
|
||||
uses: madhead/semver-utils@v4.3.0
|
||||
with:
|
||||
version: ${{ steps.get_tag.outputs.tag }}
|
||||
compare-to: ${{ steps.latest_release.outputs.tag }}
|
||||
|
||||
# Derive flags: prerelease? make_latest?
|
||||
- name: Calculate publish flags
|
||||
id: flags
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const tag = '${{ steps.get_tag.outputs.tag }}'; // v0.31.5-rc.1
|
||||
const m = tag.match(/^v(\d+\.\d+\.\d+)(-(?:alpha|beta|rc)\.\d+)?$/);
|
||||
if (!m) {
|
||||
core.setFailed(`❌ tag '${tag}' must match 'vX.Y.Z' or 'vX.Y.Z-(alpha|beta|rc).N'`);
|
||||
return;
|
||||
}
|
||||
const isRc = Boolean(m[2]);
|
||||
const version = m[1] + (m[2] ?? ''); // 0.31.5-rc.1
|
||||
const isRc = Boolean(m[2]);
|
||||
core.setOutput('is_rc', isRc);
|
||||
const outdated = '${{ steps.semver.outputs.comparison-result }}' === '<';
|
||||
core.setOutput('make_latest', isRc || outdated ? 'false' : 'legacy');
|
||||
|
||||
// Parse semver string to comparable numbers
|
||||
function parseSemver(v) {
|
||||
const match = v.replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
major: parseInt(match[1]),
|
||||
minor: parseInt(match[2]),
|
||||
patch: parseInt(match[3])
|
||||
};
|
||||
}
|
||||
|
||||
// Compare two semver objects
|
||||
function compareSemver(a, b) {
|
||||
if (a.major !== b.major) return a.major - b.major;
|
||||
if (a.minor !== b.minor) return a.minor - b.minor;
|
||||
return a.patch - b.patch;
|
||||
}
|
||||
|
||||
const currentSemver = parseSemver(tag);
|
||||
|
||||
// Get all releases
|
||||
# Publish draft release with correct flags
|
||||
- name: Publish draft release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const tag = '${{ steps.get_tag.outputs.tag }}';
|
||||
const releases = await github.rest.repos.listReleases({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 100
|
||||
repo: context.repo.repo
|
||||
});
|
||||
|
||||
// Find draft release to publish
|
||||
const draft = releases.data.find(r => r.tag_name === tag && r.draft);
|
||||
if (!draft) throw new Error(`Draft release for ${tag} not found`);
|
||||
|
||||
// Find max semver among published releases (excluding current draft)
|
||||
const publishedReleases = releases.data
|
||||
.filter(r => !r.draft && !r.prerelease)
|
||||
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name))
|
||||
.map(r => ({ id: r.id, tag: r.tag_name, semver: parseSemver(r.tag_name) }))
|
||||
.filter(r => r.semver !== null);
|
||||
|
||||
let maxRelease = null;
|
||||
for (const rel of publishedReleases) {
|
||||
if (!maxRelease || compareSemver(rel.semver, maxRelease.semver) > 0) {
|
||||
maxRelease = rel;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if this release should be latest
|
||||
const isOutdated = maxRelease && compareSemver(currentSemver, maxRelease.semver) < 0;
|
||||
const makeLatest = (isRc || isOutdated) ? 'false' : 'true';
|
||||
|
||||
if (isRc) {
|
||||
console.log(`🏷️ ${tag} is a prerelease, make_latest: false`);
|
||||
} else if (isOutdated) {
|
||||
console.log(`🏷️ ${tag} < ${maxRelease.tag} (max semver), make_latest: false`);
|
||||
} else {
|
||||
console.log(`🏷️ ${tag} is the highest version, make_latest: true`);
|
||||
}
|
||||
|
||||
// Publish the release
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: draft.id,
|
||||
draft: false,
|
||||
prerelease: isRc,
|
||||
make_latest: makeLatest
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: draft.id,
|
||||
draft: false,
|
||||
prerelease: ${{ steps.flags.outputs.is_rc }},
|
||||
make_latest: '${{ steps.flags.outputs.make_latest }}'
|
||||
});
|
||||
console.log(`🚀 Published release ${tag}`);
|
||||
|
||||
// If this is a backport/outdated release, ensure the correct release is marked as latest
|
||||
if (isOutdated && maxRelease) {
|
||||
console.log(`🔧 Ensuring ${maxRelease.tag} remains the latest release...`);
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: maxRelease.id,
|
||||
make_latest: 'true'
|
||||
});
|
||||
console.log(`✅ Restored ${maxRelease.tag} as latest release`);
|
||||
}
|
||||
console.log(`🚀 Published release for ${tag}`);
|
||||
|
||||
78
.github/workflows/retest.yaml
vendored
78
.github/workflows/retest.yaml
vendored
@@ -1,78 +0,0 @@
|
||||
name: Retest
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
retest:
|
||||
name: Retest PR
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/retest')
|
||||
permissions:
|
||||
actions: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Rerun from Prepare environment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.issue.number;
|
||||
|
||||
// Get the PR to find the head SHA
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
// Find the latest workflow run for this PR
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'pull-requests.yaml',
|
||||
head_sha: pr.data.head.sha
|
||||
});
|
||||
|
||||
if (runs.data.workflow_runs.length === 0) {
|
||||
core.setFailed('No workflow runs found for this PR');
|
||||
return;
|
||||
}
|
||||
|
||||
const latestRun = runs.data.workflow_runs[0];
|
||||
console.log(`Found workflow run: ${latestRun.id} (${latestRun.status})`);
|
||||
|
||||
// Check if workflow is waiting for approval (fork PRs)
|
||||
if (latestRun.conclusion === 'action_required') {
|
||||
core.setFailed('Workflow is waiting for approval. A maintainer must approve the workflow first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get jobs for this run
|
||||
const jobs = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: latestRun.id
|
||||
});
|
||||
|
||||
// Find "Prepare environment" job
|
||||
const prepareJob = jobs.data.jobs.find(j => j.name === 'Prepare environment');
|
||||
|
||||
if (!prepareJob) {
|
||||
core.setFailed('Could not find "Prepare environment" job');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found job: ${prepareJob.name} (id: ${prepareJob.id}, status: ${prepareJob.status})`);
|
||||
|
||||
// Rerun the job
|
||||
await github.rest.actions.reRunJobForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
job_id: prepareJob.id
|
||||
});
|
||||
|
||||
console.log(`✅ Triggered rerun of job "${prepareJob.name}" (${prepareJob.id})`);
|
||||
26
.github/workflows/tags.yaml
vendored
26
.github/workflows/tags.yaml
vendored
@@ -123,6 +123,32 @@ 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
92
.github/workflows/update-releasenotes.yaml
vendored
@@ -1,92 +0,0 @@
|
||||
name: Update Release Notes
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: update-releasenotes-${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
update-releasenotes:
|
||||
name: Update Release Notes
|
||||
runs-on: [self-hosted]
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Update release notes from changelogs
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const changelogDir = 'docs/changelogs';
|
||||
|
||||
// Get releases from first page
|
||||
const releases = await github.rest.repos.listReleases({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 30
|
||||
});
|
||||
|
||||
console.log(`Found ${releases.data.length} releases (first page only)`);
|
||||
|
||||
// Process each release
|
||||
for (const release of releases.data) {
|
||||
const tag = release.tag_name;
|
||||
const changelogFile = `${tag}.md`;
|
||||
const changelogPath = path.join(changelogDir, changelogFile);
|
||||
|
||||
console.log(`\nProcessing release: ${tag}`);
|
||||
|
||||
// Check if changelog file exists
|
||||
if (!fs.existsSync(changelogPath)) {
|
||||
console.log(` ⚠️ Changelog file ${changelogFile} does not exist, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read changelog file content
|
||||
let changelogContent;
|
||||
try {
|
||||
changelogContent = fs.readFileSync(changelogPath, 'utf8');
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error reading file ${changelogPath}: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!changelogContent.trim()) {
|
||||
console.log(` ⚠️ Changelog file ${changelogFile} is empty, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if content is already up to date
|
||||
const currentBody = release.body || '';
|
||||
if (currentBody.trim() === changelogContent.trim()) {
|
||||
console.log(` ✓ Content is already up to date, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update release notes
|
||||
try {
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: release.id,
|
||||
body: changelogContent
|
||||
});
|
||||
console.log(` ✅ Successfully updated release notes for ${tag}`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error updating release ${tag}: ${error.message}`);
|
||||
core.setFailed(`Failed to update release notes for ${tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,4 @@ This list is sorted in chronological order, based on the submission date.
|
||||
| [Urmanac](https://urmanac.com) | @kingdonb | 2024-12-04 | Urmanac is the future home of a hosting platform for the knowledge base of a community of personal server enthusiasts. We use Cozystack to provide support services for web sites hosted using both conventional deployments and on SpinKube, with WASM. |
|
||||
| [Hidora](https://hikube.cloud) | @matthieu-robin | 2025-09-17 | Hidora is a Swiss cloud provider delivering managed services and infrastructure solutions through datacenters located in Switzerland, ensuring data sovereignty and reliability. Its sovereign cloud platform, Hikube, is designed to run workloads with high availability across multiple datacenters, providing enterprises with a secure and scalable foundation for their applications based on Cozystack. |
|
||||
| [QOSI](https://qosi.kz) | @tabu-a | 2025-10-04 | QOSI is a non-profit organization driving open-source adoption and digital sovereignty across Kazakhstan and Central Asia. We use Cozystack as a platform for deploying sovereign, GPU-enabled clouds and educational environments under the National AI Program. Our goal is to accelerate the region’s transition toward open, self-hosted cloud-native technologies |
|
||||
| [Cloupard](https://cloupard.kz/) | @serjiott | 2025-12-18 | Cloupard is a public cloud provider offering IaaS and PaaS services via datacenters in Kazakhstan and Uzbekistan. Uses CozyStack on bare metal to extend its managed PaaS offerings. |
|
||||
|
|
||||
38
AGENTS.md
38
AGENTS.md
@@ -3,38 +3,14 @@
|
||||
This file provides structured guidance for AI coding assistants and agents
|
||||
working with the **Cozystack** project.
|
||||
|
||||
## Activation
|
||||
## Agent Documentation
|
||||
|
||||
**CRITICAL**: When the user asks you to do something that matches the scope of a documented process, you MUST read the corresponding documentation file and follow the instructions exactly as written.
|
||||
|
||||
- **Commits, PRs, git operations** (e.g., "create a commit", "make a PR", "fix review comments", "rebase", "cherry-pick")
|
||||
- Read: [`contributing.md`](./docs/agents/contributing.md)
|
||||
- Action: Read the entire file and follow ALL instructions step-by-step
|
||||
|
||||
- **Changelog generation** (e.g., "generate changelog", "create changelog", "prepare changelog for version X")
|
||||
- Read: [`changelog.md`](./docs/agents/changelog.md)
|
||||
- Action: Read the entire file and follow ALL steps in the checklist. Do NOT skip any mandatory steps
|
||||
|
||||
- **Release creation** (e.g., "create release", "prepare release", "tag release", "make a release")
|
||||
- Read: [`releasing.md`](./docs/agents/releasing.md)
|
||||
- Action: Read the file and follow the referenced release process in `docs/release.md`
|
||||
|
||||
- **Project structure, conventions, code layout** (e.g., "where should I put X", "what's the convention for Y", "how is the project organized")
|
||||
- Read: [`overview.md`](./docs/agents/overview.md)
|
||||
- Action: Read relevant sections to understand project structure and conventions
|
||||
|
||||
- **General questions about contributing**
|
||||
- Read: [`contributing.md`](./docs/agents/contributing.md)
|
||||
- Action: Read the file to understand git workflow, commit format, PR process
|
||||
|
||||
**Important rules:**
|
||||
- ✅ **ONLY read the file if the task matches the documented process scope** - do not read files for tasks that don't match their purpose
|
||||
- ✅ **ALWAYS read the file FIRST** before starting the task (when applicable)
|
||||
- ✅ **Follow instructions EXACTLY** as written in the documentation
|
||||
- ✅ **Do NOT skip mandatory steps** (especially in changelog.md)
|
||||
- ✅ **Do NOT assume** you know the process - always check the documentation when the task matches
|
||||
- ❌ **Do NOT read files** for tasks that are outside their documented scope
|
||||
- 📖 **Note**: [`overview.md`](./docs/agents/overview.md) can be useful as a reference to understand project structure and conventions, even when not explicitly required by the task
|
||||
| Agent | Purpose |
|
||||
|-------|---------|
|
||||
| [overview.md](./docs/agents/overview.md) | Project structure and conventions |
|
||||
| [contributing.md](./docs/agents/contributing.md) | Commits, pull requests, and git workflow |
|
||||
| [changelog.md](./docs/agents/changelog.md) | Changelog generation instructions |
|
||||
| [releasing.md](./docs/agents/releasing.md) | Release process and workflow |
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
||||
13
Makefile
13
Makefile
@@ -17,7 +17,7 @@ build: build-deps
|
||||
make -C packages/system/cozystack-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 image
|
||||
make -C packages/system/kubeovn-webhook image
|
||||
make -C packages/system/kubeovn-plunger image
|
||||
make -C packages/system/dashboard image
|
||||
@@ -26,20 +26,15 @@ build: build-deps
|
||||
make -C packages/system/bucket image
|
||||
make -C packages/system/objectstorage-controller image
|
||||
make -C packages/core/testing image
|
||||
make -C packages/core/installer image-operator
|
||||
make -C packages/core/talos image
|
||||
make -C packages/core/platform image
|
||||
make -C packages/core/installer image
|
||||
make -C packages/core/installer image-packages
|
||||
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 -n cozy-system cozystack-operator . | sed '/^WARNING/d') > _out/assets/cozystack-installer.yaml
|
||||
|
||||
assets:
|
||||
make -C packages/core/talos assets
|
||||
|
||||
@@ -18,50 +18,51 @@ package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
// +kubebuilder:resource:scope=Cluster,shortName=appdef
|
||||
|
||||
// CozystackResourceDefinition is the Schema for the cozystackresourcedefinitions API
|
||||
type CozystackResourceDefinition struct {
|
||||
// ApplicationDefinition is the Schema for the applicationdefinitions API
|
||||
type ApplicationDefinition struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec CozystackResourceDefinitionSpec `json:"spec,omitempty"`
|
||||
Spec ApplicationDefinitionSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// CozystackResourceDefinitionList contains a list of CozystackResourceDefinitions
|
||||
type CozystackResourceDefinitionList struct {
|
||||
// ApplicationDefinitionList contains a list of ApplicationDefinitions
|
||||
type ApplicationDefinitionList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []CozystackResourceDefinition `json:"items"`
|
||||
Items []ApplicationDefinition `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&CozystackResourceDefinition{}, &CozystackResourceDefinitionList{})
|
||||
SchemeBuilder.Register(&ApplicationDefinition{}, &ApplicationDefinitionList{})
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionSpec struct {
|
||||
type ApplicationDefinitionSpec struct {
|
||||
// Application configuration
|
||||
Application CozystackResourceDefinitionApplication `json:"application"`
|
||||
Application ApplicationDefinitionApplication `json:"application"`
|
||||
// Release configuration
|
||||
Release CozystackResourceDefinitionRelease `json:"release"`
|
||||
Release ApplicationDefinitionRelease `json:"release"`
|
||||
|
||||
// Secret selectors
|
||||
Secrets CozystackResourceDefinitionResources `json:"secrets,omitempty"`
|
||||
Secrets ApplicationDefinitionResources `json:"secrets,omitempty"`
|
||||
// Service selectors
|
||||
Services CozystackResourceDefinitionResources `json:"services,omitempty"`
|
||||
Services ApplicationDefinitionResources `json:"services,omitempty"`
|
||||
// Ingress selectors
|
||||
Ingresses CozystackResourceDefinitionResources `json:"ingresses,omitempty"`
|
||||
Ingresses ApplicationDefinitionResources `json:"ingresses,omitempty"`
|
||||
|
||||
// Dashboard configuration for this resource
|
||||
Dashboard *CozystackResourceDefinitionDashboard `json:"dashboard,omitempty"`
|
||||
Dashboard *ApplicationDefinitionDashboard `json:"dashboard,omitempty"`
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionChart struct {
|
||||
type ApplicationDefinitionChart struct {
|
||||
// Name of the Helm chart
|
||||
Name string `json:"name"`
|
||||
// Source reference for the Helm chart
|
||||
@@ -79,7 +80,7 @@ type SourceRef struct {
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionApplication struct {
|
||||
type ApplicationDefinitionApplication struct {
|
||||
// Kind of the application, used for UI and API
|
||||
Kind string `json:"kind"`
|
||||
// OpenAPI schema for the application, used for API validation
|
||||
@@ -90,16 +91,30 @@ type CozystackResourceDefinitionApplication struct {
|
||||
Singular string `json:"singular"`
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionRelease struct {
|
||||
// Helm chart configuration
|
||||
Chart CozystackResourceDefinitionChart `json:"chart"`
|
||||
// +kubebuilder:validation:XValidation:rule="(has(self.chart) && !has(self.chartRef)) || (!has(self.chart) && has(self.chartRef))",message="either chart or chartRef must be set, but not both"
|
||||
type ApplicationDefinitionRelease struct {
|
||||
// Helm chart configuration (for HelmRepository source)
|
||||
// +optional
|
||||
Chart *ApplicationDefinitionChart `json:"chart,omitempty"`
|
||||
// Chart reference configuration (for ExternalArtifact source)
|
||||
// +optional
|
||||
ChartRef *ApplicationDefinitionChartRef `json:"chartRef,omitempty"`
|
||||
// Labels for the release
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Prefix for the release name
|
||||
Prefix string `json:"prefix"`
|
||||
// Default values to be merged into every HelmRelease created from this resource definition
|
||||
// User-specified values in Application spec will override these default values
|
||||
// +optional
|
||||
Values *apiextensionsv1.JSON `json:"values,omitempty"`
|
||||
}
|
||||
|
||||
// CozystackResourceDefinitionResourceSelector extends metav1.LabelSelector with resourceNames support.
|
||||
type ApplicationDefinitionChartRef struct {
|
||||
// Source reference for the chart (ExternalArtifact)
|
||||
SourceRef SourceRef `json:"sourceRef"`
|
||||
}
|
||||
|
||||
// ApplicationDefinitionResourceSelector extends metav1.LabelSelector with resourceNames support.
|
||||
// A resource matches this selector only if it satisfies ALL criteria:
|
||||
// - Label selector conditions (matchExpressions and matchLabels)
|
||||
// - AND has a name that matches one of the names in resourceNames (if specified)
|
||||
@@ -121,7 +136,7 @@ type CozystackResourceDefinitionRelease struct {
|
||||
// - "{{ .name }}-secret"
|
||||
// - "{{ .kind }}-{{ .name }}-tls"
|
||||
// - "specificname"
|
||||
type CozystackResourceDefinitionResourceSelector struct {
|
||||
type ApplicationDefinitionResourceSelector struct {
|
||||
metav1.LabelSelector `json:",inline"`
|
||||
// ResourceNames is a list of resource names to match
|
||||
// If specified, the resource must have one of these exact names to match the selector
|
||||
@@ -129,16 +144,16 @@ type CozystackResourceDefinitionResourceSelector struct {
|
||||
ResourceNames []string `json:"resourceNames,omitempty"`
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionResources struct {
|
||||
type ApplicationDefinitionResources struct {
|
||||
// Exclude contains an array of resource selectors that target resources.
|
||||
// If a resource matches the selector in any of the elements in the array, it is
|
||||
// hidden from the user, regardless of the matches in the include array.
|
||||
Exclude []*CozystackResourceDefinitionResourceSelector `json:"exclude,omitempty"`
|
||||
Exclude []*ApplicationDefinitionResourceSelector `json:"exclude,omitempty"`
|
||||
// Include contains an array of resource selectors that target resources.
|
||||
// If a resource matches the selector in any of the elements in the array, and
|
||||
// matches none of the selectors in the exclude array that resource is marked
|
||||
// as a tenant resource and is visible to users.
|
||||
Include []*CozystackResourceDefinitionResourceSelector `json:"include,omitempty"`
|
||||
Include []*ApplicationDefinitionResourceSelector `json:"include,omitempty"`
|
||||
}
|
||||
|
||||
// ---- Dashboard types ----
|
||||
@@ -155,8 +170,8 @@ const (
|
||||
DashboardTabYAML DashboardTab = "yaml"
|
||||
)
|
||||
|
||||
// CozystackResourceDefinitionDashboard describes how this resource appears in the UI.
|
||||
type CozystackResourceDefinitionDashboard struct {
|
||||
// ApplicationDefinitionDashboard describes how this resource appears in the UI.
|
||||
type ApplicationDefinitionDashboard struct {
|
||||
// Human-readable name shown in the UI (e.g., "Bucket")
|
||||
Singular string `json:"singular"`
|
||||
// Plural human-readable name (e.g., "Buckets")
|
||||
230
api/v1alpha1/bundles_types.go
Normal file
230
api/v1alpha1/bundles_types.go
Normal file
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Cluster,shortName=bundle
|
||||
|
||||
// Bundle is the Schema for the bundles API
|
||||
type Bundle struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec BundleSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// BundleList contains a list of Bundles
|
||||
type BundleList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Bundle `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&Bundle{}, &BundleList{})
|
||||
}
|
||||
|
||||
// BundleSpec defines the desired state of Bundle
|
||||
type BundleSpec struct {
|
||||
// SourceRef is the source reference for the bundle charts
|
||||
// +required
|
||||
SourceRef BundleSourceRef `json:"sourceRef"`
|
||||
|
||||
// DependsOn is a list of bundle dependencies in the format "bundleName/target"
|
||||
// For example: "cozystack-system/network"
|
||||
// If specified, the dependencies listed in the target's packages will be taken
|
||||
// from the specified bundle and added to all packages in this bundle
|
||||
// +optional
|
||||
DependsOn []string `json:"dependsOn,omitempty"`
|
||||
|
||||
// DependencyTargets defines named groups of packages that can be referenced
|
||||
// by other bundles via dependsOn. Each target has a name and a list of packages.
|
||||
// +optional
|
||||
DependencyTargets []BundleDependencyTarget `json:"dependencyTargets,omitempty"`
|
||||
|
||||
// Libraries is a list of Helm library charts used by packages
|
||||
// +optional
|
||||
Libraries []BundleLibrary `json:"libraries,omitempty"`
|
||||
|
||||
// Artifacts is a list of Helm charts that will be built as ExternalArtifacts
|
||||
// These artifacts can be referenced by ApplicationDefinitions
|
||||
// +optional
|
||||
Artifacts []BundleArtifact `json:"artifacts,omitempty"`
|
||||
|
||||
// Packages is a list of Helm releases to be installed as part of this bundle
|
||||
// +required
|
||||
Packages []BundleRelease `json:"packages"`
|
||||
|
||||
// DeletionPolicy defines how child resources should be handled when the bundle is deleted.
|
||||
// - "Delete" (default): Child resources will be deleted when the bundle is deleted (via ownerReference).
|
||||
// - "Orphan": Child resources will be orphaned (ownerReferences will be removed).
|
||||
// +kubebuilder:validation:Enum=Delete;Orphan
|
||||
// +kubebuilder:default=Delete
|
||||
// +optional
|
||||
DeletionPolicy DeletionPolicy `json:"deletionPolicy,omitempty"`
|
||||
|
||||
// Labels are labels that will be applied to all resources created by this bundle
|
||||
// (ArtifactGenerators and HelmReleases). These labels are merged with the default
|
||||
// cozystack.io/bundle label.
|
||||
// +optional
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
|
||||
// BasePath is the base path where packages are located in the source.
|
||||
// For GitRepository, defaults to "packages" if not specified.
|
||||
// For OCIRepository, defaults to empty string (root) if not specified.
|
||||
// +optional
|
||||
BasePath string `json:"basePath,omitempty"`
|
||||
}
|
||||
|
||||
// DeletionPolicy defines how child resources should be handled when the parent is deleted.
|
||||
// +kubebuilder:validation:Enum=Delete;Orphan
|
||||
type DeletionPolicy string
|
||||
|
||||
const (
|
||||
// DeletionPolicyDelete means child resources will be deleted when the parent is deleted.
|
||||
DeletionPolicyDelete DeletionPolicy = "Delete"
|
||||
// DeletionPolicyOrphan means child resources will be orphaned (ownerReferences removed).
|
||||
DeletionPolicyOrphan DeletionPolicy = "Orphan"
|
||||
)
|
||||
|
||||
// BundleDependencyTarget defines a named group of packages that can be referenced
|
||||
// by other bundles via dependsOn
|
||||
type BundleDependencyTarget 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"`
|
||||
}
|
||||
|
||||
// BundleLibrary defines a Helm library chart
|
||||
type BundleLibrary struct {
|
||||
// Name is the unique identifier for this library
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// Path is the path to the library chart directory
|
||||
// +required
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// BundleArtifact defines a Helm chart artifact that will be built as ExternalArtifact
|
||||
type BundleArtifact struct {
|
||||
// Name is the unique identifier for this artifact (used as ExternalArtifact name)
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// Path is the path to the Helm chart directory
|
||||
// +required
|
||||
Path string `json:"path"`
|
||||
|
||||
// Libraries is a list of library names that this artifact depends on
|
||||
// +optional
|
||||
Libraries []string `json:"libraries,omitempty"`
|
||||
}
|
||||
|
||||
// BundleSourceRef defines the source reference for bundle charts
|
||||
type BundleSourceRef struct {
|
||||
// Kind of the source reference
|
||||
// +kubebuilder:validation:Enum=GitRepository;OCIRepository
|
||||
// +required
|
||||
Kind string `json:"kind"`
|
||||
|
||||
// Name of the source reference
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// Namespace of the source reference
|
||||
// +required
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
// +kubebuilder:validation:XValidation:rule="(has(self.path) && !has(self.artifact)) || (!has(self.path) && has(self.artifact))",message="either path or artifact must be set, but not both"
|
||||
// BundleRelease defines a single Helm release within a bundle
|
||||
type BundleRelease struct {
|
||||
// Name is the unique identifier for this release within the bundle
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// ReleaseName is the name of the HelmRelease resource that will be created
|
||||
// +required
|
||||
ReleaseName string `json:"releaseName"`
|
||||
|
||||
// Path is the path to the Helm chart directory
|
||||
// Either Path or Artifact must be specified, but not both
|
||||
// +optional
|
||||
Path string `json:"path,omitempty"`
|
||||
|
||||
// Artifact is the name of an artifact from the bundle's artifacts list
|
||||
// The artifact must exist in the bundle's artifacts section
|
||||
// Either Path or Artifact must be specified, but not both
|
||||
// +optional
|
||||
Artifact string `json:"artifact,omitempty"`
|
||||
|
||||
// Namespace is the Kubernetes namespace where the release will be installed
|
||||
// +required
|
||||
Namespace string `json:"namespace"`
|
||||
|
||||
// Privileged indicates whether this release requires privileged access
|
||||
// +optional
|
||||
Privileged bool `json:"privileged,omitempty"`
|
||||
|
||||
// Disabled indicates whether this release is disabled (should not be installed)
|
||||
// +optional
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
|
||||
// DependsOn is a list of release names that must be installed before this release
|
||||
// +optional
|
||||
DependsOn []string `json:"dependsOn,omitempty"`
|
||||
|
||||
// Libraries is a list of library names that this package depends on
|
||||
// +optional
|
||||
Libraries []string `json:"libraries,omitempty"`
|
||||
|
||||
// Values contains Helm chart values as a JSON object
|
||||
// +optional
|
||||
Values *apiextensionsv1.JSON `json:"values,omitempty"`
|
||||
|
||||
// ValuesFiles is a list of values file names to use
|
||||
// +optional
|
||||
ValuesFiles []string `json:"valuesFiles,omitempty"`
|
||||
|
||||
// Labels are labels that will be applied to the HelmRelease created for this package
|
||||
// These labels are merged with bundle-level labels and the default cozystack.io/bundle label
|
||||
// +optional
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
|
||||
// NamespaceLabels are labels that will be applied to the namespace for this package
|
||||
// These labels are merged with labels from other packages in the same namespace
|
||||
// +optional
|
||||
NamespaceLabels map[string]string `json:"namespaceLabels,omitempty"`
|
||||
|
||||
// NamespaceAnnotations are annotations that will be applied to the namespace for this package
|
||||
// These annotations are merged with annotations from other packages in the same namespace
|
||||
// +optional
|
||||
NamespaceAnnotations map[string]string `json:"namespaceAnnotations,omitempty"`
|
||||
}
|
||||
71
api/v1alpha1/platform_types.go
Normal file
71
api/v1alpha1/platform_types.go
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Cluster,shortName=platform
|
||||
|
||||
// Platform is the Schema for the platforms API
|
||||
type Platform struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec PlatformSpec `json:"spec,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// PlatformList contains a list of Platform
|
||||
type PlatformList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Platform `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&Platform{}, &PlatformList{})
|
||||
}
|
||||
|
||||
// PlatformSpec defines the desired state of Platform
|
||||
type PlatformSpec struct {
|
||||
// SourceRef is the source reference for the platform chart
|
||||
// This is used to generate the ArtifactGenerator
|
||||
// +required
|
||||
SourceRef SourceRef `json:"sourceRef"`
|
||||
|
||||
// Values contains Helm chart values as a JSON object
|
||||
// These values are passed directly to HelmRelease.values
|
||||
// +optional
|
||||
Values *apiextensionsv1.JSON `json:"values,omitempty"`
|
||||
|
||||
// Interval is the interval at which to reconcile the HelmRelease
|
||||
// +kubebuilder:default="5m"
|
||||
// +optional
|
||||
Interval *metav1.Duration `json:"interval,omitempty"`
|
||||
|
||||
// BasePath is the base path where the platform chart is located in the source.
|
||||
// For GitRepository, defaults to "packages/core/platform" if not specified.
|
||||
// For OCIRepository, defaults to "core/platform" if not specified.
|
||||
// +optional
|
||||
BasePath string `json:"basePath,omitempty"`
|
||||
}
|
||||
|
||||
@@ -21,30 +21,32 @@ limitations under the License.
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinition) DeepCopyInto(out *CozystackResourceDefinition) {
|
||||
func (in *ApplicationDefinition) DeepCopyInto(out *ApplicationDefinition) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinition.
|
||||
func (in *CozystackResourceDefinition) DeepCopy() *CozystackResourceDefinition {
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinition.
|
||||
func (in *ApplicationDefinition) DeepCopy() *ApplicationDefinition {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinition)
|
||||
out := new(ApplicationDefinition)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *CozystackResourceDefinition) DeepCopyObject() runtime.Object {
|
||||
func (in *ApplicationDefinition) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
@@ -52,38 +54,54 @@ func (in *CozystackResourceDefinition) DeepCopyObject() runtime.Object {
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionApplication) DeepCopyInto(out *CozystackResourceDefinitionApplication) {
|
||||
func (in *ApplicationDefinitionApplication) DeepCopyInto(out *ApplicationDefinitionApplication) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionApplication.
|
||||
func (in *CozystackResourceDefinitionApplication) DeepCopy() *CozystackResourceDefinitionApplication {
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionApplication.
|
||||
func (in *ApplicationDefinitionApplication) DeepCopy() *ApplicationDefinitionApplication {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionApplication)
|
||||
out := new(ApplicationDefinitionApplication)
|
||||
in.DeepCopyInto(out)
|
||||
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) {
|
||||
func (in *ApplicationDefinitionChart) DeepCopyInto(out *ApplicationDefinitionChart) {
|
||||
*out = *in
|
||||
out.SourceRef = in.SourceRef
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionChart.
|
||||
func (in *CozystackResourceDefinitionChart) DeepCopy() *CozystackResourceDefinitionChart {
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionChart.
|
||||
func (in *ApplicationDefinitionChart) DeepCopy() *ApplicationDefinitionChart {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionChart)
|
||||
out := new(ApplicationDefinitionChart)
|
||||
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) {
|
||||
func (in *ApplicationDefinitionChartRef) DeepCopyInto(out *ApplicationDefinitionChartRef) {
|
||||
*out = *in
|
||||
out.SourceRef = in.SourceRef
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionChartRef.
|
||||
func (in *ApplicationDefinitionChartRef) DeepCopy() *ApplicationDefinitionChartRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApplicationDefinitionChartRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApplicationDefinitionDashboard) DeepCopyInto(out *ApplicationDefinitionDashboard) {
|
||||
*out = *in
|
||||
if in.Tags != nil {
|
||||
in, out := &in.Tags, &out.Tags
|
||||
@@ -108,42 +126,42 @@ func (in *CozystackResourceDefinitionDashboard) DeepCopyInto(out *CozystackResou
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionDashboard.
|
||||
func (in *CozystackResourceDefinitionDashboard) DeepCopy() *CozystackResourceDefinitionDashboard {
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionDashboard.
|
||||
func (in *ApplicationDefinitionDashboard) DeepCopy() *ApplicationDefinitionDashboard {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionDashboard)
|
||||
out := new(ApplicationDefinitionDashboard)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionList) DeepCopyInto(out *CozystackResourceDefinitionList) {
|
||||
func (in *ApplicationDefinitionList) DeepCopyInto(out *ApplicationDefinitionList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]CozystackResourceDefinition, len(*in))
|
||||
*out = make([]ApplicationDefinition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionList.
|
||||
func (in *CozystackResourceDefinitionList) DeepCopy() *CozystackResourceDefinitionList {
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionList.
|
||||
func (in *ApplicationDefinitionList) DeepCopy() *ApplicationDefinitionList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionList)
|
||||
out := new(ApplicationDefinitionList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *CozystackResourceDefinitionList) DeepCopyObject() runtime.Object {
|
||||
func (in *ApplicationDefinitionList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
@@ -151,9 +169,18 @@ 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) {
|
||||
func (in *ApplicationDefinitionRelease) DeepCopyInto(out *ApplicationDefinitionRelease) {
|
||||
*out = *in
|
||||
out.Chart = in.Chart
|
||||
if in.Chart != nil {
|
||||
in, out := &in.Chart, &out.Chart
|
||||
*out = new(ApplicationDefinitionChart)
|
||||
**out = **in
|
||||
}
|
||||
if in.ChartRef != nil {
|
||||
in, out := &in.ChartRef, &out.ChartRef
|
||||
*out = new(ApplicationDefinitionChartRef)
|
||||
**out = **in
|
||||
}
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
@@ -161,20 +188,25 @@ func (in *CozystackResourceDefinitionRelease) DeepCopyInto(out *CozystackResourc
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Values != nil {
|
||||
in, out := &in.Values, &out.Values
|
||||
*out = new(v1.JSON)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionRelease.
|
||||
func (in *CozystackResourceDefinitionRelease) DeepCopy() *CozystackResourceDefinitionRelease {
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionRelease.
|
||||
func (in *ApplicationDefinitionRelease) DeepCopy() *ApplicationDefinitionRelease {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionRelease)
|
||||
out := new(ApplicationDefinitionRelease)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionResourceSelector) DeepCopyInto(out *CozystackResourceDefinitionResourceSelector) {
|
||||
func (in *ApplicationDefinitionResourceSelector) DeepCopyInto(out *ApplicationDefinitionResourceSelector) {
|
||||
*out = *in
|
||||
in.LabelSelector.DeepCopyInto(&out.LabelSelector)
|
||||
if in.ResourceNames != nil {
|
||||
@@ -184,55 +216,55 @@ func (in *CozystackResourceDefinitionResourceSelector) DeepCopyInto(out *Cozysta
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionResourceSelector.
|
||||
func (in *CozystackResourceDefinitionResourceSelector) DeepCopy() *CozystackResourceDefinitionResourceSelector {
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionResourceSelector.
|
||||
func (in *ApplicationDefinitionResourceSelector) DeepCopy() *ApplicationDefinitionResourceSelector {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionResourceSelector)
|
||||
out := new(ApplicationDefinitionResourceSelector)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionResources) DeepCopyInto(out *CozystackResourceDefinitionResources) {
|
||||
func (in *ApplicationDefinitionResources) DeepCopyInto(out *ApplicationDefinitionResources) {
|
||||
*out = *in
|
||||
if in.Exclude != nil {
|
||||
in, out := &in.Exclude, &out.Exclude
|
||||
*out = make([]*CozystackResourceDefinitionResourceSelector, len(*in))
|
||||
*out = make([]*ApplicationDefinitionResourceSelector, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(CozystackResourceDefinitionResourceSelector)
|
||||
*out = new(ApplicationDefinitionResourceSelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
}
|
||||
if in.Include != nil {
|
||||
in, out := &in.Include, &out.Include
|
||||
*out = make([]*CozystackResourceDefinitionResourceSelector, len(*in))
|
||||
*out = make([]*ApplicationDefinitionResourceSelector, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] != nil {
|
||||
in, out := &(*in)[i], &(*out)[i]
|
||||
*out = new(CozystackResourceDefinitionResourceSelector)
|
||||
*out = new(ApplicationDefinitionResourceSelector)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionResources.
|
||||
func (in *CozystackResourceDefinitionResources) DeepCopy() *CozystackResourceDefinitionResources {
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionResources.
|
||||
func (in *ApplicationDefinitionResources) DeepCopy() *ApplicationDefinitionResources {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionResources)
|
||||
out := new(ApplicationDefinitionResources)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionSpec) DeepCopyInto(out *CozystackResourceDefinitionSpec) {
|
||||
func (in *ApplicationDefinitionSpec) DeepCopyInto(out *ApplicationDefinitionSpec) {
|
||||
*out = *in
|
||||
out.Application = in.Application
|
||||
in.Release.DeepCopyInto(&out.Release)
|
||||
@@ -241,17 +273,339 @@ func (in *CozystackResourceDefinitionSpec) DeepCopyInto(out *CozystackResourceDe
|
||||
in.Ingresses.DeepCopyInto(&out.Ingresses)
|
||||
if in.Dashboard != nil {
|
||||
in, out := &in.Dashboard, &out.Dashboard
|
||||
*out = new(CozystackResourceDefinitionDashboard)
|
||||
*out = new(ApplicationDefinitionDashboard)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionSpec.
|
||||
func (in *CozystackResourceDefinitionSpec) DeepCopy() *CozystackResourceDefinitionSpec {
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationDefinitionSpec.
|
||||
func (in *ApplicationDefinitionSpec) DeepCopy() *ApplicationDefinitionSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionSpec)
|
||||
out := new(ApplicationDefinitionSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Bundle) DeepCopyInto(out *Bundle) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bundle.
|
||||
func (in *Bundle) DeepCopy() *Bundle {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Bundle)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Bundle) 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 *BundleArtifact) DeepCopyInto(out *BundleArtifact) {
|
||||
*out = *in
|
||||
if in.Libraries != nil {
|
||||
in, out := &in.Libraries, &out.Libraries
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleArtifact.
|
||||
func (in *BundleArtifact) DeepCopy() *BundleArtifact {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BundleArtifact)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BundleDependencyTarget) DeepCopyInto(out *BundleDependencyTarget) {
|
||||
*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 BundleDependencyTarget.
|
||||
func (in *BundleDependencyTarget) DeepCopy() *BundleDependencyTarget {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BundleDependencyTarget)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BundleLibrary) DeepCopyInto(out *BundleLibrary) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleLibrary.
|
||||
func (in *BundleLibrary) DeepCopy() *BundleLibrary {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BundleLibrary)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BundleList) DeepCopyInto(out *BundleList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Bundle, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleList.
|
||||
func (in *BundleList) DeepCopy() *BundleList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BundleList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BundleList) 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 *BundleRelease) DeepCopyInto(out *BundleRelease) {
|
||||
*out = *in
|
||||
if in.DependsOn != nil {
|
||||
in, out := &in.DependsOn, &out.DependsOn
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Libraries != nil {
|
||||
in, out := &in.Libraries, &out.Libraries
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Values != nil {
|
||||
in, out := &in.Values, &out.Values
|
||||
*out = new(v1.JSON)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ValuesFiles != nil {
|
||||
in, out := &in.ValuesFiles, &out.ValuesFiles
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.NamespaceLabels != nil {
|
||||
in, out := &in.NamespaceLabels, &out.NamespaceLabels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.NamespaceAnnotations != nil {
|
||||
in, out := &in.NamespaceAnnotations, &out.NamespaceAnnotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleRelease.
|
||||
func (in *BundleRelease) DeepCopy() *BundleRelease {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BundleRelease)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BundleSourceRef) DeepCopyInto(out *BundleSourceRef) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleSourceRef.
|
||||
func (in *BundleSourceRef) DeepCopy() *BundleSourceRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BundleSourceRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BundleSpec) DeepCopyInto(out *BundleSpec) {
|
||||
*out = *in
|
||||
out.SourceRef = in.SourceRef
|
||||
if in.DependsOn != nil {
|
||||
in, out := &in.DependsOn, &out.DependsOn
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.DependencyTargets != nil {
|
||||
in, out := &in.DependencyTargets, &out.DependencyTargets
|
||||
*out = make([]BundleDependencyTarget, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Libraries != nil {
|
||||
in, out := &in.Libraries, &out.Libraries
|
||||
*out = make([]BundleLibrary, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Artifacts != nil {
|
||||
in, out := &in.Artifacts, &out.Artifacts
|
||||
*out = make([]BundleArtifact, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Packages != nil {
|
||||
in, out := &in.Packages, &out.Packages
|
||||
*out = make([]BundleRelease, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleSpec.
|
||||
func (in *BundleSpec) DeepCopy() *BundleSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BundleSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Platform) DeepCopyInto(out *Platform) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Platform.
|
||||
func (in *Platform) DeepCopy() *Platform {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Platform)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Platform) 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 *PlatformList) DeepCopyInto(out *PlatformList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Platform, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformList.
|
||||
func (in *PlatformList) DeepCopy() *PlatformList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PlatformList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *PlatformList) 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 *PlatformSpec) DeepCopyInto(out *PlatformSpec) {
|
||||
*out = *in
|
||||
out.SourceRef = in.SourceRef
|
||||
if in.Values != nil {
|
||||
in, out := &in.Values, &out.Values
|
||||
*out = new(v1.JSON)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Interval != nil {
|
||||
in, out := &in.Interval, &out.Interval
|
||||
*out = new(metav1.Duration)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlatformSpec.
|
||||
func (in *PlatformSpec) DeepCopy() *PlatformSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PlatformSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
@@ -39,7 +38,6 @@ import (
|
||||
cozystackiov1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/controller"
|
||||
"github.com/cozystack/cozystack/internal/controller/dashboard"
|
||||
"github.com/cozystack/cozystack/internal/telemetry"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
// +kubebuilder:scaffold:imports
|
||||
@@ -65,10 +63,6 @@ func main() {
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var disableTelemetry bool
|
||||
var telemetryEndpoint string
|
||||
var telemetryInterval string
|
||||
var cozystackVersion string
|
||||
var reconcileDeployment bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
@@ -81,14 +75,6 @@ func main() {
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
flag.BoolVar(&disableTelemetry, "disable-telemetry", false,
|
||||
"Disable telemetry collection")
|
||||
flag.StringVar(&telemetryEndpoint, "telemetry-endpoint", "https://telemetry.cozystack.io",
|
||||
"Endpoint for sending telemetry data")
|
||||
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
|
||||
"Interval between telemetry data collection (e.g. 15m, 1h)")
|
||||
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
|
||||
"Version of Cozystack")
|
||||
flag.BoolVar(&reconcileDeployment, "reconcile-deployment", false,
|
||||
"If set, the Cozystack API server is assumed to run as a Deployment, else as a DaemonSet.")
|
||||
opts := zap.Options{
|
||||
@@ -97,21 +83,6 @@ func main() {
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
// Parse telemetry interval
|
||||
interval, err := time.ParseDuration(telemetryInterval)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "invalid telemetry interval")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Configure telemetry
|
||||
telemetryConfig := telemetry.Config{
|
||||
Disabled: disableTelemetry,
|
||||
Endpoint: telemetryEndpoint,
|
||||
Interval: interval,
|
||||
CozystackVersion: cozystackVersion,
|
||||
}
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
@@ -200,24 +171,20 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cozyAPIKind := "DaemonSet"
|
||||
if reconcileDeployment {
|
||||
cozyAPIKind = "Deployment"
|
||||
}
|
||||
if err = (&controller.CozystackResourceDefinitionReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
CozystackAPIKind: cozyAPIKind,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controller.CozystackResourceDefinitionHelmReconciler{
|
||||
if err = (&controller.NamespaceHelmReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionHelmReconciler")
|
||||
setupLog.Error(err, "unable to create controller", "controller", "NamespaceHelmReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controller.ApplicationDefinitionReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
CozystackAPIKind: "Deployment",
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "ApplicationDefinitionReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -241,19 +208,6 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Initialize telemetry collector
|
||||
collector, err := telemetry.NewCollector(mgr.GetClient(), &telemetryConfig, mgr.GetConfig())
|
||||
if err != nil {
|
||||
setupLog.V(1).Error(err, "unable to create telemetry collector, telemetry will be disabled")
|
||||
}
|
||||
|
||||
if collector != nil {
|
||||
if err := mgr.Add(collector); err != nil {
|
||||
setupLog.Error(err, "unable to set up telemetry collector")
|
||||
setupLog.V(1).Error(err, "unable to set up telemetry collector, continuing without telemetry")
|
||||
}
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
ctx := ctrl.SetupSignalHandler()
|
||||
dashboardManager.InitializeStaticResources(ctx)
|
||||
|
||||
312
cmd/cozystack-operator/main.go
Normal file
312
cmd/cozystack-operator/main.go
Normal file
@@ -0,0 +1,312 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"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/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
"github.com/cozystack/cozystack/internal/fluxinstall"
|
||||
"github.com/cozystack/cozystack/internal/operator"
|
||||
"github.com/cozystack/cozystack/internal/telemetry"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
// 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))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(helmv2.AddToScheme(scheme))
|
||||
utilruntime.Must(sourcev1.AddToScheme(scheme))
|
||||
utilruntime.Must(sourcewatcherv1beta1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var installFlux bool
|
||||
var disableTelemetry bool
|
||||
var telemetryEndpoint string
|
||||
var telemetryInterval string
|
||||
var cozystackVersion string
|
||||
var installFluxResources stringSliceFlag
|
||||
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", false,
|
||||
"If set the metrics endpoint is served securely")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
flag.BoolVar(&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.BoolVar(&disableTelemetry, "disable-telemetry", false,
|
||||
"Disable telemetry collection")
|
||||
flag.StringVar(&telemetryEndpoint, "telemetry-endpoint", "https://telemetry.cozystack.io",
|
||||
"Endpoint for sending telemetry data")
|
||||
flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
|
||||
"Interval between telemetry data collection (e.g. 15m, 1h)")
|
||||
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
|
||||
"Version of Cozystack")
|
||||
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
// Parse telemetry interval
|
||||
interval, err := time.ParseDuration(telemetryInterval)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "invalid telemetry interval")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Configure telemetry
|
||||
telemetryConfig := telemetry.Config{
|
||||
Disabled: disableTelemetry,
|
||||
Endpoint: telemetryEndpoint,
|
||||
Interval: interval,
|
||||
CozystackVersion: cozystackVersion,
|
||||
}
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
config := ctrl.GetConfigOrDie()
|
||||
|
||||
// Start the controller manager
|
||||
setupLog.Info("Starting controller manager")
|
||||
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
},
|
||||
WebhookServer: webhook.NewServer(webhook.Options{
|
||||
Port: 9443,
|
||||
}),
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "platform-operator.cozystack.io",
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, setting this significantly speeds up voluntary
|
||||
// leader transitions as the new leader don't have to wait LeaseDuration time first.
|
||||
//
|
||||
// In the default scaffold provided, the program ends immediately after
|
||||
// the manager stops, so would be fine to enable this option. However,
|
||||
// if you are doing or is intended to do any operation such as perform cleanups
|
||||
// after the manager stops then its usage might be unsafe.
|
||||
// LeaderElectionReleaseOnCancel: true,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Install Flux before starting reconcile loop
|
||||
if installFlux {
|
||||
setupLog.Info("Installing Flux components before starting reconcile loop")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
bundleReconciler := &operator.BundleReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}
|
||||
if err = bundleReconciler.SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Bundle")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
platformReconciler := &operator.PlatformReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}
|
||||
if err = platformReconciler.SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Platform")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Initialize telemetry collector
|
||||
collector, err := telemetry.NewCollector(mgr.GetClient(), &telemetryConfig, mgr.GetConfig())
|
||||
if err != nil {
|
||||
setupLog.V(1).Error(err, "unable to create telemetry collector, telemetry will be disabled")
|
||||
}
|
||||
|
||||
if collector != nil {
|
||||
if err := mgr.Add(collector); err != nil {
|
||||
setupLog.Error(err, "unable to set up telemetry collector")
|
||||
setupLog.V(1).Error(err, "unable to set up telemetry collector, continuing without telemetry")
|
||||
}
|
||||
}
|
||||
|
||||
setupLog.Info("Starting controller manager")
|
||||
mgrCtx := ctrl.SetupSignalHandler()
|
||||
if err := mgr.Start(mgrCtx); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -22,7 +22,7 @@ When the user asks to generate a changelog, follow these steps in the specified
|
||||
- [ ] Step 5: Get the list of commits for the release period
|
||||
- [ ] Step 6: Check additional repositories (website is REQUIRED, optional repos if tags exist)
|
||||
- [ ] **MANDATORY**: Check website repository for documentation changes WITH authors and PR links via GitHub CLI
|
||||
- [ ] **MANDATORY**: Check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) for tags during release period
|
||||
- [ ] **MANDATORY**: Check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy) for tags during release period
|
||||
- [ ] **MANDATORY**: For ALL commits from additional repos, get GitHub username via CLI, prioritizing PR author over commit author.
|
||||
- [ ] Step 7: Analyze commits (extract PR numbers, authors, user impact)
|
||||
- [ ] **MANDATORY**: For EVERY PR in main repo, get PR author via `gh pr view <PR_NUMBER> --json author --jq .author.login` (do NOT skip this step)
|
||||
@@ -146,7 +146,7 @@ Cozystack release may include changes from related repositories. Check and inclu
|
||||
**Optional repositories (MUST check ALL of them for tags during release period):**
|
||||
- [https://github.com/cozystack/talm](https://github.com/cozystack/talm)
|
||||
- [https://github.com/cozystack/boot-to-talos](https://github.com/cozystack/boot-to-talos)
|
||||
- [https://github.com/cozystack/cozyhr](https://github.com/cozystack/cozyhr)
|
||||
- [https://github.com/cozystack/cozypkg](https://github.com/cozystack/cozypkg)
|
||||
- [https://github.com/cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy)
|
||||
|
||||
**⚠️ IMPORTANT**: You MUST check ALL optional repositories for tags created during the release period. Do NOT skip this step even if you think there might not be any tags. Use the process below to verify.
|
||||
@@ -195,7 +195,7 @@ Cozystack release may include changes from related repositories. Check and inclu
|
||||
|
||||
3. **For optional repositories, check if tags exist during release period:**
|
||||
|
||||
**⚠️ MANDATORY: You MUST check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy). Do NOT skip any repository!**
|
||||
**⚠️ MANDATORY: You MUST check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy). Do NOT skip any repository!**
|
||||
|
||||
**Use the helper script:**
|
||||
```bash
|
||||
@@ -208,7 +208,7 @@ Cozystack release may include changes from related repositories. Check and inclu
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy)
|
||||
- Check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy)
|
||||
- Look for tags created during the release period
|
||||
- Get commits between tags (if tags exist) or by date range (if no tags)
|
||||
- Extract PR numbers from commit messages
|
||||
@@ -569,7 +569,7 @@ Create a new changelog file in the format matching previous versions:
|
||||
- [ ] Step 5 completed: **ALL commits included** (including merge commits and backports) - do not skip any commits
|
||||
- [ ] Step 5 completed: **Backports identified and handled correctly** - original PR author used, both original and backport PR numbers included
|
||||
- [ ] Step 6 completed: Website repository checked for documentation changes WITH authors and PR links via GitHub CLI
|
||||
- [ ] Step 6 completed: **ALL** optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) checked for tags during release period
|
||||
- [ ] Step 6 completed: **ALL** optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy) checked for tags during release period
|
||||
- [ ] Step 6 completed: For ALL commits from additional repos, GitHub username obtained via GitHub CLI (not skipped). For commits with PR numbers, PR author used via `gh pr view` (not commit author)
|
||||
- [ ] Step 7 completed: For EVERY PR in main repo (including backports), PR author obtained via `gh pr view <PR_NUMBER> --json author --jq .author.login` (not skipped or assumed). Commit author NOT used - always use PR author
|
||||
- [ ] Step 7 completed: **Backports verified** - for each backport PR, original PR found and original PR author used in changelog
|
||||
@@ -628,7 +628,7 @@ Save the changelog to file `docs/changelogs/v<version>.md` according to the vers
|
||||
|
||||
- **Additional repositories (Step 6) - MANDATORY**:
|
||||
- **⚠️ CRITICAL**: Always check the **website** repository for documentation changes during the release period. This is a required step and MUST NOT be skipped.
|
||||
- **⚠️ CRITICAL**: You MUST check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) for tags during the release period. Do NOT skip any repository even if you think there might not be tags.
|
||||
- **⚠️ CRITICAL**: You MUST check ALL optional repositories (talm, boot-to-talos, cozypkg, cozy-proxy) for tags during the release period. Do NOT skip any repository even if you think there might not be tags.
|
||||
- **CRITICAL**: For ALL entries from additional repositories (website and optional), you MUST:
|
||||
- **MANDATORY**: Extract PR number from commit message first
|
||||
- **MANDATORY**: For commits with PR numbers, ALWAYS use `gh pr view <PR_NUMBER> --repo cozystack/<repo> --json author --jq .author.login` to get PR author (not commit author)
|
||||
@@ -637,7 +637,7 @@ Save the changelog to file `docs/changelogs/v<version>.md` according to the vers
|
||||
- **MANDATORY**: Do NOT use commit author for PRs - always use PR author
|
||||
- Include PR link or commit hash reference
|
||||
- Format: `* **[repo] Description**: details ([**@username**](https://github.com/username) in cozystack/repo#123)`
|
||||
- For **optional repositories** (talm, boot-to-talos, cozyhr, cozy-proxy), you MUST check ALL of them for tags during the release period. Use the loop provided in Step 6 to check each repository systematically.
|
||||
- For **optional repositories** (talm, boot-to-talos, cozypkg, cozy-proxy), you MUST check ALL of them for tags during the release period. Use the loop provided in Step 6 to check each repository systematically.
|
||||
- When including changes from additional repositories, use the format: `[repo-name] Description` and link to the repository's PR/issue if available
|
||||
- **Prefer PR numbers over commit hashes**: For commits from additional repositories, extract PR number from commit message using GitHub API. Use PR format (`cozystack/website#123`) instead of commit hash (`cozystack/website@abc1234`) when available
|
||||
- **Never add entries without author and PR/commit reference**: Every entry from additional repositories must have both author and link
|
||||
|
||||
@@ -155,91 +155,6 @@ git diff
|
||||
|
||||
The user will commit and push when ready.
|
||||
|
||||
## Code Review Comments
|
||||
|
||||
When asked to fix code review comments, **always work only with unresolved (open) comments**. Resolved comments should be ignored as they have already been addressed.
|
||||
|
||||
### Getting Unresolved Review Comments
|
||||
|
||||
Use GitHub GraphQL API to fetch only unresolved review comments from a pull request:
|
||||
|
||||
```bash
|
||||
gh api graphql -F owner=cozystack -F repo=cozystack -F pr=<PR_NUMBER> -f query='
|
||||
query($owner: String!, $repo: String!, $pr: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $pr) {
|
||||
reviewThreads(first: 100) {
|
||||
nodes {
|
||||
isResolved
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
path
|
||||
line
|
||||
author { login }
|
||||
bodyText
|
||||
url
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[]'
|
||||
```
|
||||
|
||||
### Filtering for Unresolved Comments
|
||||
|
||||
The key filter is `select(.isResolved == false)` which ensures only unresolved review threads are processed. Each thread can contain multiple comments, but if the thread is resolved, all its comments should be ignored.
|
||||
|
||||
### Working with Review Comments
|
||||
|
||||
1. **Fetch unresolved comments** using the GraphQL query above
|
||||
2. **Parse the results** to identify:
|
||||
- File path (`path`)
|
||||
- Line number (`line` or `originalLine`)
|
||||
- Comment text (`bodyText`)
|
||||
- Author (`author.login`)
|
||||
3. **Address each unresolved comment** by:
|
||||
- Locating the relevant code section
|
||||
- Making the requested changes
|
||||
- Ensuring the fix addresses the concern raised
|
||||
4. **Do NOT process resolved comments** - they have already been handled
|
||||
|
||||
### Example: Compact List of Unresolved Comments
|
||||
|
||||
For a quick overview of unresolved comments:
|
||||
|
||||
```bash
|
||||
gh api graphql -F owner=cozystack -F repo=cozystack -F pr=<PR_NUMBER> -f query='
|
||||
query($owner: String!, $repo: String!, $pr: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $pr) {
|
||||
reviewThreads(first: 100) {
|
||||
nodes {
|
||||
isResolved
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
path
|
||||
line
|
||||
author { login }
|
||||
bodyText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[] | "\(.path):\(.line // "N/A") - \(.author.login): \(.bodyText[:150])"'
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **REST API limitation**: The REST endpoint `/pulls/{pr}/reviews` returns review summaries, not individual review comments. Use GraphQL API for accessing `reviewThreads` with `isResolved` status.
|
||||
- **Thread-based resolution**: Comments are organized in threads. If a thread is resolved (`isResolved: true`), ignore all comments in that thread.
|
||||
- **Always filter**: Never process comments from resolved threads, even if they appear in the results.
|
||||
|
||||
### Example Workflow
|
||||
|
||||
```bash
|
||||
|
||||
@@ -10,7 +10,7 @@ Cozystack is an open-source Kubernetes-based platform and framework for building
|
||||
- **Multi-tenancy**: Full isolation and self-service for tenants
|
||||
- **GitOps-driven**: FluxCD-based continuous delivery
|
||||
- **Modular Architecture**: Extensible with custom packages and services
|
||||
- **Developer Experience**: Simplified local development with cozyhr tool
|
||||
- **Developer Experience**: Simplified local development with cozypkg tool
|
||||
|
||||
The platform exposes infrastructure services via the Kubernetes API with ready-made configs, built-in monitoring, and alerts.
|
||||
|
||||
|
||||
@@ -5,13 +5,10 @@ https://github.com/cozystack/cozystack/releases/tag/v0.36.2
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
* [vm-disk] New SVG icon for VM disk application. (@kvaps and @kvapsova in https://github.com/cozystack/cozystack/pull/1435)
|
||||
## Security
|
||||
|
||||
## Fixes
|
||||
|
||||
* [kubernetes] Pin CoreDNS image tag to v1.12.4 for consistent, reproducible deployments. (@kvaps in https://github.com/cozystack/cozystack/pull/1469)
|
||||
* [dashboard] Fix FerretDB spec typo that prevented deploy/display in the web UI. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1440)
|
||||
|
||||
## Dependencies
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.38.3
|
||||
-->
|
||||
|
||||
## Improvements
|
||||
|
||||
* **[core:installer] Address buildx warnings for installer image builds**: Aligns Dockerfile syntax casing to remove buildx warnings, keeping installer builds clean ([**@nbykov0**](https://github.com/nbykov0) in #1682).
|
||||
* **[system:coredns] Align CoreDNS app labels with Talos defaults**: Matches CoreDNS labels to Talos conventions so services select pods consistently across platform and tenant clusters ([**@nbykov0**](https://github.com/nbykov0) in #1675).
|
||||
* **[system:monitoring-agents] Rename CoreDNS metrics service to avoid conflicts**: Renames the metrics service so it no longer clashes with the CoreDNS service used for name resolution in tenant clusters ([**@nbykov0**](https://github.com/nbykov0) in #1676).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.38.2...v0.38.3](https://github.com/cozystack/cozystack/compare/v0.38.2...v0.38.3)
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.38.4
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[linstor] Update piraeus-operator v2.10.2 to handle fsck checks reliably**: Upgrades LINSTOR CSI to avoid failed mounts when fsck sees mounted volumes, improving volume publish reliability ([**@kvaps**](https://github.com/kvaps) in #1689, #1697).
|
||||
* **[dashboard] Nest CustomFormsOverride properties under spec.properties**: Fixes schema generation so custom form properties are placed under `spec.properties`, preventing mis-rendered or missing form fields ([**@kvaps**](https://github.com/kvaps) in #1692, #1700).
|
||||
* **[virtual-machine] Guard PVC resize to only expand storage**: Ensures resize jobs run only when storage size increases, avoiding unintended shrink attempts during VM updates ([**@kvaps**](https://github.com/kvaps) in #1688, #1701).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] Clarify GPU check command**: Makes the kubectl command for validating GPU binding more explicit, including namespace context ([**@nbykov0**](https://github.com/nbykov0) in cozystack/website#379).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.38.3...v0.38.4](https://github.com/cozystack/cozystack/compare/v0.38.3...v0.38.4)
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
# Cozystack v0.39 — "Enhanced Networking & Monitoring"
|
||||
|
||||
This release introduces topology-aware routing for Cilium services, automatic pod rollouts on configuration changes, improved monitoring capabilities, and numerous bug fixes and improvements across the platform.
|
||||
|
||||
## Highlights
|
||||
|
||||
* **Topology-Aware Routing**: Enabled topology-aware routing for Cilium services, improving traffic distribution and reducing latency by routing traffic to endpoints in the same zone when possible ([**@nbykov0**](https://github.com/nbykov0) in #1734).
|
||||
* **Automatic Pod Rollouts**: Cilium and Cilium operator pods now automatically restart when configuration changes, ensuring configuration updates are applied immediately ([**@kvaps**](https://github.com/kvaps) in #1728).
|
||||
* **Windows VM Scheduling**: Added nodeAffinity configuration for Windows VMs based on scheduling config, enabling dedicated nodes for Windows workloads ([**@kvaps**](https://github.com/kvaps) in #1693).
|
||||
* **SeaweedFS Updates**: Updated to SeaweedFS v4.02 with improved S3 daemon performance and fixes ([**@kvaps**](https://github.com/kvaps) in #1725).
|
||||
|
||||
---
|
||||
|
||||
## Major Features and Improvements
|
||||
|
||||
### Networking
|
||||
|
||||
* **[system/cilium] Enable topology-aware routing for services**: Enabled topology-aware routing for services, improving traffic distribution and reducing latency by routing traffic to endpoints in the same zone when possible. This feature helps optimize network performance in multi-zone deployments ([**@nbykov0**](https://github.com/nbykov0) in #1734).
|
||||
* **[cilium] Enable automatic pod rollout on configmap updates**: Cilium and Cilium operator pods now automatically restart when the cilium-config ConfigMap is updated, ensuring configuration changes are applied immediately without manual intervention ([**@kvaps**](https://github.com/kvaps) in #1728).
|
||||
|
||||
### Virtual Machines
|
||||
|
||||
* **[virtual-machine,vm-instance] Add nodeAffinity for Windows VMs based on scheduling config**: Added nodeAffinity configuration to virtual-machine and vm-instance charts to support dedicated nodes for Windows VMs. When `dedicatedNodesForWindowsVMs` is enabled in the `cozystack-scheduling` ConfigMap, Windows VMs are scheduled on nodes with label `scheduling.cozystack.io/vm-windows=true`, while non-Windows VMs prefer nodes without this label ([**@kvaps**](https://github.com/kvaps) in #1693).
|
||||
|
||||
### Storage
|
||||
|
||||
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 with improved performance for S3 daemon and fixes for known issues. This update includes better S3 compatibility and performance improvements ([**@kvaps**](https://github.com/kvaps) in #1725).
|
||||
|
||||
### Tools
|
||||
|
||||
* **[talm] feat(init)!: require --name flag for cluster name**: Breaking change: The `talm init` command now requires the `--name` flag to specify the cluster name. This ensures consistent cluster naming and prevents accidental initialization without a name ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#86).
|
||||
* **[talm] feat(template): preserve extra YAML documents in output**: Templates now preserve extra YAML documents in the output, allowing for more flexible template processing ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#87).
|
||||
* **[talm] feat: add directory expansion for -f flag**: Added directory expansion support for the `-f` flag, allowing users to specify directories instead of individual files ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@ca5713e).
|
||||
* **[talm] Introduce automatic root detection**: Added automatic root detection logic to simplify talm usage and reduce manual configuration ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@d165162).
|
||||
* **[talm] Introduce talm kubeconfig --login command**: Added new `talm kubeconfig --login` command for easier kubeconfig management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@5f7e05b).
|
||||
* **[talm] Introduce encryption**: Added encryption support to talm for secure configuration management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#81).
|
||||
* **[talm] Replace code-generation with wrapper on talosctl**: Refactored talm to use a wrapper on talosctl instead of code generation, simplifying the codebase and improving maintainability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#80).
|
||||
* **[talm] Use go embed instead of code generation**: Migrated from code generation to go embed for better build performance and simpler dependency management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#79).
|
||||
* **[boot-to-talos] Cozystack: Update Talos Linux v1.11.3**: Updated boot-to-talos to use Talos Linux v1.11.3 ([**@kvaps**](https://github.com/kvaps) in cozystack/boot-to-talos#7).
|
||||
|
||||
## Improvements
|
||||
|
||||
* **[seaweedfs] Extended CA certificate duration to reduce disruptive CA rotations**: Extended CA certificate duration to reduce disruptive CA rotations, improving long-term certificate management and reducing operational overhead ([**@IvanHunters**](https://github.com/IvanHunters) in #1657).
|
||||
* **[dashboard] Add config hash annotations to restart pods on config changes**: Added config hash annotations to dashboard deployment templates to ensure pods are automatically restarted when their configuration changes, ensuring configuration updates are applied immediately ([**@kvaps**](https://github.com/kvaps) in #1662).
|
||||
* **[tenant][kubernetes] Introduce better cleanup logic**: Improved cleanup logic for tenant Kubernetes resources, ensuring proper resource cleanup when tenants are deleted or updated. Added automated pre-delete cleanup job for tenant namespaces to remove tenant-related releases during uninstall ([**@kvaps**](https://github.com/kvaps) in #1661).
|
||||
* **[system:coredns] update coredns app labels to match Talos coredns labels**: Updated coredns app labels to match Talos coredns labels, ensuring consistency across the platform ([**@nbykov0**](https://github.com/nbykov0) in #1675).
|
||||
* **[system:monitoring-agents] rename coredns metrics service**: Renamed coredns metrics service to avoid interference with coredns service used for name resolution in tenant k8s clusters ([**@nbykov0**](https://github.com/nbykov0) in #1676).
|
||||
* **[core:installer] Address buildx warnings**: Fixed Dockerfile syntax warnings from buildx, ensuring clean builds without warnings ([**@nbykov0**](https://github.com/nbykov0) in #1682).
|
||||
* **[talm] Refactor root detection logic into single file**: Improved code organization by consolidating root detection logic into a single file ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@487b479).
|
||||
* **[talm] Refactor init logic, better upgrade**: Improved initialization logic and upgrade process for better reliability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@c512777).
|
||||
* **[talm] Sugar for kubeconfig command**: Added convenience features to the kubeconfig command for improved usability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@a4010b3).
|
||||
* **[talm] wrap upgrade command**: Wrapped upgrade command for better integration and error handling ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@2e1afbf).
|
||||
* **[talm] docs(readme): add Homebrew installation option**: Added Homebrew installation option to the README for easier installation on macOS ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm@12bd4f2).
|
||||
* **[talm] cozystack: disable nodeCIDRs allocation**: Disabled nodeCIDRs allocation in talm for better network configuration control ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#82).
|
||||
* **[talm] Update license to Apache2.0**: Updated license to Apache 2.0 for better compatibility and clarity ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@eda1032).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK**: Refactored the apiserver REST handlers to use typed objects (`appsv1alpha1.Application`) instead of `unstructured.Unstructured`, eliminating the need for runtime conversions and simplifying the codebase. Additionally, fixed an issue where `UnstructuredList` objects were using the first registered kind from `typeToGVK` instead of the kind from the object's field when multiple kinds are registered with the same Go type. This fix includes the upstream fix from kubernetes/kubernetes#135537 ([**@kvaps**](https://github.com/kvaps) in #1679).
|
||||
* **[dashboard] Fix CustomFormsOverride schema to nest properties under spec.properties**: Fixed the logic for generating CustomFormsOverride schema to properly nest properties under `spec.properties` instead of directly under `properties`, ensuring correct form schema generation ([**@kvaps**](https://github.com/kvaps) in #1692).
|
||||
* **[virtual-machine] Improve check for resizing job**: Improved storage resize logic to only expand persistent volume claims when storage is being increased, preventing unintended storage reduction operations. Added validation to accurately compare current and desired storage sizes before triggering resize operations ([**@kvaps**](https://github.com/kvaps) in #1688).
|
||||
* **[linstor] Update piraeus-operator v2.10.2**: Updated LINSTOR CSI to fix issues with the new fsck behaviour, resolving mount failures when fsck attempts to run on mounted devices ([**@kvaps**](https://github.com/kvaps) in #1689).
|
||||
* **[api] Revert dynamic list kinds representation fix (fixes namespace deletion regression)**: Reverted changes from #1630 that caused a regression affecting namespace deletion and upgrades from previous versions. The regression caused namespace deletion failures with errors like "content is not a list: []unstructured.Unstructured" during namespace finalization. This revert restores compatibility with namespace deletion controller and fixes upgrade issues from previous versions ([**@kvaps**](https://github.com/kvaps) in #1677).
|
||||
* **[talm] fix: normalize template paths for Windows compatibility**: Fixed template path handling to ensure Windows compatibility by normalizing paths ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#88).
|
||||
|
||||
## Dependencies
|
||||
|
||||
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 ([**@kvaps**](https://github.com/kvaps) in #1725).
|
||||
* **[linstor] Update piraeus-operator v2.10.2**: Updated piraeus-operator to version 2.10.2 ([**@kvaps**](https://github.com/kvaps) in #1689).
|
||||
* **[talm] Cozystack: Update Talos Linux v1.11.3**: Updated talm to use Talos Linux v1.11.3 ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#83).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] Add article: Talm v0.17: Built-in Age Encryption for Secrets Management**: Added comprehensive blog post announcing Talm v0.17 and its built-in age-based encryption for secrets. Covers initial setup and key generation, encryption/decryption workflows, idempotent encryption behavior, automatic .gitignore handling, file permission safeguards, security best practices, and guidance for GitOps and CI/CD integration ([**@kvaps**](https://github.com/kvaps) in [cozystack/website#384](https://github.com/cozystack/website/pull/384)).
|
||||
* **[website] docs(talm): update talm init syntax for mandatory --preset and --name flags**: Updated documentation to reflect breaking changes in talm, adding mandatory `--preset` and `--name` flags to the talm init command ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#386).
|
||||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
We'd like to thank all contributors who made this release possible:
|
||||
|
||||
* [**@IvanHunters**](https://github.com/IvanHunters)
|
||||
* [**@kvaps**](https://github.com/kvaps)
|
||||
* [**@lexfrei**](https://github.com/lexfrei)
|
||||
* [**@nbykov0**](https://github.com/nbykov0)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.38.0...v0.39.0](https://github.com/cozystack/cozystack/compare/v0.38.0...v0.39.0)
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.39.0
|
||||
-->
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.39.1
|
||||
-->
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
* **[monitoring] Add SLACK_SEVERITY_FILTER field and VMAgent for tenant monitoring**: Introduced the SLACK_SEVERITY_FILTER environment variable in the Alerta deployment to enable filtering of alert severities for Slack notifications based on the disabledSeverity configuration. Additionally, added a VMAgent resource template for scraping metrics within tenant namespaces, improving monitoring granularity and control. This enhancement allows administrators to configure which alert severities are sent to Slack and enables tenant-specific metrics collection for better observability ([**@IvanHunters**](https://github.com/IvanHunters) in #1712).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.39.0...v0.39.1](https://github.com/cozystack/cozystack/compare/v0.39.0...v0.39.1)
|
||||
|
||||
31
examples/platform-example.yaml
Normal file
31
examples/platform-example.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
kind: Platform
|
||||
metadata:
|
||||
name: cozystack-platform
|
||||
# Cluster-scoped resource, no namespace needed
|
||||
spec:
|
||||
# SourceRef is required - reference to the OCIRepository or GitRepository
|
||||
sourceRef:
|
||||
kind: OCIRepository
|
||||
name: cozystack-packages
|
||||
namespace: cozy-system
|
||||
|
||||
# Optional: Interval for HelmRelease reconciliation (default: 5m)
|
||||
interval: 5m
|
||||
|
||||
# Optional: BasePath is the base path where the platform chart is located in the source.
|
||||
# For GitRepository, defaults to "packages/core/platform" if not specified.
|
||||
# For OCIRepository, defaults to "core/platform" if not specified.
|
||||
# basePath: core/platform
|
||||
|
||||
# Optional: Values to pass to HelmRelease
|
||||
# These values will be merged with sourceRef (which is automatically added)
|
||||
values:
|
||||
# Any custom values can be added here
|
||||
# sourceRef will be automatically added by the controller
|
||||
# Example custom values:
|
||||
# customKey: customValue
|
||||
# nested:
|
||||
# config:
|
||||
# enabled: true
|
||||
|
||||
8
go.mod
8
go.mod
@@ -6,14 +6,16 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
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/go-logr/logr v1.4.3
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/google/gofuzz v1.2.0
|
||||
github.com/onsi/ginkgo/v2 v2.23.3
|
||||
github.com/onsi/gomega v1.37.0
|
||||
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/stretchr/testify v1.11.1
|
||||
go.uber.org/zap v1.27.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.34.1
|
||||
@@ -44,6 +46,7 @@ require (
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
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/fsnotify/fsnotify v1.9.0 // indirect
|
||||
@@ -124,6 +127,3 @@ require (
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
// See: issues.k8s.io/135537
|
||||
replace k8s.io/apimachinery => github.com/cozystack/apimachinery v0.0.0-20251219010959-1f91eabae46c
|
||||
|
||||
12
go.sum
12
go.sum
@@ -18,8 +18,6 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cozystack/apimachinery v0.0.0-20251219010959-1f91eabae46c h1:C2wIfH/OzhU9XOK/e6Ik9cg7nZ1z6fN4lf6a3yFdik8=
|
||||
github.com/cozystack/apimachinery v0.0.0-20251219010959-1f91eabae46c/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -37,10 +35,16 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fluxcd/helm-controller/api v1.4.3 h1:CdZwjL1liXmYCWyk2jscmFEB59tICIlnWB9PfDDW5q4=
|
||||
github.com/fluxcd/helm-controller/api v1.4.3/go.mod h1:0XrBhKEaqvxyDj/FziG1Q8Fmx2UATdaqLgYqmZh6wW4=
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA=
|
||||
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/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=
|
||||
@@ -142,8 +146,6 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
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=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -296,6 +298,8 @@ k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
|
||||
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
|
||||
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
|
||||
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
|
||||
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
|
||||
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||
k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA=
|
||||
k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0=
|
||||
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
|
||||
|
||||
@@ -48,7 +48,7 @@ echo " End: $RELEASE_END"
|
||||
echo ""
|
||||
|
||||
# Loop through ALL optional repositories
|
||||
for repo_name in talm boot-to-talos cozyhr cozy-proxy; do
|
||||
for repo_name in talm boot-to-talos cozypkg cozy-proxy; do
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Checking repository: $repo_name"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
# macOS-compatible sed in-place
|
||||
ifeq ($(shell uname),Darwin)
|
||||
SED_INPLACE := sed -i ''
|
||||
else
|
||||
SED_INPLACE := sed -i
|
||||
endif
|
||||
|
||||
REGISTRY ?= ghcr.io/cozystack/cozystack
|
||||
TAG = $(shell git describe --tags --exact-match 2>/dev/null || echo latest)
|
||||
PUSH := 1
|
||||
@@ -12,13 +12,19 @@ command -V tar >/dev/null || exit $?
|
||||
|
||||
echo "Collecting Cozystack information..."
|
||||
mkdir -p $REPORT_DIR/cozystack
|
||||
kubectl get deploy -n cozy-system cozystack -o jsonpath='{.spec.template.spec.containers[0].image}' > $REPORT_DIR/cozystack/image.txt 2>&1
|
||||
kubectl get cm -n cozy-system --no-headers | awk '$1 ~ /^cozystack/' |
|
||||
while read NAME _; do
|
||||
DIR=$REPORT_DIR/cozystack/configs
|
||||
mkdir -p $DIR
|
||||
kubectl get cm -n cozy-system $NAME -o yaml > $DIR/$NAME.yaml 2>&1
|
||||
done
|
||||
kubectl get deploy -n cozy-system cozystack-operator cozystack-controller -o yaml > $REPORT_DIR/cozystack/deployments.yaml 2>&1
|
||||
|
||||
echo "Collecting platforms..."
|
||||
kubectl get platforms.cozystack.io -A > $REPORT_DIR/cozystack/platforms.txt 2>&1
|
||||
kubectl get platforms.cozystack.io -A -o yaml > $REPORT_DIR/cozystack/platforms.yaml 2>&1
|
||||
|
||||
echo "Collecting bundles..."
|
||||
kubectl get bundles.cozystack.io -A > $REPORT_DIR/cozystack/bundles.txt 2>&1
|
||||
kubectl get bundles.cozystack.io -A -o yaml > $REPORT_DIR/cozystack/bundles.yaml 2>&1
|
||||
|
||||
echo "Collecting applicationdefinitions..."
|
||||
kubectl get applicationdefinitions.cozystack.io -A > $REPORT_DIR/cozystack/applicationdefinitions.txt 2>&1
|
||||
kubectl get applicationdefinitions.cozystack.io -A -o yaml > $REPORT_DIR/cozystack/applicationdefinitions.yaml 2>&1
|
||||
|
||||
# -- kubernetes module
|
||||
|
||||
@@ -56,6 +62,36 @@ kubectl get hr -A --no-headers | awk '$4 != "True"' | \
|
||||
kubectl describe hr -n $NAMESPACE $NAME > $DIR/describe.txt 2>&1
|
||||
done
|
||||
|
||||
echo "Collecting artifactgenerators..."
|
||||
kubectl get artifactgenerators.source.extensions.fluxcd.io -A > $REPORT_DIR/kubernetes/artifactgenerators.txt 2>&1
|
||||
kubectl get artifactgenerators.source.extensions.fluxcd.io -A --no-headers | awk '$4 != "True"' | \
|
||||
while read NAMESPACE NAME _; do
|
||||
DIR=$REPORT_DIR/kubernetes/artifactgenerators/$NAMESPACE/$NAME
|
||||
mkdir -p $DIR
|
||||
kubectl get artifactgenerators.source.extensions.fluxcd.io -n $NAMESPACE $NAME -o yaml > $DIR/artifactgenerator.yaml 2>&1
|
||||
kubectl describe artifactgenerators.source.extensions.fluxcd.io -n $NAMESPACE $NAME > $DIR/describe.txt 2>&1
|
||||
done
|
||||
|
||||
echo "Collecting ocirepositories..."
|
||||
kubectl get ocirepositories.source.toolkit.fluxcd.io -A > $REPORT_DIR/kubernetes/ocirepositories.txt 2>&1
|
||||
kubectl get ocirepositories.source.toolkit.fluxcd.io -A --no-headers | awk '$4 != "True"' | \
|
||||
while read NAMESPACE NAME _; do
|
||||
DIR=$REPORT_DIR/kubernetes/ocirepositories/$NAMESPACE/$NAME
|
||||
mkdir -p $DIR
|
||||
kubectl get ocirepositories.source.toolkit.fluxcd.io -n $NAMESPACE $NAME -o yaml > $DIR/ocirepository.yaml 2>&1
|
||||
kubectl describe ocirepositories.source.toolkit.fluxcd.io -n $NAMESPACE $NAME > $DIR/describe.txt 2>&1
|
||||
done
|
||||
|
||||
echo "Collecting gitrepositories..."
|
||||
kubectl get gitrepositories.source.toolkit.fluxcd.io -A > $REPORT_DIR/kubernetes/gitrepositories.txt 2>&1
|
||||
kubectl get gitrepositories.source.toolkit.fluxcd.io -A --no-headers | awk '$4 != "True"' | \
|
||||
while read NAMESPACE NAME _; do
|
||||
DIR=$REPORT_DIR/kubernetes/gitrepositories/$NAMESPACE/$NAME
|
||||
mkdir -p $DIR
|
||||
kubectl get gitrepositories.source.toolkit.fluxcd.io -n $NAMESPACE $NAME -o yaml > $DIR/gitrepository.yaml 2>&1
|
||||
kubectl describe gitrepositories.source.toolkit.fluxcd.io -n $NAMESPACE $NAME > $DIR/describe.txt 2>&1
|
||||
done
|
||||
|
||||
echo "Collecting pods..."
|
||||
kubectl get pod -A -o wide > $REPORT_DIR/kubernetes/pods.txt 2>&1
|
||||
kubectl get pod -A --no-headers | awk '$4 !~ /Running|Succeeded|Completed/' |
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
@test "Create DB MongoDB" {
|
||||
name='test'
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: apps.cozystack.io/v1alpha1
|
||||
kind: MongoDB
|
||||
metadata:
|
||||
name: $name
|
||||
namespace: tenant-test
|
||||
spec:
|
||||
external: false
|
||||
size: 10Gi
|
||||
replicas: 1
|
||||
storageClass: ""
|
||||
resourcesPreset: "nano"
|
||||
backup:
|
||||
enabled: false
|
||||
EOF
|
||||
sleep 5
|
||||
# Wait for HelmRelease
|
||||
kubectl -n tenant-test wait hr mongodb-$name --timeout=60s --for=condition=ready
|
||||
# Wait for MongoDB service (port 27017)
|
||||
timeout 120 sh -ec "until kubectl -n tenant-test get svc mongodb-$name-rs0 -o jsonpath='{.spec.ports[0].port}' | grep -q '27017'; do sleep 10; done"
|
||||
# Wait for endpoints
|
||||
timeout 180 sh -ec "until kubectl -n tenant-test get endpoints mongodb-$name-rs0 -o jsonpath='{.subsets[*].addresses[*].ip}' | grep -q '[0-9]'; do sleep 10; done"
|
||||
# Wait for StatefulSet replicas
|
||||
kubectl -n tenant-test wait statefulset.apps/mongodb-$name-rs0 --timeout=300s --for=jsonpath='{.status.replicas}'=1
|
||||
# Cleanup
|
||||
kubectl -n tenant-test delete mongodbs.apps.cozystack.io $name
|
||||
}
|
||||
@@ -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=5m --for=jsonpath='{.status.kubernetesResources.version.status}'=Ready
|
||||
kubectl wait tcp -n tenant-test kubernetes-${test_name} --timeout=2m --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 500s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &'
|
||||
bash -c 'timeout 300s 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,100 +124,6 @@ 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
|
||||
|
||||
|
||||
@@ -8,23 +8,51 @@
|
||||
}
|
||||
|
||||
@test "Install Cozystack" {
|
||||
# Create namespace & configmap required by installer
|
||||
kubectl create namespace cozy-system --dry-run=client -o yaml | kubectl apply -f -
|
||||
kubectl create configmap cozystack -n cozy-system \
|
||||
--from-literal=bundle-name=paas-full \
|
||||
--from-literal=ipv4-pod-cidr=10.244.0.0/16 \
|
||||
--from-literal=ipv4-pod-gateway=10.244.0.1 \
|
||||
--from-literal=ipv4-svc-cidr=10.96.0.0/16 \
|
||||
--from-literal=ipv4-join-cidr=100.64.0.0/16 \
|
||||
--from-literal=root-host=example.org \
|
||||
--from-literal=api-server-endpoint=https://192.168.123.10:6443 \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# Apply installer manifests from file
|
||||
kubectl apply -f _out/assets/cozystack-installer.yaml
|
||||
|
||||
# Wait for the installer deployment to become available
|
||||
kubectl wait deployment/cozystack -n cozy-system --timeout=1m --for=condition=Available
|
||||
kubectl wait deployment/cozystack-operator -n cozy-system --timeout=1m --for=condition=Available
|
||||
|
||||
# Wait for cozy-fluxcd namespace to be created
|
||||
timeout 30 sh -ec 'until kubectl get namespace cozy-fluxcd >/dev/null 2>&1; do sleep 1; done'
|
||||
|
||||
# Wait for Flux deployment
|
||||
timeout 30 sh -ec 'until kubectl get deployment/flux -n cozy-fluxcd >/dev/null 2>&1; do sleep 1; done'
|
||||
kubectl wait deployment/flux -n cozy-fluxcd --timeout=1m --for=condition=Available
|
||||
|
||||
# Create Platform resource instead of configmap
|
||||
kubectl apply -f - <<'EOF'
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
kind: Platform
|
||||
metadata:
|
||||
name: cozystack-platform
|
||||
spec:
|
||||
sourceRef:
|
||||
kind: OCIRepository
|
||||
name: cozystack-packages
|
||||
namespace: cozy-system
|
||||
values:
|
||||
bundles:
|
||||
system:
|
||||
type: "full"
|
||||
networking:
|
||||
podCIDR: "10.244.0.0/16"
|
||||
podGateway: "10.244.0.1"
|
||||
serviceCIDR: "10.96.0.0/16"
|
||||
joinCIDR: "100.64.0.0/16"
|
||||
publishing:
|
||||
host: "example.org"
|
||||
apiServerEndpoint: "https://192.168.123.10:6443"
|
||||
EOF
|
||||
|
||||
# Wait for ArtifactGenerator for cozystack-packages
|
||||
timeout 60 sh -ec 'until kubectl get artifactgenerators.source.extensions.fluxcd.io cozystack-packages -n cozy-system >/dev/null 2>&1; do sleep 1; done'
|
||||
kubectl wait artifactgenerators.source.extensions.fluxcd.io/cozystack-packages -n cozy-system --for=condition=ready --timeout=5m
|
||||
|
||||
# Wait for bundle ArtifactGenerators
|
||||
timeout 60 sh -ec 'until kubectl get artifactgenerators.source.extensions.fluxcd.io cozystack-system cozystack-iaas cozystack-paas cozystack-naas -n cozy-system >/dev/null 2>&1; do sleep 1; done'
|
||||
kubectl wait artifactgenerators.source.extensions.fluxcd.io -n cozy-system --for=condition=ready --timeout=5m cozystack-system cozystack-iaas cozystack-paas cozystack-naas
|
||||
|
||||
# Wait until HelmReleases appear & reconcile them
|
||||
timeout 60 sh -ec 'until kubectl get hr -A -l cozystack.io/system-app=true | grep -q cozys; do sleep 1; done'
|
||||
@@ -140,9 +168,8 @@ EOF
|
||||
kubectl wait hr/seaweedfs-system -n tenant-root --timeout=2m --for=condition=ready
|
||||
fi
|
||||
|
||||
|
||||
# Expose Cozystack services through ingress
|
||||
kubectl patch configmap/cozystack -n cozy-system --type merge -p '{"data":{"expose-services":"api,dashboard,cdi-uploadproxy,vm-exportproxy,keycloak"}}'
|
||||
kubectl patch platform/cozystack-platform --type merge -p '{"spec":{"values":{"publishing":{"exposedServices":["api","dashboard","cdi-uploadproxy","vm-exportproxy","keycloak"]}}}}'
|
||||
|
||||
# NGINX ingress controller
|
||||
timeout 60 sh -ec 'until kubectl get deploy root-ingress-controller -n tenant-root >/dev/null 2>&1; do sleep 1; done'
|
||||
@@ -169,7 +196,7 @@ EOF
|
||||
}
|
||||
|
||||
@test "Keycloak OIDC stack is healthy" {
|
||||
kubectl patch configmap/cozystack -n cozy-system --type merge -p '{"data":{"oidc-enabled":"true"}}'
|
||||
kubectl patch platform/cozystack-platform --type merge -p '{"spec":{"values":{"authentication":{"oidc":{"enabled":true}}}}}'
|
||||
|
||||
timeout 120 sh -ec 'until kubectl get hr -n cozy-keycloak keycloak keycloak-configure keycloak-operator >/dev/null 2>&1; do sleep 1; done'
|
||||
kubectl wait hr/keycloak hr/keycloak-configure hr/keycloak-operator -n cozy-keycloak --timeout=10m --for=condition=ready
|
||||
|
||||
@@ -21,33 +21,14 @@
|
||||
}
|
||||
|
||||
@test "Test kinds" {
|
||||
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/tenants | jq -r '.kind')
|
||||
if [ "$val" != "TenantList" ]; then
|
||||
echo "Expected kind to be TenantList, got $val"
|
||||
exit 1
|
||||
fi
|
||||
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/tenants | jq -r '.items[0].kind')
|
||||
if [ "$val" != "Tenant" ]; then
|
||||
echo "Expected kind to be Tenant, got $val"
|
||||
exit 1
|
||||
fi
|
||||
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/ingresses | jq -r '.kind')
|
||||
if [ "$val" != "IngressList" ]; then
|
||||
echo "Expected kind to be IngressList, got $val"
|
||||
exit 1
|
||||
fi
|
||||
val=$(kubectl get --raw /apis/apps.cozystack.io/v1alpha1/ingresses | jq -r '.items[0].kind')
|
||||
if [ "$val" != "Ingress" ]; then
|
||||
echo "Expected kind to be Ingress, got $val"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "Create and delete namespace" {
|
||||
kubectl create ns cozy-test-create-and-delete-namespace --dry-run=client -o yaml | kubectl apply -f -
|
||||
if ! kubectl delete ns cozy-test-create-and-delete-namespace; then
|
||||
echo "Failed to delete namespace"
|
||||
kubectl describe ns cozy-test-create-and-delete-namespace
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -5,22 +5,22 @@ help: ## Show this help.
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
show: check ## Show output of rendered templates
|
||||
cozyhr show -n $(NAMESPACE) $(NAME)
|
||||
cozypkg show -n $(NAMESPACE) $(NAME)
|
||||
|
||||
apply: check suspend ## Apply Helm release to a Kubernetes cluster
|
||||
cozyhr apply -n $(NAMESPACE) $(NAME)
|
||||
cozypkg apply -n $(NAMESPACE) $(NAME)
|
||||
|
||||
diff: check ## Diff Helm release against objects in a Kubernetes cluster
|
||||
cozyhr diff -n $(NAMESPACE) $(NAME)
|
||||
cozypkg diff -n $(NAMESPACE) $(NAME)
|
||||
|
||||
suspend: check ## Suspend reconciliation for an existing Helm release
|
||||
cozyhr suspend -n $(NAMESPACE) $(NAME)
|
||||
cozypkg suspend -n $(NAMESPACE) $(NAME)
|
||||
|
||||
resume: check ## Resume reconciliation for an existing Helm release
|
||||
cozyhr resume -n $(NAMESPACE) $(NAME)
|
||||
cozypkg resume -n $(NAMESPACE) $(NAME)
|
||||
|
||||
delete: check suspend ## Delete Helm release from a Kubernetes cluster
|
||||
cozyhr delete -n $(NAMESPACE) $(NAME)
|
||||
cozypkg delete -n $(NAMESPACE) $(NAME)
|
||||
|
||||
check:
|
||||
@if [ -z "$(NAME)" ]; then echo "env NAME is not set!" >&2; exit 1; fi
|
||||
@@ -54,5 +54,9 @@ kube::codegen::gen_openapi \
|
||||
|
||||
$CONTROLLER_GEN object:headerFile="hack/boilerplate.go.txt" paths="./api/..."
|
||||
$CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=packages/system/cozystack-controller/crds
|
||||
mv packages/system/cozystack-controller/crds/cozystack.io_cozystackresourcedefinitions.yaml \
|
||||
packages/system/cozystack-resource-definition-crd/definition/cozystack.io_cozystackresourcedefinitions.yaml
|
||||
mv packages/system/cozystack-controller/crds/cozystack.io_applicationdefinitions.yaml \
|
||||
packages/core/installer/crds/cozystack.io_applicationdefinitions.yaml
|
||||
mv packages/system/cozystack-controller/crds/cozystack.io_bundles.yaml \
|
||||
packages/core/installer/crds/cozystack.io_bundles.yaml
|
||||
mv packages/system/cozystack-controller/crds/cozystack.io_platforms.yaml \
|
||||
packages/core/installer/crds/cozystack.io_platforms.yaml
|
||||
|
||||
@@ -8,6 +8,7 @@ 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="../../core/platform/bundles/*/applicationdefinitions"
|
||||
|
||||
[[ -f "$CHART_YAML" ]] || { echo "No $CHART_YAML found"; exit 1; }
|
||||
[[ -f "$SCHEMA_JSON" ]] || { echo "No $SCHEMA_JSON found"; exit 1; }
|
||||
@@ -21,8 +22,6 @@ 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
|
||||
@@ -55,37 +54,71 @@ fi
|
||||
# Base64 (portable: no -w / -b options)
|
||||
ICON_B64="$(base64 < "$ICON_PATH" | tr -d '\n' | tr -d '\r')"
|
||||
|
||||
# Decide which HelmRepository name to use based on path
|
||||
# .../apps/... -> cozystack-apps
|
||||
# .../extra/... -> cozystack-extra
|
||||
# default: cozystack-apps
|
||||
SOURCE_NAME="cozystack-apps"
|
||||
case "$PWD" in
|
||||
*"/apps/"*) SOURCE_NAME="cozystack-apps" ;;
|
||||
*"/extra/"*) SOURCE_NAME="cozystack-extra" ;;
|
||||
esac
|
||||
# Find path to output CRD YAML
|
||||
OUT="$(find $CRD_DIR -type f -name "${NAME}.yaml" | head -n 1)"
|
||||
if [[ -z "$OUT" ]]; then
|
||||
echo "Error: ApplicationDefinition file for '${NAME}' not found in ${CRD_DIR}"
|
||||
echo "Please create the file first in one of the following directories:"
|
||||
|
||||
# Auto-detect existing directories
|
||||
BASE_DIR="../../core/platform/bundles"
|
||||
if [[ -d "$BASE_DIR" ]]; then
|
||||
for bundle_dir in "$BASE_DIR"/*/applicationdefinitions; do
|
||||
if [[ -d "$bundle_dir" ]]; then
|
||||
bundle_name="$(basename "$(dirname "$bundle_dir")")"
|
||||
echo " touch ${bundle_dir}/${NAME}.yaml # ${bundle_name}"
|
||||
fi
|
||||
done
|
||||
else
|
||||
# Fallback if base directory doesn't exist
|
||||
echo " touch ../../core/platform/bundles/iaas/applicationdefinitions/${NAME}.yaml"
|
||||
echo " touch ../../core/platform/bundles/paas/applicationdefinitions/${NAME}.yaml"
|
||||
echo " touch ../../core/platform/bundles/naas/applicationdefinitions/${NAME}.yaml"
|
||||
echo " touch ../../core/platform/bundles/system/applicationdefinitions/${NAME}.yaml"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# If file doesn't exist, create a minimal skeleton
|
||||
OUT="${OUT:-$CRD_DIR/$NAME.yaml}"
|
||||
if [[ ! -f "$OUT" ]]; then
|
||||
if [[ ! -s "$OUT" ]]; then
|
||||
cat >"$OUT" <<EOF
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
kind: CozystackResourceDefinition
|
||||
kind: ApplicationDefinition
|
||||
metadata:
|
||||
name: ${NAME}
|
||||
spec: {}
|
||||
spec:
|
||||
release:
|
||||
values:
|
||||
_cozystack:
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Determine package type (apps or extra) from current directory
|
||||
CURRENT_DIR="$(pwd)"
|
||||
PACKAGE_TYPE="apps" # default
|
||||
if [[ "$CURRENT_DIR" == *"/packages/extra/"* ]]; then
|
||||
PACKAGE_TYPE="extra"
|
||||
elif [[ "$CURRENT_DIR" == *"/packages/apps/"* ]]; then
|
||||
PACKAGE_TYPE="apps"
|
||||
fi
|
||||
|
||||
# Extract bundle type (iaas, paas, naas, system) from OUT path
|
||||
OUT_DIR="$(dirname "$OUT")"
|
||||
BUNDLE_DIR="$(dirname "$OUT_DIR")"
|
||||
BUNDLE_TYPE="$(basename "$BUNDLE_DIR")"
|
||||
ARTIFACT_PREFIX="cozystack-${BUNDLE_TYPE}"
|
||||
ARTIFACT_NAME="${ARTIFACT_PREFIX}-${NAME}"
|
||||
|
||||
# Export vars for yq env()
|
||||
export RES_NAME="$NAME"
|
||||
export PREFIX="$NAME-"
|
||||
if [ "$SOURCE_NAME" == "cozystack-extra" ]; then
|
||||
# For packages/extra, prefix should be empty; for packages/apps, prefix is "${NAME}-"
|
||||
if [[ "$PACKAGE_TYPE" == "extra" ]]; then
|
||||
export PREFIX=""
|
||||
else
|
||||
export PREFIX="${NAME}-"
|
||||
fi
|
||||
export DESCRIPTION="$DESC"
|
||||
export ICON_B64="$ICON_B64"
|
||||
export SOURCE_NAME="$SOURCE_NAME"
|
||||
export ARTIFACT_NAME="$ARTIFACT_NAME"
|
||||
export SCHEMA_JSON_MIN="$(jq -c . "$SCHEMA_JSON")"
|
||||
|
||||
# Generate keysOrder from values.yaml
|
||||
@@ -115,6 +148,12 @@ export KEYS_ORDER="$(
|
||||
'
|
||||
)"
|
||||
|
||||
# Remove lines with cozystack.build-values before updating (Helm template syntax breaks yq parsing)
|
||||
if [[ -f "$OUT" && -n "$OUT" ]]; then
|
||||
# Use grep to filter out the line, more reliable than sed
|
||||
grep -v 'cozystack\.build-values' "$OUT" > "${OUT}.tmp" && mv "${OUT}.tmp" "$OUT"
|
||||
fi
|
||||
|
||||
# Update only necessary fields in-place
|
||||
# - openAPISchema is loaded from file as a multi-line string (block scalar)
|
||||
# - labels ensure cozystack.io/ui: "true"
|
||||
@@ -122,19 +161,26 @@ export KEYS_ORDER="$(
|
||||
# - sourceRef derived from directory (apps|extra)
|
||||
yq -i '
|
||||
.apiVersion = (.apiVersion // "cozystack.io/v1alpha1") |
|
||||
.kind = (.kind // "CozystackResourceDefinition") |
|
||||
.kind = (.kind // "ApplicationDefinition") |
|
||||
.metadata.name = strenv(RES_NAME) |
|
||||
.spec.application.openAPISchema = strenv(SCHEMA_JSON_MIN) |
|
||||
(.spec.application.openAPISchema style="literal") |
|
||||
.spec.release.prefix = (strenv(PREFIX)) |
|
||||
.spec.release.labels."cozystack.io/ui" = "true" |
|
||||
.spec.release.chart.name = strenv(RES_NAME) |
|
||||
.spec.release.chart.sourceRef.kind = "HelmRepository" |
|
||||
.spec.release.chart.sourceRef.name = strenv(SOURCE_NAME) |
|
||||
.spec.release.chart.sourceRef.namespace = "cozy-public" |
|
||||
del(.spec.release.chart) |
|
||||
.spec.release.chartRef.sourceRef.kind = "ExternalArtifact" |
|
||||
.spec.release.chartRef.sourceRef.name = strenv(ARTIFACT_NAME) |
|
||||
.spec.release.chartRef.sourceRef.namespace = "cozy-system" |
|
||||
.spec.dashboard.description = strenv(DESCRIPTION) |
|
||||
.spec.dashboard.icon = strenv(ICON_B64) |
|
||||
.spec.dashboard.keysOrder = env(KEYS_ORDER)
|
||||
' "$OUT"
|
||||
|
||||
# Add back the Helm template line after _cozystack
|
||||
if [[ -f "$OUT" && -n "$OUT" ]]; then
|
||||
HELM_TEMPLATE=' {{- include "cozystack.build-values" . | nindent 8 }}'
|
||||
# Use awk for more reliable insertion
|
||||
awk -v template="$HELM_TEMPLATE" '/_cozystack:/ {print; print template; next} {print}' "$OUT" > "${OUT}.tmp" && mv "${OUT}.tmp" "$OUT"
|
||||
fi
|
||||
|
||||
echo "Updated $OUT"
|
||||
|
||||
502
internal/controller/applicationdefinition_controller.go
Normal file
502
internal/controller/applicationdefinition_controller.go
Normal file
@@ -0,0 +1,502 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"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/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/cozystack/cozystack/pkg/cozylib"
|
||||
)
|
||||
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=applicationdefinitions,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;update;patch
|
||||
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch
|
||||
type ApplicationDefinitionReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
|
||||
Debounce time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
lastEvent time.Time
|
||||
lastHandled time.Time
|
||||
|
||||
CozystackAPIKind string
|
||||
}
|
||||
|
||||
func (r *ApplicationDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.Info("Reconciling ApplicationDefinitions", "request", req.NamespacedName)
|
||||
|
||||
// Get all ApplicationDefinitions
|
||||
crdList := &cozyv1alpha1.ApplicationDefinitionList{}
|
||||
if err := r.List(ctx, crdList); err != nil {
|
||||
logger.Error(err, "failed to list ApplicationDefinitions")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
logger.Info("Found ApplicationDefinitions", "count", len(crdList.Items))
|
||||
|
||||
// Update HelmReleases for each CRD
|
||||
for i := range crdList.Items {
|
||||
crd := &crdList.Items[i]
|
||||
logger.V(4).Info("Processing CRD", "crd", crd.Name, "hasValues", crd.Spec.Release.Values != nil)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with debounced restart logic
|
||||
return r.debouncedRestart(ctx)
|
||||
}
|
||||
|
||||
func (r *ApplicationDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
if r.Debounce == 0 {
|
||||
r.Debounce = 5 * time.Second
|
||||
}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("applicationdefinition-controller").
|
||||
Watches(
|
||||
&cozyv1alpha1.ApplicationDefinition{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
r.mu.Lock()
|
||||
r.lastEvent = time.Now()
|
||||
r.mu.Unlock()
|
||||
return []reconcile.Request{{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: "cozy-system",
|
||||
Name: "cozystack-api",
|
||||
},
|
||||
}}
|
||||
}),
|
||||
).
|
||||
Watches(
|
||||
&helmv2.HelmRelease{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
hr, ok := obj.(*helmv2.HelmRelease)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Only watch HelmReleases with cozystack.io/ui=true label
|
||||
if hr.Labels == nil || hr.Labels["cozystack.io/ui"] != "true" {
|
||||
return nil
|
||||
}
|
||||
// Trigger reconciliation of all CRDs when a HelmRelease with the label is created/updated
|
||||
r.mu.Lock()
|
||||
r.lastEvent = time.Now()
|
||||
r.mu.Unlock()
|
||||
return []reconcile.Request{{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: "cozy-system",
|
||||
Name: "cozystack-api",
|
||||
},
|
||||
}}
|
||||
}),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
type crdHashView struct {
|
||||
Name string `json:"name"`
|
||||
Spec cozyv1alpha1.ApplicationDefinitionSpec `json:"spec"`
|
||||
}
|
||||
|
||||
func (r *ApplicationDefinitionReconciler) computeConfigHash(ctx context.Context) (string, error) {
|
||||
list := &cozyv1alpha1.ApplicationDefinitionList{}
|
||||
if err := r.List(ctx, list); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
slices.SortFunc(list.Items, sortApplicationDefinitions)
|
||||
|
||||
views := make([]crdHashView, 0, len(list.Items))
|
||||
for i := range list.Items {
|
||||
views = append(views, crdHashView{
|
||||
Name: list.Items[i].Name,
|
||||
Spec: list.Items[i].Spec,
|
||||
})
|
||||
}
|
||||
b, err := json.Marshal(views)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sum := sha256.Sum256(b)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
func (r *ApplicationDefinitionReconciler) debouncedRestart(ctx context.Context) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
r.mu.Lock()
|
||||
le := r.lastEvent
|
||||
lh := r.lastHandled
|
||||
debounce := r.Debounce
|
||||
r.mu.Unlock()
|
||||
|
||||
if debounce <= 0 {
|
||||
debounce = 5 * time.Second
|
||||
}
|
||||
if le.IsZero() {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if d := time.Since(le); d < debounce {
|
||||
return ctrl.Result{RequeueAfter: debounce - d}, nil
|
||||
}
|
||||
if !lh.Before(le) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
newHash, err := r.computeConfigHash(ctx)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
tpl, obj, patch, err := r.getWorkload(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"})
|
||||
if err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
oldHash := tpl.Annotations["cozystack.io/config-hash"]
|
||||
|
||||
if oldHash == newHash && oldHash != "" {
|
||||
r.mu.Lock()
|
||||
r.lastHandled = le
|
||||
r.mu.Unlock()
|
||||
logger.Info("No changes in CRD config; skipping restart", "hash", newHash)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
tpl.Annotations["cozystack.io/config-hash"] = newHash
|
||||
|
||||
if err := r.Patch(ctx, obj, patch); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.lastHandled = le
|
||||
r.mu.Unlock()
|
||||
|
||||
logger.Info("Updated cozystack-api podTemplate config-hash; rollout triggered",
|
||||
"old", oldHash, "new", newHash)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *ApplicationDefinitionReconciler) getWorkload(
|
||||
ctx context.Context,
|
||||
key types.NamespacedName,
|
||||
) (tpl *corev1.PodTemplateSpec, obj client.Object, patch client.Patch, err error) {
|
||||
if r.CozystackAPIKind == "Deployment" {
|
||||
dep := &appsv1.Deployment{}
|
||||
if err := r.Get(ctx, key, dep); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
obj = dep
|
||||
tpl = &dep.Spec.Template
|
||||
patch = client.MergeFrom(dep.DeepCopy())
|
||||
} else {
|
||||
ds := &appsv1.DaemonSet{}
|
||||
if err := r.Get(ctx, key, ds); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
obj = ds
|
||||
tpl = &ds.Spec.Template
|
||||
patch = client.MergeFrom(ds.DeepCopy())
|
||||
}
|
||||
if tpl.Annotations == nil {
|
||||
tpl.Annotations = make(map[string]string)
|
||||
}
|
||||
return tpl, obj, patch, nil
|
||||
}
|
||||
|
||||
func sortApplicationDefinitions(a, b cozyv1alpha1.ApplicationDefinition) int {
|
||||
if a.Name == b.Name {
|
||||
return 0
|
||||
}
|
||||
if a.Name < b.Name {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// updateHelmReleasesForCRD updates all HelmReleases that match the application labels from ApplicationDefinition
|
||||
func (r *ApplicationDefinitionReconciler) updateHelmReleasesForCRD(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) 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
|
||||
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.Info("Found HelmReleases to update", "crd", crd.Name, "kind", applicationKind, "count", len(hrList.Items), "hasValues", crd.Spec.Release.Values != nil)
|
||||
if crd.Spec.Release.Values != nil {
|
||||
logger.V(4).Info("CRD has values", "crd", crd.Name, "valuesSize", len(crd.Spec.Release.Values.Raw))
|
||||
}
|
||||
|
||||
// Log each HelmRelease that will be updated
|
||||
for i := range hrList.Items {
|
||||
hr := &hrList.Items[i]
|
||||
logger.V(4).Info("Processing HelmRelease", "name", hr.Name, "namespace", hr.Namespace, "kind", applicationKind)
|
||||
}
|
||||
|
||||
// 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/chartRef and values in HelmRelease based on ApplicationDefinition
|
||||
func (r *ApplicationDefinitionReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.ApplicationDefinition) error {
|
||||
logger := log.FromContext(ctx)
|
||||
updated := false
|
||||
hrCopy := hr.DeepCopy()
|
||||
|
||||
// Update based on Chart or ChartRef configuration
|
||||
if crd.Spec.Release.Chart != nil {
|
||||
// Using Chart (HelmRepository)
|
||||
if hrCopy.Spec.Chart == nil {
|
||||
// Need to create Chart spec
|
||||
hrCopy.Spec.Chart = &helmv2.HelmChartTemplate{
|
||||
Spec: helmv2.HelmChartTemplateSpec{
|
||||
Chart: crd.Spec.Release.Chart.Name,
|
||||
SourceRef: helmv2.CrossNamespaceObjectReference{
|
||||
Kind: crd.Spec.Release.Chart.SourceRef.Kind,
|
||||
Name: crd.Spec.Release.Chart.SourceRef.Name,
|
||||
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
|
||||
},
|
||||
},
|
||||
}
|
||||
// Clear ChartRef if it exists
|
||||
hrCopy.Spec.ChartRef = nil
|
||||
updated = true
|
||||
} else {
|
||||
// Update existing Chart spec
|
||||
if hrCopy.Spec.Chart.Spec.Chart != crd.Spec.Release.Chart.Name ||
|
||||
hrCopy.Spec.Chart.Spec.SourceRef.Kind != crd.Spec.Release.Chart.SourceRef.Kind ||
|
||||
hrCopy.Spec.Chart.Spec.SourceRef.Name != crd.Spec.Release.Chart.SourceRef.Name ||
|
||||
hrCopy.Spec.Chart.Spec.SourceRef.Namespace != crd.Spec.Release.Chart.SourceRef.Namespace {
|
||||
hrCopy.Spec.Chart.Spec.Chart = crd.Spec.Release.Chart.Name
|
||||
hrCopy.Spec.Chart.Spec.SourceRef = helmv2.CrossNamespaceObjectReference{
|
||||
Kind: crd.Spec.Release.Chart.SourceRef.Kind,
|
||||
Name: crd.Spec.Release.Chart.SourceRef.Name,
|
||||
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
|
||||
}
|
||||
// Clear ChartRef if it exists
|
||||
hrCopy.Spec.ChartRef = nil
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
} else if crd.Spec.Release.ChartRef != nil {
|
||||
// Using ChartRef (ExternalArtifact)
|
||||
expectedChartRef := &helmv2.CrossNamespaceSourceReference{
|
||||
Kind: "ExternalArtifact",
|
||||
Name: crd.Spec.Release.ChartRef.SourceRef.Name,
|
||||
Namespace: crd.Spec.Release.ChartRef.SourceRef.Namespace,
|
||||
}
|
||||
|
||||
if hrCopy.Spec.ChartRef == nil {
|
||||
// Need to create ChartRef
|
||||
hrCopy.Spec.ChartRef = expectedChartRef
|
||||
// Clear Chart if it exists
|
||||
hrCopy.Spec.Chart = nil
|
||||
updated = true
|
||||
} else {
|
||||
// Update existing ChartRef
|
||||
if hrCopy.Spec.ChartRef.Kind != expectedChartRef.Kind ||
|
||||
hrCopy.Spec.ChartRef.Name != expectedChartRef.Name ||
|
||||
hrCopy.Spec.ChartRef.Namespace != expectedChartRef.Namespace {
|
||||
hrCopy.Spec.ChartRef = expectedChartRef
|
||||
// Clear Chart if it exists
|
||||
hrCopy.Spec.Chart = nil
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update Values from CRD if specified
|
||||
var mergedValues *apiextensionsv1.JSON
|
||||
var err error
|
||||
if crd.Spec.Release.Values != nil {
|
||||
logger.V(4).Info("Merging values from CRD", "name", hr.Name, "namespace", hr.Namespace, "crd", crd.Name)
|
||||
mergedValues, err = cozylib.MergeValuesWithCRDPriority(crd.Spec.Release.Values, hrCopy.Spec.Values)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to merge values", "name", hr.Name, "namespace", hr.Namespace)
|
||||
return fmt.Errorf("failed to merge values: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Even if CRD has no values, we still need to ensure _namespace is set
|
||||
mergedValues = hrCopy.Spec.Values
|
||||
}
|
||||
|
||||
// Always inject namespace annotations (top-level _namespace field)
|
||||
// This matches the behavior in cozystack-api and NamespaceHelmReconciler
|
||||
namespace := &corev1.Namespace{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: hrCopy.Namespace}, namespace); err == nil {
|
||||
mergedValues, err = cozylib.InjectNamespaceAnnotationsIntoValues(mergedValues, namespace)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to inject namespace annotations", "name", hr.Name, "namespace", hr.Namespace)
|
||||
// Continue even if namespace annotations injection fails
|
||||
}
|
||||
}
|
||||
|
||||
// Always update values to ensure _cozystack and _namespace are applied
|
||||
// This ensures that CRD values (especially _cozystack and _namespace) are always applied
|
||||
// We always update to ensure CRD values are propagated, even if they appear equal
|
||||
// This is important because JSON comparison might not catch all differences (e.g., field order)
|
||||
if crd.Spec.Release.Values != nil || mergedValues != hrCopy.Spec.Values {
|
||||
hrCopy.Spec.Values = mergedValues
|
||||
updated = true
|
||||
if crd.Spec.Release.Values != nil {
|
||||
logger.Info("Updated values from CRD", "name", hr.Name, "namespace", hr.Namespace, "crd", crd.Name)
|
||||
} else {
|
||||
logger.V(4).Info("Updated values with namespace labels", "name", hr.Name, "namespace", hr.Namespace, "crd", crd.Name)
|
||||
}
|
||||
} else {
|
||||
logger.V(4).Info("No values update needed", "name", hr.Name, "namespace", hr.Namespace, "crd", crd.Name)
|
||||
}
|
||||
|
||||
if !updated {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update the HelmRelease
|
||||
patch := client.MergeFrom(hr.DeepCopy())
|
||||
if err := r.Patch(ctx, hrCopy, patch); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Updated HelmRelease", "name", hr.Name, "namespace", hr.Namespace, "crd", crd.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeHelmReleaseValues merges CRD default values with existing HelmRelease values
|
||||
// All fields are merged except "_cozystack" and "_namespace" which are fully overwritten from CRD values
|
||||
// Existing HelmRelease values (outside of _cozystack and _namespace) take precedence (user values override defaults)
|
||||
func (r *ApplicationDefinitionReconciler) mergeHelmReleaseValues(crdValues, existingValues *apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
|
||||
// If CRD has no values, preserve existing
|
||||
if crdValues == nil || len(crdValues.Raw) == 0 {
|
||||
return existingValues, nil
|
||||
}
|
||||
|
||||
// If existing has no values, use CRD values
|
||||
if existingValues == nil || len(existingValues.Raw) == 0 {
|
||||
return crdValues, nil
|
||||
}
|
||||
|
||||
var crdMap, existingMap map[string]interface{}
|
||||
|
||||
// Parse CRD values (defaults)
|
||||
if err := json.Unmarshal(crdValues.Raw, &crdMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal CRD values: %w", err)
|
||||
}
|
||||
|
||||
// Parse existing HelmRelease values
|
||||
if err := json.Unmarshal(existingValues.Raw, &existingMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal existing values: %w", err)
|
||||
}
|
||||
|
||||
// Start with existing values as base (user values take priority)
|
||||
// Then merge CRD values on top, but _cozystack and _namespace from CRD completely overwrite
|
||||
merged := deepMergeMaps(existingMap, crdMap)
|
||||
|
||||
// Explicitly handle "_cozystack" field: CRD values completely overwrite existing
|
||||
// This ensures _cozystack field from CRD is always used, even if user modified it
|
||||
if crdCozystack, exists := crdMap["_cozystack"]; exists {
|
||||
merged["_cozystack"] = crdCozystack
|
||||
}
|
||||
|
||||
// Explicitly handle "_namespace" field: CRD values completely overwrite existing
|
||||
// This ensures _namespace field from CRD is always used, even if user modified it
|
||||
if crdNamespace, exists := crdMap["_namespace"]; exists {
|
||||
merged["_namespace"] = crdNamespace
|
||||
}
|
||||
|
||||
mergedJSON, err := json.Marshal(merged)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal merged values: %w", err)
|
||||
}
|
||||
|
||||
return &apiextensionsv1.JSON{Raw: mergedJSON}, nil
|
||||
}
|
||||
|
||||
// deepMergeMaps performs a deep merge of two maps
|
||||
func deepMergeMaps(base, override map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// Copy base map
|
||||
for k, v := range base {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
// Merge override map
|
||||
for k, v := range override {
|
||||
if baseVal, exists := result[k]; exists {
|
||||
// If both are maps, recursively merge
|
||||
if baseMap, ok := baseVal.(map[string]interface{}); ok {
|
||||
if overrideMap, ok := v.(map[string]interface{}); ok {
|
||||
result[k] = deepMergeMaps(baseMap, overrideMap)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// Override takes precedence
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// valuesEqual compares two JSON values for equality
|
||||
func valuesEqual(a, b *apiextensionsv1.JSON) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
// Simple byte comparison (could be improved with canonical JSON)
|
||||
return string(a.Raw) == string(b.Raw)
|
||||
}
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"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/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
type CozystackResourceDefinitionReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
|
||||
Debounce time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
lastEvent time.Time
|
||||
lastHandled time.Time
|
||||
|
||||
CozystackAPIKind string
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
// Only handle debounced restart logic
|
||||
// HelmRelease reconciliation is handled by CozystackResourceDefinitionHelmReconciler
|
||||
return r.debouncedRestart(ctx)
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
if r.Debounce == 0 {
|
||||
r.Debounce = 5 * time.Second
|
||||
}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystackresource-controller").
|
||||
Watches(
|
||||
&cozyv1alpha1.CozystackResourceDefinition{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
r.mu.Lock()
|
||||
r.lastEvent = time.Now()
|
||||
r.mu.Unlock()
|
||||
return []reconcile.Request{{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: "cozy-system",
|
||||
Name: "cozystack-api",
|
||||
},
|
||||
}}
|
||||
}),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
type crdHashView struct {
|
||||
Name string `json:"name"`
|
||||
Spec cozyv1alpha1.CozystackResourceDefinitionSpec `json:"spec"`
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) computeConfigHash(ctx context.Context) (string, error) {
|
||||
list := &cozyv1alpha1.CozystackResourceDefinitionList{}
|
||||
if err := r.List(ctx, list); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
slices.SortFunc(list.Items, sortCozyRDs)
|
||||
|
||||
views := make([]crdHashView, 0, len(list.Items))
|
||||
for i := range list.Items {
|
||||
views = append(views, crdHashView{
|
||||
Name: list.Items[i].Name,
|
||||
Spec: list.Items[i].Spec,
|
||||
})
|
||||
}
|
||||
b, err := json.Marshal(views)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sum := sha256.Sum256(b)
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Context) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
r.mu.Lock()
|
||||
le := r.lastEvent
|
||||
lh := r.lastHandled
|
||||
debounce := r.Debounce
|
||||
r.mu.Unlock()
|
||||
|
||||
if debounce <= 0 {
|
||||
debounce = 5 * time.Second
|
||||
}
|
||||
if le.IsZero() {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
if d := time.Since(le); d < debounce {
|
||||
return ctrl.Result{RequeueAfter: debounce - d}, nil
|
||||
}
|
||||
if !lh.Before(le) {
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
newHash, err := r.computeConfigHash(ctx)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
tpl, obj, patch, err := r.getWorkload(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"})
|
||||
if err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
oldHash := tpl.Annotations["cozystack.io/config-hash"]
|
||||
|
||||
if oldHash == newHash && oldHash != "" {
|
||||
r.mu.Lock()
|
||||
r.lastHandled = le
|
||||
r.mu.Unlock()
|
||||
logger.Info("No changes in CRD config; skipping restart", "hash", newHash)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
tpl.Annotations["cozystack.io/config-hash"] = newHash
|
||||
|
||||
if err := r.Patch(ctx, obj, patch); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.lastHandled = le
|
||||
r.mu.Unlock()
|
||||
|
||||
logger.Info("Updated cozystack-api podTemplate config-hash; rollout triggered",
|
||||
"old", oldHash, "new", newHash)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) getWorkload(
|
||||
ctx context.Context,
|
||||
key types.NamespacedName,
|
||||
) (tpl *corev1.PodTemplateSpec, obj client.Object, patch client.Patch, err error) {
|
||||
if r.CozystackAPIKind == "Deployment" {
|
||||
dep := &appsv1.Deployment{}
|
||||
if err := r.Get(ctx, key, dep); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
obj = dep
|
||||
tpl = &dep.Spec.Template
|
||||
patch = client.MergeFrom(dep.DeepCopy())
|
||||
} else {
|
||||
ds := &appsv1.DaemonSet{}
|
||||
if err := r.Get(ctx, key, ds); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
obj = ds
|
||||
tpl = &ds.Spec.Template
|
||||
patch = client.MergeFrom(ds.DeepCopy())
|
||||
}
|
||||
if tpl.Annotations == nil {
|
||||
tpl.Annotations = make(map[string]string)
|
||||
}
|
||||
return tpl, obj, patch, nil
|
||||
}
|
||||
|
||||
func sortCozyRDs(a, b cozyv1alpha1.CozystackResourceDefinition) int {
|
||||
if a.Name == b.Name {
|
||||
return 0
|
||||
}
|
||||
if a.Name < b.Name {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
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 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
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
)
|
||||
|
||||
// ensureBreadcrumb creates or updates a Breadcrumb resource for the given CRD
|
||||
func (m *Manager) ensureBreadcrumb(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
func (m *Manager) ensureBreadcrumb(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
|
||||
group, version, kind := pickGVK(crd)
|
||||
|
||||
lowerKind := strings.ToLower(kind)
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
//
|
||||
// metadata.name: stock-namespace-<group>.<version>.<plural>
|
||||
// spec.id: stock-namespace-/<group>/<version>/<plural>
|
||||
func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (controllerutil.OperationResult, error) {
|
||||
func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) (controllerutil.OperationResult, error) {
|
||||
g, v, kind := pickGVK(crd)
|
||||
plural := pickPlural(kind, crd)
|
||||
// Details page segment uses lowercase kind, mirroring your example
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
// ensureCustomFormsOverride creates or updates a CustomFormsOverride resource for the given CRD
|
||||
func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
|
||||
g, v, kind := pickGVK(crd)
|
||||
plural := pickPlural(kind, crd)
|
||||
|
||||
@@ -105,26 +105,8 @@ func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
|
||||
"properties": map[string]any{},
|
||||
}
|
||||
|
||||
// Check if there's a spec property
|
||||
specProp, ok := props["spec"].(map[string]any)
|
||||
if !ok {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
specProps, ok := specProp["properties"].(map[string]any)
|
||||
if !ok {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
// Create spec.properties structure in schema
|
||||
schemaProps := schema["properties"].(map[string]any)
|
||||
specSchema := map[string]any{
|
||||
"properties": map[string]any{},
|
||||
}
|
||||
schemaProps["spec"] = specSchema
|
||||
|
||||
// Process spec properties recursively
|
||||
processSpecProperties(specProps, specSchema["properties"].(map[string]any))
|
||||
processSpecProperties(props, schema["properties"].(map[string]any))
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
@@ -9,46 +9,41 @@ func TestBuildMultilineStringSchema(t *testing.T) {
|
||||
// Test OpenAPI schema with various field types
|
||||
openAPISchema := `{
|
||||
"properties": {
|
||||
"spec": {
|
||||
"simpleString": {
|
||||
"type": "string",
|
||||
"description": "A simple string field"
|
||||
},
|
||||
"stringWithEnum": {
|
||||
"type": "string",
|
||||
"enum": ["option1", "option2"],
|
||||
"description": "String with enum should be skipped"
|
||||
},
|
||||
"numberField": {
|
||||
"type": "number",
|
||||
"description": "Number field should be skipped"
|
||||
},
|
||||
"nestedObject": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"simpleString": {
|
||||
"nestedString": {
|
||||
"type": "string",
|
||||
"description": "A simple string field"
|
||||
"description": "Nested string should get multilineString"
|
||||
},
|
||||
"stringWithEnum": {
|
||||
"nestedStringWithEnum": {
|
||||
"type": "string",
|
||||
"enum": ["option1", "option2"],
|
||||
"description": "String with enum should be skipped"
|
||||
},
|
||||
"numberField": {
|
||||
"type": "number",
|
||||
"description": "Number field should be skipped"
|
||||
},
|
||||
"nestedObject": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"nestedString": {
|
||||
"type": "string",
|
||||
"description": "Nested string should get multilineString"
|
||||
},
|
||||
"nestedStringWithEnum": {
|
||||
"type": "string",
|
||||
"enum": ["a", "b"],
|
||||
"description": "Nested string with enum should be skipped"
|
||||
}
|
||||
}
|
||||
},
|
||||
"arrayOfObjects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"itemString": {
|
||||
"type": "string",
|
||||
"description": "String in array item"
|
||||
}
|
||||
}
|
||||
"enum": ["a", "b"],
|
||||
"description": "Nested string with enum should be skipped"
|
||||
}
|
||||
}
|
||||
},
|
||||
"arrayOfObjects": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"itemString": {
|
||||
"type": "string",
|
||||
"description": "String in array item"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,44 +70,33 @@ func TestBuildMultilineStringSchema(t *testing.T) {
|
||||
t.Fatal("schema.properties is not a map")
|
||||
}
|
||||
|
||||
// Check spec property exists
|
||||
spec, ok := props["spec"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("spec not found in properties")
|
||||
}
|
||||
|
||||
specProps, ok := spec["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("spec.properties is not a map")
|
||||
}
|
||||
|
||||
// Check simpleString
|
||||
simpleString, ok := specProps["simpleString"].(map[string]any)
|
||||
simpleString, ok := props["simpleString"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("simpleString not found in spec.properties")
|
||||
t.Fatal("simpleString not found in properties")
|
||||
}
|
||||
if simpleString["type"] != "multilineString" {
|
||||
t.Errorf("simpleString should have type multilineString, got %v", simpleString["type"])
|
||||
}
|
||||
|
||||
// Check stringWithEnum should not be present (or should not have multilineString)
|
||||
if stringWithEnum, ok := specProps["stringWithEnum"].(map[string]any); ok {
|
||||
if stringWithEnum, ok := props["stringWithEnum"].(map[string]any); ok {
|
||||
if stringWithEnum["type"] == "multilineString" {
|
||||
t.Error("stringWithEnum should not have multilineString type")
|
||||
}
|
||||
}
|
||||
|
||||
// Check numberField should not be present
|
||||
if numberField, ok := specProps["numberField"].(map[string]any); ok {
|
||||
if numberField, ok := props["numberField"].(map[string]any); ok {
|
||||
if numberField["type"] != nil {
|
||||
t.Error("numberField should not have any type override")
|
||||
}
|
||||
}
|
||||
|
||||
// Check nested object
|
||||
nestedObject, ok := specProps["nestedObject"].(map[string]any)
|
||||
nestedObject, ok := props["nestedObject"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("nestedObject not found in spec.properties")
|
||||
t.Fatal("nestedObject not found in properties")
|
||||
}
|
||||
nestedProps, ok := nestedObject["properties"].(map[string]any)
|
||||
if !ok {
|
||||
@@ -129,9 +113,9 @@ func TestBuildMultilineStringSchema(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check array of objects
|
||||
arrayOfObjects, ok := specProps["arrayOfObjects"].(map[string]any)
|
||||
arrayOfObjects, ok := props["arrayOfObjects"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("arrayOfObjects not found in spec.properties")
|
||||
t.Fatal("arrayOfObjects not found in properties")
|
||||
}
|
||||
items, ok := arrayOfObjects["items"].(map[string]any)
|
||||
if !ok {
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// ensureCustomFormsPrefill creates or updates a CustomFormsPrefill resource for the given CRD
|
||||
func (m *Manager) ensureCustomFormsPrefill(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (reconcile.Result, error) {
|
||||
func (m *Manager) ensureCustomFormsPrefill(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) (reconcile.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
app := crd.Spec.Application
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
)
|
||||
|
||||
// ensureFactory creates or updates a Factory resource for the given CRD
|
||||
func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
|
||||
g, v, kind := pickGVK(crd)
|
||||
plural := pickPlural(kind, crd)
|
||||
|
||||
@@ -557,7 +557,7 @@ type factoryFlags struct {
|
||||
|
||||
// factoryFeatureFlags tries several conventional locations so you can evolve the API
|
||||
// without breaking the controller. Defaults are false (hidden).
|
||||
func factoryFeatureFlags(crd *cozyv1alpha1.CozystackResourceDefinition) factoryFlags {
|
||||
func factoryFeatureFlags(crd *cozyv1alpha1.ApplicationDefinition) factoryFlags {
|
||||
var f factoryFlags
|
||||
|
||||
f.Workloads = true
|
||||
|
||||
@@ -23,7 +23,7 @@ type fieldInfo struct {
|
||||
|
||||
// pickGVK tries to read group/version/kind from the CRD. We prefer the "application" section,
|
||||
// falling back to other likely fields if your schema differs.
|
||||
func pickGVK(crd *cozyv1alpha1.CozystackResourceDefinition) (group, version, kind string) {
|
||||
func pickGVK(crd *cozyv1alpha1.ApplicationDefinition) (group, version, kind string) {
|
||||
// Best guess based on your examples:
|
||||
if crd.Spec.Application.Kind != "" {
|
||||
kind = crd.Spec.Application.Kind
|
||||
@@ -41,7 +41,7 @@ func pickGVK(crd *cozyv1alpha1.CozystackResourceDefinition) (group, version, kin
|
||||
}
|
||||
|
||||
// pickPlural prefers a field on the CRD if you have it; otherwise do a simple lowercase + "s".
|
||||
func pickPlural(kind string, crd *cozyv1alpha1.CozystackResourceDefinition) string {
|
||||
func pickPlural(kind string, crd *cozyv1alpha1.ApplicationDefinition) string {
|
||||
// If you have crd.Spec.Application.Plural, prefer it. Example:
|
||||
if crd.Spec.Application.Plural != "" {
|
||||
return crd.Spec.Application.Plural
|
||||
|
||||
@@ -56,7 +56,7 @@ func NewManager(c client.Client, scheme *runtime.Scheme) *Manager {
|
||||
func (m *Manager) SetupWithManager(mgr ctrl.Manager) error {
|
||||
if err := ctrl.NewControllerManagedBy(mgr).
|
||||
Named("dashboard-reconciler").
|
||||
For(&cozyv1alpha1.CozystackResourceDefinition{}).
|
||||
For(&cozyv1alpha1.ApplicationDefinition{}).
|
||||
Complete(m); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func (m *Manager) SetupWithManager(mgr ctrl.Manager) error {
|
||||
func (m *Manager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
crd := &cozyv1alpha1.CozystackResourceDefinition{}
|
||||
crd := &cozyv1alpha1.ApplicationDefinition{}
|
||||
|
||||
err := m.Get(ctx, types.NamespacedName{Name: req.Name}, crd)
|
||||
if err != nil {
|
||||
@@ -99,7 +99,7 @@ func (m *Manager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result,
|
||||
// - ensureMarketplacePanel (implemented)
|
||||
// - ensureSidebar (implemented)
|
||||
// - ensureTableUriMapping (implemented)
|
||||
func (m *Manager) EnsureForCRD(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (reconcile.Result, error) {
|
||||
func (m *Manager) EnsureForCRD(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) (reconcile.Result, error) {
|
||||
// Early return if crd.Spec.Dashboard is nil to prevent oscillation
|
||||
if crd.Spec.Dashboard == nil {
|
||||
return reconcile.Result{}, nil
|
||||
@@ -148,7 +148,7 @@ func (m *Manager) InitializeStaticResources(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// addDashboardLabels adds standard dashboard management labels to a resource
|
||||
func (m *Manager) addDashboardLabels(obj client.Object, crd *cozyv1alpha1.CozystackResourceDefinition, resourceType string) {
|
||||
func (m *Manager) addDashboardLabels(obj client.Object, crd *cozyv1alpha1.ApplicationDefinition, resourceType string) {
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
@@ -197,7 +197,7 @@ func (m *Manager) getStaticResourceSelector() client.MatchingLabels {
|
||||
// CleanupOrphanedResources removes dashboard resources that are no longer needed
|
||||
// This should be called after cache warming to ensure all current resources are known
|
||||
func (m *Manager) CleanupOrphanedResources(ctx context.Context) error {
|
||||
var crdList cozyv1alpha1.CozystackResourceDefinitionList
|
||||
var crdList cozyv1alpha1.ApplicationDefinitionList
|
||||
if err := m.List(ctx, &crdList, &client.ListOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -228,7 +228,7 @@ func (m *Manager) CleanupOrphanedResources(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// buildExpectedResourceSet creates a map of expected resource names by type
|
||||
func (m *Manager) buildExpectedResourceSet(crds []cozyv1alpha1.CozystackResourceDefinition) map[string]map[string]bool {
|
||||
func (m *Manager) buildExpectedResourceSet(crds []cozyv1alpha1.ApplicationDefinition) map[string]map[string]bool {
|
||||
expected := make(map[string]map[string]bool)
|
||||
|
||||
// Initialize maps for each resource type
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// ensureMarketplacePanel creates or updates a MarketplacePanel resource for the given CRD
|
||||
func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) (reconcile.Result, error) {
|
||||
func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) (reconcile.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
mp := &dashv1alpha1.MarketplacePanel{}
|
||||
|
||||
@@ -28,12 +28,12 @@ import (
|
||||
// - Categories are ordered strictly as:
|
||||
// Marketplace, IaaS, PaaS, NaaS, <others A→Z>, Resources, Administration
|
||||
// - Items within each category: sort by Weight (desc), then Label (A→Z).
|
||||
func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
|
||||
// Build the full menu once.
|
||||
|
||||
// 1) Fetch all CRDs
|
||||
var all []cozyv1alpha1.CozystackResourceDefinition
|
||||
var crdList cozyv1alpha1.CozystackResourceDefinitionList
|
||||
var all []cozyv1alpha1.ApplicationDefinition
|
||||
var crdList cozyv1alpha1.ApplicationDefinitionList
|
||||
if err := m.List(ctx, &crdList, &client.ListOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -228,7 +228,7 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Cozystack
|
||||
// upsertMultipleSidebars creates/updates several Sidebar resources with the same menu spec.
|
||||
func (m *Manager) upsertMultipleSidebars(
|
||||
ctx context.Context,
|
||||
crd *cozyv1alpha1.CozystackResourceDefinition,
|
||||
crd *cozyv1alpha1.ApplicationDefinition,
|
||||
ids []string,
|
||||
keysAndTags map[string]any,
|
||||
menuItems []any,
|
||||
@@ -335,7 +335,7 @@ func orderCategoryLabels[T any](cats map[string][]T) []string {
|
||||
}
|
||||
|
||||
// safeCategory returns spec.dashboard.category or "Resources" if not set.
|
||||
func safeCategory(def *cozyv1alpha1.CozystackResourceDefinition) string {
|
||||
func safeCategory(def *cozyv1alpha1.ApplicationDefinition) string {
|
||||
if def == nil || def.Spec.Dashboard == nil {
|
||||
return "Resources"
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid
|
||||
createCustomColumnsOverride("factory-details-v1.services", []any{
|
||||
createCustomColumnWithSpecificColor("Name", "Service", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/kube-service-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
|
||||
createStringColumn("ClusterIP", ".spec.clusterIP"),
|
||||
createStringColumn("LoadbalancerIP", ".status.loadBalancer.ingress[0].ip"),
|
||||
createStringColumn("LoadbalancerIP", ".spec.loadBalancerIP"),
|
||||
createTimestampColumn("Created", ".metadata.creationTimestamp"),
|
||||
}),
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
// ensureTableUriMapping creates or updates a TableUriMapping resource for the given CRD
|
||||
func (m *Manager) ensureTableUriMapping(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
func (m *Manager) ensureTableUriMapping(ctx context.Context, crd *cozyv1alpha1.ApplicationDefinition) error {
|
||||
// Links are fully managed by the CustomColumnsOverride.
|
||||
return nil
|
||||
}
|
||||
|
||||
170
internal/controller/namespace_helm_reconciler.go
Normal file
170
internal/controller/namespace_helm_reconciler.go
Normal file
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
"github.com/cozystack/cozystack/pkg/cozylib"
|
||||
)
|
||||
|
||||
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;update;patch
|
||||
type NamespaceHelmReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// Reconcile processes namespace changes and updates HelmReleases with namespace labels
|
||||
func (r *NamespaceHelmReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Get the namespace
|
||||
namespace := &corev1.Namespace{}
|
||||
if err := r.Get(ctx, req.NamespacedName, namespace); err != nil {
|
||||
logger.Error(err, "unable to fetch Namespace")
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// Extract namespace.cozystack.io/* annotations
|
||||
namespaceLabels := cozylib.ExtractNamespaceAnnotations(namespace)
|
||||
if len(namespaceLabels) == 0 {
|
||||
// No namespace labels to process, skip
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
logger.Info("processing namespace labels", "namespace", namespace.Name, "labels", namespaceLabels)
|
||||
|
||||
// List all HelmReleases in this namespace
|
||||
helmReleaseList := &helmv2.HelmReleaseList{}
|
||||
if err := r.List(ctx, helmReleaseList, client.InNamespace(namespace.Name)); err != nil {
|
||||
logger.Error(err, "unable to list HelmReleases in namespace", "namespace", namespace.Name)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Update each HelmRelease with namespace labels
|
||||
updated := 0
|
||||
for i := range helmReleaseList.Items {
|
||||
hr := &helmReleaseList.Items[i]
|
||||
if err := r.updateHelmReleaseWithNamespaceLabels(ctx, hr, namespaceLabels); err != nil {
|
||||
logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
|
||||
continue
|
||||
}
|
||||
updated++
|
||||
}
|
||||
|
||||
if updated > 0 {
|
||||
logger.Info("updated HelmReleases with namespace labels", "namespace", namespace.Name, "count", updated)
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
|
||||
// updateHelmReleaseWithNamespaceLabels updates HelmRelease values with namespace labels
|
||||
func (r *NamespaceHelmReconciler) updateHelmReleaseWithNamespaceLabels(ctx context.Context, hr *helmv2.HelmRelease, namespaceLabels map[string]string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Parse current values
|
||||
var valuesMap map[string]interface{}
|
||||
if hr.Spec.Values != nil && len(hr.Spec.Values.Raw) > 0 {
|
||||
if err := json.Unmarshal(hr.Spec.Values.Raw, &valuesMap); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal HelmRelease values: %w", err)
|
||||
}
|
||||
} else {
|
||||
valuesMap = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Convert namespaceLabels from map[string]string to map[string]interface{}
|
||||
namespaceLabelsMap := make(map[string]interface{})
|
||||
for k, v := range namespaceLabels {
|
||||
namespaceLabelsMap[k] = v
|
||||
}
|
||||
|
||||
// Check if namespace labels need to be updated (top-level _namespace field)
|
||||
needsUpdate := false
|
||||
currentNamespace, exists := valuesMap["_namespace"]
|
||||
if !exists {
|
||||
needsUpdate = true
|
||||
valuesMap["_namespace"] = namespaceLabelsMap
|
||||
} else {
|
||||
currentNamespaceMap, ok := currentNamespace.(map[string]interface{})
|
||||
if !ok {
|
||||
needsUpdate = true
|
||||
valuesMap["_namespace"] = namespaceLabelsMap
|
||||
} else {
|
||||
// Compare and update if different
|
||||
for k, v := range namespaceLabelsMap {
|
||||
if currentVal, exists := currentNamespaceMap[k]; !exists || currentVal != v {
|
||||
needsUpdate = true
|
||||
currentNamespaceMap[k] = v
|
||||
}
|
||||
}
|
||||
// Remove keys that are no longer in namespace labels
|
||||
for k := range currentNamespaceMap {
|
||||
if _, exists := namespaceLabelsMap[k]; !exists {
|
||||
needsUpdate = true
|
||||
delete(currentNamespaceMap, k)
|
||||
}
|
||||
}
|
||||
if needsUpdate {
|
||||
valuesMap["_namespace"] = currentNamespaceMap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !needsUpdate {
|
||||
// No changes needed
|
||||
return nil
|
||||
}
|
||||
|
||||
// Marshal back to JSON
|
||||
mergedJSON, err := json.Marshal(valuesMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal values with namespace labels: %w", err)
|
||||
}
|
||||
|
||||
// Update HelmRelease
|
||||
patchTarget := hr.DeepCopy()
|
||||
patchTarget.Spec.Values = &apiextensionsv1.JSON{Raw: mergedJSON}
|
||||
|
||||
patch := client.MergeFrom(hr)
|
||||
if err := r.Patch(ctx, patchTarget, patch); err != nil {
|
||||
return fmt.Errorf("failed to patch HelmRelease: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("updated HelmRelease with namespace labels", "name", hr.Name, "namespace", hr.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager
|
||||
func (r *NamespaceHelmReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&corev1.Namespace{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
352
internal/fluxinstall/install.go
Normal file
352
internal/fluxinstall/install.go
Normal file
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
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 fluxinstall
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Install installs Flux components using embedded manifests.
|
||||
// It extracts the manifests and applies them to the cluster.
|
||||
// The namespace is automatically determined from the Namespace object in the manifests.
|
||||
func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifests func(string) error) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Create temporary directory for manifests
|
||||
tmpDir, err := os.MkdirTemp("", "flux-install-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Extract embedded manifests (generated by cozypkg)
|
||||
manifestsDir := filepath.Join(tmpDir, "manifests")
|
||||
if err := os.MkdirAll(manifestsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create manifests directory: %w", err)
|
||||
}
|
||||
|
||||
if err := writeEmbeddedManifests(manifestsDir); err != nil {
|
||||
return fmt.Errorf("failed to extract embedded manifests: %w", err)
|
||||
}
|
||||
|
||||
// Find the manifest file (should be fluxcd.yaml from cozypkg)
|
||||
manifestPath := filepath.Join(manifestsDir, "fluxcd.yaml")
|
||||
if _, err := os.Stat(manifestPath); err != nil {
|
||||
// Try to find any YAML file if fluxcd.yaml doesn't exist
|
||||
entries, err := os.ReadDir(manifestsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read manifests directory: %w", err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if strings.HasSuffix(entry.Name(), ".yaml") {
|
||||
manifestPath = filepath.Join(manifestsDir, entry.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and apply manifests
|
||||
objects, err := parseManifests(manifestPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse manifests: %w", err)
|
||||
}
|
||||
|
||||
if len(objects) == 0 {
|
||||
return fmt.Errorf("no objects found in manifests")
|
||||
}
|
||||
|
||||
// Inject KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT if set in operator environment
|
||||
if err := injectKubernetesServiceEnv(objects); err != nil {
|
||||
logger.Info("Failed to inject KUBERNETES_SERVICE_* env vars, continuing anyway", "error", err)
|
||||
}
|
||||
|
||||
// Extract namespace from Namespace object in manifests
|
||||
namespace, err := extractNamespace(objects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract namespace from manifests: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Installing Flux components", "namespace", namespace)
|
||||
|
||||
// Apply manifests using server-side apply
|
||||
logger.Info("Applying Flux manifests", "count", len(objects), "manifest", manifestPath, "namespace", namespace)
|
||||
if err := applyManifests(ctx, k8sClient, objects); err != nil {
|
||||
return fmt.Errorf("failed to apply manifests: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Flux installation completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseManifests parses YAML manifests into unstructured objects.
|
||||
func parseManifests(manifestPath string) ([]*unstructured.Unstructured, error) {
|
||||
data, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read manifest file: %w", err)
|
||||
}
|
||||
|
||||
return readYAMLObjects(bytes.NewReader(data))
|
||||
}
|
||||
|
||||
// readYAMLObjects parses multi-document YAML into unstructured objects.
|
||||
func readYAMLObjects(reader io.Reader) ([]*unstructured.Unstructured, error) {
|
||||
var objects []*unstructured.Unstructured
|
||||
yamlReader := k8syaml.NewYAMLReader(bufio.NewReader(reader))
|
||||
|
||||
for {
|
||||
doc, err := yamlReader.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read YAML document: %w", err)
|
||||
}
|
||||
|
||||
// Skip empty documents
|
||||
if len(bytes.TrimSpace(doc)) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
obj := &unstructured.Unstructured{}
|
||||
decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(doc), len(doc))
|
||||
if err := decoder.Decode(obj); err != nil {
|
||||
// Skip documents that can't be decoded (might be comments or empty)
|
||||
if err == io.EOF {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("failed to decode YAML document: %w", err)
|
||||
}
|
||||
|
||||
// Skip empty objects (no kind)
|
||||
if obj.GetKind() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
var stageTwo []*unstructured.Unstructured // Everything else
|
||||
|
||||
for _, obj := range objects {
|
||||
if isClusterDefinition(obj) {
|
||||
stageOne = append(stageOne, obj)
|
||||
} else {
|
||||
stageTwo = append(stageTwo, obj)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("failed to apply cluster definitions: %w", err)
|
||||
}
|
||||
|
||||
// Wait a bit for CRDs to be registered
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return fmt.Errorf("failed to apply resources: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
for _, obj := range objects {
|
||||
// Use server-side apply with force ownership and field manager
|
||||
// FieldManager is required for apply patch operations
|
||||
patchOptions := &client.PatchOptions{
|
||||
FieldManager: "cozystack-operator",
|
||||
Force: func() *bool { b := true; return &b }(),
|
||||
}
|
||||
|
||||
if err := k8sClient.Patch(ctx, obj, client.Apply, patchOptions); err != nil {
|
||||
return fmt.Errorf("failed to apply object %s/%s: %w", obj.GetKind(), obj.GetName(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// extractNamespace extracts the namespace name from the Namespace object in the manifests.
|
||||
func extractNamespace(objects []*unstructured.Unstructured) (string, error) {
|
||||
for _, obj := range objects {
|
||||
if obj.GetKind() == "Namespace" {
|
||||
namespace := obj.GetName()
|
||||
if namespace == "" {
|
||||
return "", fmt.Errorf("Namespace object has no name")
|
||||
}
|
||||
return namespace, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no Namespace object found in manifests")
|
||||
}
|
||||
|
||||
// isClusterDefinition checks if an object is a CRD or Namespace.
|
||||
func isClusterDefinition(obj *unstructured.Unstructured) bool {
|
||||
kind := obj.GetKind()
|
||||
return kind == "CustomResourceDefinition" || kind == "Namespace"
|
||||
}
|
||||
|
||||
// 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.
|
||||
func injectKubernetesServiceEnv(objects []*unstructured.Unstructured) error {
|
||||
kubernetesHost := os.Getenv("KUBERNETES_SERVICE_HOST")
|
||||
kubernetesPort := os.Getenv("KUBERNETES_SERVICE_PORT")
|
||||
|
||||
// If neither variable is set, nothing to do
|
||||
if kubernetesHost == "" && kubernetesPort == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, obj := range objects {
|
||||
kind := obj.GetKind()
|
||||
if kind != "Deployment" && kind != "StatefulSet" && kind != "DaemonSet" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Navigate to spec.template.spec.containers
|
||||
spec, found, err := unstructured.NestedMap(obj.Object, "spec", "template", "spec")
|
||||
if !found || err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update containers
|
||||
containers, found, err := unstructured.NestedSlice(spec, "containers")
|
||||
if found && err == nil {
|
||||
containers = updateContainersEnv(containers, kubernetesHost, kubernetesPort)
|
||||
if err := unstructured.SetNestedSlice(spec, containers, "containers"); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Update initContainers
|
||||
initContainers, found, err := unstructured.NestedSlice(spec, "initContainers")
|
||||
if found && err == nil {
|
||||
initContainers = updateContainersEnv(initContainers, kubernetesHost, kubernetesPort)
|
||||
if err := unstructured.SetNestedSlice(spec, initContainers, "initContainers"); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Update spec in the object
|
||||
if err := unstructured.SetNestedMap(obj.Object, spec, "spec", "template", "spec"); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateContainersEnv updates environment variables for a slice of containers.
|
||||
func updateContainersEnv(containers []interface{}, kubernetesHost, kubernetesPort string) []interface{} {
|
||||
for i, container := range containers {
|
||||
containerMap, ok := container.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
env, found, err := unstructured.NestedSlice(containerMap, "env")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !found {
|
||||
env = []interface{}{}
|
||||
}
|
||||
|
||||
// Update or add KUBERNETES_SERVICE_HOST
|
||||
if kubernetesHost != "" {
|
||||
env = setEnvVar(env, "KUBERNETES_SERVICE_HOST", kubernetesHost)
|
||||
}
|
||||
|
||||
// Update or add KUBERNETES_SERVICE_PORT
|
||||
if kubernetesPort != "" {
|
||||
env = setEnvVar(env, "KUBERNETES_SERVICE_PORT", kubernetesPort)
|
||||
}
|
||||
|
||||
// Update the container's env
|
||||
if err := unstructured.SetNestedSlice(containerMap, env, "env"); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update the container in the slice
|
||||
containers[i] = containerMap
|
||||
}
|
||||
|
||||
return containers
|
||||
}
|
||||
|
||||
// setEnvVar updates or adds an environment variable in the env slice.
|
||||
func setEnvVar(env []interface{}, name, value string) []interface{} {
|
||||
// Check if variable already exists
|
||||
for i, envVar := range env {
|
||||
envVarMap, ok := envVar.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if envVarMap["name"] == name {
|
||||
// Update existing variable
|
||||
envVarMap["value"] = value
|
||||
env[i] = envVarMap
|
||||
return env
|
||||
}
|
||||
}
|
||||
|
||||
// Add new variable
|
||||
env = append(env, map[string]interface{}{
|
||||
"name": name,
|
||||
"value": value,
|
||||
})
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
51
internal/fluxinstall/manifests.embed.go
Normal file
51
internal/fluxinstall/manifests.embed.go
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
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 fluxinstall
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
//go:embed manifests/*.yaml
|
||||
var embeddedFluxManifests embed.FS
|
||||
|
||||
// WriteEmbeddedManifests extracts embedded Flux manifests to a temporary directory.
|
||||
func WriteEmbeddedManifests(dir string) error {
|
||||
manifests, err := fs.ReadDir(embeddedFluxManifests, "manifests")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read embedded manifests: %w", err)
|
||||
}
|
||||
|
||||
for _, manifest := range manifests {
|
||||
data, err := fs.ReadFile(embeddedFluxManifests, path.Join("manifests", manifest.Name()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file %s: %w", manifest.Name(), err)
|
||||
}
|
||||
|
||||
outputPath := path.Join(dir, manifest.Name())
|
||||
if err := os.WriteFile(outputPath, data, 0666); err != nil {
|
||||
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11623,7 +11623,6 @@ spec:
|
||||
value: /tmp/.sigstore
|
||||
- name: NO_PROXY
|
||||
value: .svc
|
||||
{{- include "cozy.kubernetes_envs" . | nindent 12 }}
|
||||
image: ghcr.io/fluxcd/source-controller:v1.7.3
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
@@ -11694,7 +11693,6 @@ spec:
|
||||
value: /tmp/.sigstore
|
||||
- name: NO_PROXY
|
||||
value: .svc
|
||||
{{- include "cozy.kubernetes_envs" . | nindent 12 }}
|
||||
image: ghcr.io/fluxcd/kustomize-controller:v1.7.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
@@ -11760,7 +11758,6 @@ spec:
|
||||
value: /tmp/.sigstore
|
||||
- name: NO_PROXY
|
||||
value: .svc
|
||||
{{- include "cozy.kubernetes_envs" . | nindent 12 }}
|
||||
image: ghcr.io/fluxcd/helm-controller:v1.4.3
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
@@ -11823,7 +11820,6 @@ spec:
|
||||
value: /tmp/.sigstore
|
||||
- name: NO_PROXY
|
||||
value: .svc
|
||||
{{- include "cozy.kubernetes_envs" . | nindent 12 }}
|
||||
image: ghcr.io/fluxcd/notification-controller:v1.7.4
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
@@ -11894,7 +11890,6 @@ spec:
|
||||
value: /tmp/.sigstore
|
||||
- name: NO_PROXY
|
||||
value: .svc
|
||||
{{- include "cozy.kubernetes_envs" . | nindent 12 }}
|
||||
image: ghcr.io/fluxcd/source-watcher:v2.0.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
@@ -11936,7 +11931,6 @@ spec:
|
||||
name: data
|
||||
- mountPath: /tmp
|
||||
name: tmp
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
hostNetwork: true
|
||||
priorityClassName: system-cluster-critical
|
||||
securityContext:
|
||||
@@ -4,56 +4,28 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
)
|
||||
|
||||
type appRef struct {
|
||||
group string
|
||||
kind string
|
||||
}
|
||||
|
||||
type runtimeConfig struct {
|
||||
appCRDMap map[appRef]*cozyv1alpha1.CozystackResourceDefinition
|
||||
}
|
||||
|
||||
func (l *LineageControllerWebhook) initConfig() {
|
||||
l.initOnce.Do(func() {
|
||||
if l.config.Load() == nil {
|
||||
l.config.Store(&runtimeConfig{
|
||||
appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition),
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// getApplicationLabel safely extracts an application label from HelmRelease
|
||||
func getApplicationLabel(hr *helmv2.HelmRelease, key string) (string, error) {
|
||||
if hr.Labels == nil {
|
||||
return "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: labels are nil", hr.Namespace, hr.Name)
|
||||
}
|
||||
val, ok := hr.Labels[key]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: missing %s label", hr.Namespace, hr.Name, key)
|
||||
}
|
||||
return val, nil
|
||||
// No longer needed - we use labels directly from HelmRelease
|
||||
}
|
||||
|
||||
func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string, string, error) {
|
||||
// Extract application metadata from labels
|
||||
appKind, err := getApplicationLabel(hr, "apps.cozystack.io/application.kind")
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
appKind, ok := hr.Labels["apps.cozystack.io/application.kind"]
|
||||
if !ok {
|
||||
return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: missing apps.cozystack.io/application.kind label", hr.Namespace, hr.Name)
|
||||
}
|
||||
|
||||
appGroup, err := getApplicationLabel(hr, "apps.cozystack.io/application.group")
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
appGroup, ok := hr.Labels["apps.cozystack.io/application.group"]
|
||||
if !ok {
|
||||
return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: missing apps.cozystack.io/application.group label", hr.Namespace, hr.Name)
|
||||
}
|
||||
|
||||
appName, err := getApplicationLabel(hr, "apps.cozystack.io/application.name")
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
appName, ok := hr.Labels["apps.cozystack.io/application.name"]
|
||||
if !ok {
|
||||
return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app: missing apps.cozystack.io/application.name label", hr.Namespace, hr.Name)
|
||||
}
|
||||
|
||||
// Construct API version from group
|
||||
@@ -63,11 +35,5 @@ func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string,
|
||||
// HelmRelease name format: <prefix><application-name>
|
||||
prefix := strings.TrimSuffix(hr.Name, appName)
|
||||
|
||||
// Validate the derived prefix
|
||||
// This ensures correctness when appName appears multiple times in hr.Name
|
||||
if prefix+appName != hr.Name {
|
||||
return "", "", "", fmt.Errorf("cannot derive prefix from helm release %s/%s: name does not end with application name %s", hr.Namespace, hr.Name, appName)
|
||||
}
|
||||
|
||||
return apiVersion, appKind, prefix, nil
|
||||
}
|
||||
|
||||
@@ -1,44 +1,11 @@
|
||||
package lineagecontrollerwebhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=cozystackresourcedefinitions,verbs=list;watch;get
|
||||
|
||||
// SetupWithManagerAsController is no longer needed since we don't watch ApplicationDefinitions
|
||||
func (c *LineageControllerWebhook) SetupWithManagerAsController(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&cozyv1alpha1.CozystackResourceDefinition{}).
|
||||
Complete(c)
|
||||
}
|
||||
|
||||
func (c *LineageControllerWebhook) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
l := log.FromContext(ctx)
|
||||
crds := &cozyv1alpha1.CozystackResourceDefinitionList{}
|
||||
if err := c.List(ctx, crds); err != nil {
|
||||
l.Error(err, "failed reading CozystackResourceDefinitions")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
cfg := &runtimeConfig{
|
||||
appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition),
|
||||
}
|
||||
for _, crd := range crds.Items {
|
||||
appRef := appRef{
|
||||
"apps.cozystack.io",
|
||||
crd.Spec.Application.Kind,
|
||||
}
|
||||
|
||||
newRef := crd
|
||||
if _, exists := cfg.appCRDMap[appRef]; exists {
|
||||
l.Info("duplicate app mapping detected; ignoring subsequent entry", "key", appRef)
|
||||
} else {
|
||||
cfg.appCRDMap[appRef] = &newRef
|
||||
}
|
||||
}
|
||||
c.config.Store(cfg)
|
||||
return ctrl.Result{}, nil
|
||||
// No controller needed - we use labels directly from HelmRelease
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func matchName(ctx context.Context, name string, templateContext map[string]stri
|
||||
return false
|
||||
}
|
||||
|
||||
func matchResourceToSelector(ctx context.Context, name string, templateContext, l map[string]string, s *cozyv1alpha1.CozystackResourceDefinitionResourceSelector) bool {
|
||||
func matchResourceToSelector(ctx context.Context, name string, templateContext, l map[string]string, s *cozyv1alpha1.ApplicationDefinitionResourceSelector) bool {
|
||||
sel, err := metav1.LabelSelectorAsSelector(&s.LabelSelector)
|
||||
if err != nil {
|
||||
log.FromContext(ctx).Error(err, "failed to convert label selector to selector")
|
||||
@@ -53,7 +53,7 @@ func matchResourceToSelector(ctx context.Context, name string, templateContext,
|
||||
return labelMatches && nameMatches
|
||||
}
|
||||
|
||||
func matchResourceToSelectorArray(ctx context.Context, name string, templateContext, l map[string]string, ss []*cozyv1alpha1.CozystackResourceDefinitionResourceSelector) bool {
|
||||
func matchResourceToSelectorArray(ctx context.Context, name string, templateContext, l map[string]string, ss []*cozyv1alpha1.ApplicationDefinitionResourceSelector) bool {
|
||||
for _, s := range ss {
|
||||
if matchResourceToSelector(ctx, name, templateContext, l, s) {
|
||||
return true
|
||||
@@ -62,7 +62,7 @@ func matchResourceToSelectorArray(ctx context.Context, name string, templateCont
|
||||
return false
|
||||
}
|
||||
|
||||
func matchResourceToExcludeInclude(ctx context.Context, name string, templateContext, l map[string]string, resources *cozyv1alpha1.CozystackResourceDefinitionResources) bool {
|
||||
func matchResourceToExcludeInclude(ctx context.Context, name string, templateContext, l map[string]string, resources *cozyv1alpha1.ApplicationDefinitionResources) bool {
|
||||
if resources == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cozystack/cozystack/pkg/lineage"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -33,8 +32,8 @@ const (
|
||||
ManagerNameKey = "apps.cozystack.io/application.name"
|
||||
)
|
||||
|
||||
// getResourceSelectors returns the appropriate CozystackResourceDefinitionResources for a given GroupKind
|
||||
func (h *LineageControllerWebhook) getResourceSelectors(gk schema.GroupKind, crd *cozyv1alpha1.CozystackResourceDefinition) *cozyv1alpha1.CozystackResourceDefinitionResources {
|
||||
// getResourceSelectors returns the appropriate ApplicationDefinitionResources for a given GroupKind
|
||||
func (h *LineageControllerWebhook) getResourceSelectors(gk schema.GroupKind, crd *cozyv1alpha1.ApplicationDefinition) *cozyv1alpha1.ApplicationDefinitionResources {
|
||||
switch {
|
||||
case gk.Group == "" && gk.Kind == "Secret":
|
||||
return &crd.Spec.Secrets
|
||||
@@ -88,13 +87,16 @@ func (h *LineageControllerWebhook) Handle(ctx context.Context, req admission.Req
|
||||
"name", req.Name,
|
||||
"operation", req.Operation,
|
||||
)
|
||||
logger.Info("webhook called", "gvk", req.Kind.String(), "namespace", req.Namespace, "name", req.Name, "operation", req.Operation)
|
||||
warn := make(admission.Warnings, 0)
|
||||
|
||||
obj := &unstructured.Unstructured{}
|
||||
if err := h.decodeUnstructured(req, obj); err != nil {
|
||||
logger.Error(err, "failed to decode object")
|
||||
return admission.Errored(400, fmt.Errorf("decode object: %w", err))
|
||||
}
|
||||
|
||||
logger.V(1).Info("decoded object", "labels", obj.GetLabels(), "ownerReferences", obj.GetOwnerReferences())
|
||||
labels, err := h.computeLabels(ctx, obj)
|
||||
for {
|
||||
if err != nil && errors.Is(err, NoAncestors) {
|
||||
@@ -117,9 +119,10 @@ func (h *LineageControllerWebhook) Handle(ctx context.Context, req admission.Req
|
||||
|
||||
mutated, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return admission.Errored(500, fmt.Errorf("marshal mutated pod: %w", err))
|
||||
logger.Error(err, "failed to marshal mutated object")
|
||||
return admission.Errored(500, fmt.Errorf("marshal mutated object: %w", err))
|
||||
}
|
||||
logger.V(1).Info("mutated pod", "namespace", obj.GetNamespace(), "name", obj.GetName())
|
||||
logger.Info("mutated object", "namespace", obj.GetNamespace(), "name", obj.GetName(), "labels", labels)
|
||||
return admission.PatchResponseFromRaw(req.Object.Raw, mutated).WithWarnings(warn...)
|
||||
}
|
||||
|
||||
@@ -156,21 +159,9 @@ func (h *LineageControllerWebhook) computeLabels(ctx context.Context, o *unstruc
|
||||
ManagerKindKey: obj.GetKind(),
|
||||
ManagerNameKey: obj.GetName(),
|
||||
}
|
||||
templateLabels := map[string]string{
|
||||
"kind": strings.ToLower(obj.GetKind()),
|
||||
"name": obj.GetName(),
|
||||
"namespace": o.GetNamespace(),
|
||||
}
|
||||
cfg := h.config.Load().(*runtimeConfig)
|
||||
crd := cfg.appCRDMap[appRef{gv.Group, obj.GetKind()}]
|
||||
resourceSelectors := h.getResourceSelectors(o.GroupVersionKind().GroupKind(), crd)
|
||||
|
||||
labels[corev1alpha1.TenantResourceLabelKey] = func(b bool) string {
|
||||
if b {
|
||||
return corev1alpha1.TenantResourceLabelValue
|
||||
}
|
||||
return "false"
|
||||
}(matchResourceToExcludeInclude(ctx, o.GetName(), templateLabels, o.GetLabels(), resourceSelectors))
|
||||
// Resource selectors are no longer needed since we don't use ApplicationDefinitions
|
||||
// Set tenant resource label to false by default (can be overridden by other logic if needed)
|
||||
labels[corev1alpha1.TenantResourceLabelKey] = "false"
|
||||
return labels, err
|
||||
}
|
||||
|
||||
|
||||
1235
internal/operator/bundle_reconciler.go
Normal file
1235
internal/operator/bundle_reconciler.go
Normal file
File diff suppressed because it is too large
Load Diff
541
internal/operator/platform_reconciler.go
Normal file
541
internal/operator/platform_reconciler.go
Normal file
@@ -0,0 +1,541 @@
|
||||
/*
|
||||
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 operator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
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/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
// PlatformReconciler reconciles Platform resources
|
||||
type PlatformReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=platforms,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=platforms/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=source.extensions.fluxcd.io,resources=artifactgenerators,verbs=get;list;watch;create;update;patch;delete
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop
|
||||
func (r *PlatformReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
platform := &cozyv1alpha1.Platform{}
|
||||
if err := r.Get(ctx, req.NamespacedName, platform); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Cleanup orphaned resources
|
||||
return r.cleanupOrphanedResources(ctx, req.NamespacedName)
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if platform.Spec.Interval == nil {
|
||||
platform.Spec.Interval = &metav1.Duration{Duration: 5 * 60 * 1000000000} // 5m
|
||||
}
|
||||
|
||||
// Reconcile ArtifactGenerator
|
||||
if err := r.reconcileArtifactGenerator(ctx, platform); err != nil {
|
||||
logger.Error(err, "failed to reconcile ArtifactGenerator")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Reconcile HelmRelease
|
||||
if err := r.reconcileHelmRelease(ctx, platform); err != nil {
|
||||
logger.Error(err, "failed to reconcile HelmRelease")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Cleanup orphaned resources with platform label
|
||||
if err := r.cleanupOrphanedPlatformResources(ctx, platform); err != nil {
|
||||
logger.Error(err, "failed to cleanup orphaned platform resources")
|
||||
// Don't return error, just log it - cleanup is best effort
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// reconcileArtifactGenerator creates or updates the ArtifactGenerator for the platform
|
||||
func (r *PlatformReconciler) reconcileArtifactGenerator(ctx context.Context, platform *cozyv1alpha1.Platform) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Use fixed namespace for cluster-scoped resource
|
||||
namespace := "cozy-system"
|
||||
|
||||
// Get basePath with default values (already includes full path to platform)
|
||||
basePath := r.getBasePath(platform)
|
||||
|
||||
// Build full path from basePath (basePath already contains the full path)
|
||||
fullPath := r.buildSourcePath(platform.Spec.SourceRef.Name, basePath, "")
|
||||
// Extract the last component for the artifact name
|
||||
artifactPathParts := strings.Split(strings.Trim(basePath, "/"), "/")
|
||||
artifactName := artifactPathParts[len(artifactPathParts)-1]
|
||||
|
||||
copyOps := []sourcewatcherv1beta1.CopyOperation{
|
||||
{
|
||||
From: fullPath + "/**",
|
||||
To: fmt.Sprintf("@artifact/%s/", artifactName),
|
||||
},
|
||||
}
|
||||
|
||||
// Create ArtifactGenerator
|
||||
ag := &sourcewatcherv1beta1.ArtifactGenerator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: platform.Name,
|
||||
Namespace: namespace,
|
||||
Labels: map[string]string{
|
||||
"cozystack.io/platform": platform.Name,
|
||||
},
|
||||
},
|
||||
Spec: sourcewatcherv1beta1.ArtifactGeneratorSpec{
|
||||
Sources: []sourcewatcherv1beta1.SourceReference{
|
||||
{
|
||||
Alias: platform.Spec.SourceRef.Name,
|
||||
Kind: platform.Spec.SourceRef.Kind,
|
||||
Name: platform.Spec.SourceRef.Name,
|
||||
Namespace: platform.Spec.SourceRef.Namespace,
|
||||
},
|
||||
},
|
||||
OutputArtifacts: []sourcewatcherv1beta1.OutputArtifact{
|
||||
{
|
||||
Name: artifactName,
|
||||
Copy: copyOps,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Set ownerReference
|
||||
ag.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: platform.APIVersion,
|
||||
Kind: platform.Kind,
|
||||
Name: platform.Name,
|
||||
UID: platform.UID,
|
||||
Controller: func() *bool { b := true; return &b }(),
|
||||
},
|
||||
}
|
||||
|
||||
logger.Info("reconciling ArtifactGenerator", "name", platform.Name, "namespace", namespace)
|
||||
|
||||
if err := r.createOrUpdate(ctx, ag); err != nil {
|
||||
return fmt.Errorf("failed to reconcile ArtifactGenerator %s: %w", platform.Name, err)
|
||||
}
|
||||
|
||||
logger.Info("reconciled ArtifactGenerator", "name", platform.Name, "namespace", namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconcileHelmRelease creates or updates the HelmRelease for the platform
|
||||
func (r *PlatformReconciler) reconcileHelmRelease(ctx context.Context, platform *cozyv1alpha1.Platform) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// HelmRelease name is fixed: cozystack-platform
|
||||
// Use fixed namespace for cluster-scoped resource
|
||||
namespace := "cozy-system"
|
||||
|
||||
// Get artifact name (last component of basePath)
|
||||
basePath := r.getBasePath(platform)
|
||||
artifactPathParts := strings.Split(strings.Trim(basePath, "/"), "/")
|
||||
artifactName := artifactPathParts[len(artifactPathParts)-1]
|
||||
|
||||
// Merge values with sourceRef
|
||||
values := r.mergeValuesWithSourceRef(platform.Spec.Values, platform.Spec.SourceRef)
|
||||
|
||||
// Create HelmRelease
|
||||
hr := &helmv2.HelmRelease{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: platform.Name,
|
||||
Namespace: namespace,
|
||||
Labels: map[string]string{
|
||||
"cozystack.io/platform": platform.Name,
|
||||
},
|
||||
},
|
||||
Spec: helmv2.HelmReleaseSpec{
|
||||
Interval: *platform.Spec.Interval,
|
||||
TargetNamespace: "cozy-system",
|
||||
ReleaseName: "cozystack-platform",
|
||||
ChartRef: &helmv2.CrossNamespaceSourceReference{
|
||||
Kind: "ExternalArtifact",
|
||||
Name: artifactName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Values: values,
|
||||
Install: &helmv2.Install{
|
||||
Remediation: &helmv2.InstallRemediation{
|
||||
Retries: -1,
|
||||
},
|
||||
},
|
||||
Upgrade: &helmv2.Upgrade{
|
||||
Remediation: &helmv2.UpgradeRemediation{
|
||||
Retries: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Set ownerReference
|
||||
hr.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: platform.APIVersion,
|
||||
Kind: platform.Kind,
|
||||
Name: platform.Name,
|
||||
UID: platform.UID,
|
||||
Controller: func() *bool { b := true; return &b }(),
|
||||
},
|
||||
}
|
||||
|
||||
logger.Info("reconciling HelmRelease", "name", platform.Name, "namespace", namespace)
|
||||
|
||||
if err := r.createOrUpdate(ctx, hr); err != nil {
|
||||
return fmt.Errorf("failed to reconcile HelmRelease %s: %w", platform.Name, err)
|
||||
}
|
||||
|
||||
logger.Info("reconciled HelmRelease", "name", platform.Name, "namespace", namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeValuesWithSourceRef merges platform values with sourceRef
|
||||
func (r *PlatformReconciler) mergeValuesWithSourceRef(values *apiextensionsv1.JSON, sourceRef cozyv1alpha1.SourceRef) *apiextensionsv1.JSON {
|
||||
// Build sourceRef map
|
||||
sourceRefMap := map[string]interface{}{
|
||||
"kind": sourceRef.Kind,
|
||||
"name": sourceRef.Name,
|
||||
"namespace": sourceRef.Namespace,
|
||||
}
|
||||
|
||||
// If values is nil or empty, create new values with sourceRef
|
||||
if values == nil || len(values.Raw) == 0 {
|
||||
valuesMap := map[string]interface{}{
|
||||
"sourceRef": sourceRefMap,
|
||||
}
|
||||
raw, _ := json.Marshal(valuesMap)
|
||||
return &apiextensionsv1.JSON{Raw: raw}
|
||||
}
|
||||
|
||||
// Parse existing values
|
||||
var valuesMap map[string]interface{}
|
||||
if err := json.Unmarshal(values.Raw, &valuesMap); err != nil {
|
||||
// If unmarshal fails, create new values with sourceRef
|
||||
valuesMap = map[string]interface{}{
|
||||
"sourceRef": sourceRefMap,
|
||||
}
|
||||
raw, _ := json.Marshal(valuesMap)
|
||||
return &apiextensionsv1.JSON{Raw: raw}
|
||||
}
|
||||
|
||||
// Merge sourceRef into values (overwrite if exists)
|
||||
valuesMap["sourceRef"] = sourceRefMap
|
||||
|
||||
// Marshal back to JSON
|
||||
raw, err := json.Marshal(valuesMap)
|
||||
if err != nil {
|
||||
// If marshal fails, return original values
|
||||
return values
|
||||
}
|
||||
|
||||
return &apiextensionsv1.JSON{Raw: raw}
|
||||
}
|
||||
|
||||
// getBasePath returns the basePath with default values based on source kind
|
||||
func (r *PlatformReconciler) getBasePath(platform *cozyv1alpha1.Platform) string {
|
||||
if platform.Spec.BasePath != "" {
|
||||
return platform.Spec.BasePath
|
||||
}
|
||||
// Default values based on kind
|
||||
if platform.Spec.SourceRef.Kind == "OCIRepository" {
|
||||
return "core/platform" // Full path for OCI
|
||||
}
|
||||
// Default for GitRepository
|
||||
return "packages/core/platform" // Full path for Git
|
||||
}
|
||||
|
||||
// buildSourcePath builds the full source path from basePath and chart path
|
||||
func (r *PlatformReconciler) buildSourcePath(sourceName, basePath, chartPath string) string {
|
||||
// Remove leading/trailing slashes and combine
|
||||
parts := []string{}
|
||||
if basePath != "" {
|
||||
parts = append(parts, strings.Trim(basePath, "/"))
|
||||
}
|
||||
if chartPath != "" {
|
||||
parts = append(parts, strings.Trim(chartPath, "/"))
|
||||
}
|
||||
fullPath := strings.Join(parts, "/")
|
||||
if fullPath == "" {
|
||||
return fmt.Sprintf("@%s", sourceName)
|
||||
}
|
||||
return fmt.Sprintf("@%s/%s", sourceName, fullPath)
|
||||
}
|
||||
|
||||
// cleanupOrphanedResources removes ArtifactGenerator and HelmRelease when Platform is deleted
|
||||
func (r *PlatformReconciler) cleanupOrphanedResources(ctx context.Context, name client.ObjectKey) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
namespace := "cozy-system"
|
||||
|
||||
// Cleanup HelmReleases with the platform label that don't match
|
||||
hrList := &helmv2.HelmReleaseList{}
|
||||
if err := r.List(ctx, hrList, client.InNamespace(namespace), client.MatchingLabels{
|
||||
"cozystack.io/platform": name.Name,
|
||||
}); err != nil {
|
||||
logger.Error(err, "failed to list HelmReleases for cleanup")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
for i := range hrList.Items {
|
||||
hr := &hrList.Items[i]
|
||||
// Check if this HelmRelease should exist (matches current Platform name)
|
||||
// Since Platform is being deleted, all matching HelmReleases should be deleted
|
||||
// OwnerReferences should handle this, but we'll also delete explicitly
|
||||
if err := r.Delete(ctx, hr); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "failed to delete orphaned HelmRelease", "name", hr.Name)
|
||||
}
|
||||
} else {
|
||||
logger.Info("deleted orphaned HelmRelease", "name", hr.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup ArtifactGenerators with the platform label
|
||||
agList := &sourcewatcherv1beta1.ArtifactGeneratorList{}
|
||||
if err := r.List(ctx, agList, client.InNamespace(namespace), client.MatchingLabels{
|
||||
"cozystack.io/platform": name.Name,
|
||||
}); err != nil {
|
||||
logger.Error(err, "failed to list ArtifactGenerators for cleanup")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
for i := range agList.Items {
|
||||
ag := &agList.Items[i]
|
||||
if err := r.Delete(ctx, ag); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "failed to delete orphaned ArtifactGenerator", "name", ag.Name)
|
||||
}
|
||||
} else {
|
||||
logger.Info("deleted orphaned ArtifactGenerator", "name", ag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// cleanupOrphanedPlatformResources removes HelmRelease and ArtifactGenerator resources
|
||||
// that have the platform label but don't match the current Platform
|
||||
func (r *PlatformReconciler) cleanupOrphanedPlatformResources(ctx context.Context, platform *cozyv1alpha1.Platform) error {
|
||||
logger := log.FromContext(ctx)
|
||||
namespace := "cozy-system"
|
||||
platformName := platform.Name
|
||||
|
||||
// Cleanup orphaned HelmReleases
|
||||
hrList := &helmv2.HelmReleaseList{}
|
||||
if err := r.List(ctx, hrList, client.InNamespace(namespace), client.MatchingLabels{
|
||||
"cozystack.io/platform": platformName,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to list HelmReleases: %w", err)
|
||||
}
|
||||
|
||||
for i := range hrList.Items {
|
||||
hr := &hrList.Items[i]
|
||||
// Only delete if it doesn't match the current Platform name
|
||||
// (in case Platform name changed)
|
||||
if hr.Name != platformName {
|
||||
logger.Info("deleting orphaned HelmRelease", "name", hr.Name, "expected", platformName)
|
||||
if err := r.Delete(ctx, hr); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "failed to delete orphaned HelmRelease", "name", hr.Name)
|
||||
// Continue with other resources
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup orphaned ArtifactGenerators
|
||||
agList := &sourcewatcherv1beta1.ArtifactGeneratorList{}
|
||||
if err := r.List(ctx, agList, client.InNamespace(namespace), client.MatchingLabels{
|
||||
"cozystack.io/platform": platformName,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to list ArtifactGenerators: %w", err)
|
||||
}
|
||||
|
||||
for i := range agList.Items {
|
||||
ag := &agList.Items[i]
|
||||
// Only delete if it doesn't match the current Platform name
|
||||
if ag.Name != platformName {
|
||||
logger.Info("deleting orphaned ArtifactGenerator", "name", ag.Name, "expected", platformName)
|
||||
if err := r.Delete(ctx, ag); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "failed to delete orphaned ArtifactGenerator", "name", ag.Name)
|
||||
// Continue with other resources
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createOrUpdate creates or updates a resource
|
||||
func (r *PlatformReconciler) 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 and ownerReferences
|
||||
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)
|
||||
existingAG.Spec = ag.Spec
|
||||
existingAG.SetLabels(ag.GetLabels())
|
||||
existingAG.SetAnnotations(ag.GetAnnotations())
|
||||
// Always use ownerReferences from the new object (set in reconcileArtifactGenerator)
|
||||
existingAG.SetOwnerReferences(ag.GetOwnerReferences())
|
||||
obj = existingAG
|
||||
}
|
||||
}
|
||||
|
||||
// For HelmRelease, explicitly update Spec and ownerReferences
|
||||
if hr, ok := obj.(*helmv2.HelmRelease); ok {
|
||||
if existingHR, ok := existing.(*helmv2.HelmRelease); ok {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.V(1).Info("updating HelmRelease Spec", "name", hr.Name, "namespace", hr.Namespace)
|
||||
existingHR.Spec = hr.Spec
|
||||
existingHR.SetLabels(hr.GetLabels())
|
||||
existingHR.SetAnnotations(hr.GetAnnotations())
|
||||
// Always use ownerReferences from the new object (set in reconcileHelmRelease)
|
||||
existingHR.SetOwnerReferences(hr.GetOwnerReferences())
|
||||
obj = existingHR
|
||||
}
|
||||
}
|
||||
|
||||
return r.Update(ctx, obj)
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager
|
||||
func (r *PlatformReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystack-platform").
|
||||
For(&cozyv1alpha1.Platform{}).
|
||||
Watches(
|
||||
&helmv2.HelmRelease{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
hr, ok := obj.(*helmv2.HelmRelease)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Only watch HelmReleases with cozystack.io/platform label
|
||||
platformName := hr.Labels["cozystack.io/platform"]
|
||||
if platformName == "" {
|
||||
return nil
|
||||
}
|
||||
return []reconcile.Request{
|
||||
{
|
||||
NamespacedName: client.ObjectKey{
|
||||
Name: platformName,
|
||||
// Cluster-scoped resource has no namespace
|
||||
},
|
||||
},
|
||||
}
|
||||
}),
|
||||
builder.WithPredicates(
|
||||
predicate.NewPredicateFuncs(func(obj client.Object) bool {
|
||||
// Only watch resources with cozystack.io/platform label
|
||||
labels := obj.GetLabels()
|
||||
return labels != nil && labels["cozystack.io/platform"] != ""
|
||||
}),
|
||||
),
|
||||
).
|
||||
Watches(
|
||||
&sourcewatcherv1beta1.ArtifactGenerator{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
ag, ok := obj.(*sourcewatcherv1beta1.ArtifactGenerator)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Only watch ArtifactGenerators with cozystack.io/platform label
|
||||
platformName := ag.Labels["cozystack.io/platform"]
|
||||
if platformName == "" {
|
||||
return nil
|
||||
}
|
||||
return []reconcile.Request{
|
||||
{
|
||||
NamespacedName: client.ObjectKey{
|
||||
Name: platformName,
|
||||
// Cluster-scoped resource has no namespace
|
||||
},
|
||||
},
|
||||
}
|
||||
}),
|
||||
builder.WithPredicates(
|
||||
predicate.NewPredicateFuncs(func(obj client.Object) bool {
|
||||
// Only watch resources with cozystack.io/platform label
|
||||
labels := obj.GetLabels()
|
||||
return labels != nil && labels["cozystack.io/platform"] != ""
|
||||
}),
|
||||
),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -11,13 +11,13 @@ import (
|
||||
|
||||
type Memory struct {
|
||||
mu sync.RWMutex
|
||||
data map[string]cozyv1alpha1.CozystackResourceDefinition
|
||||
data map[string]cozyv1alpha1.ApplicationDefinition
|
||||
primed bool
|
||||
primeOnce sync.Once
|
||||
}
|
||||
|
||||
func New() *Memory {
|
||||
return &Memory{data: make(map[string]cozyv1alpha1.CozystackResourceDefinition)}
|
||||
return &Memory{data: make(map[string]cozyv1alpha1.ApplicationDefinition)}
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -30,7 +30,7 @@ func Global() *Memory {
|
||||
return global
|
||||
}
|
||||
|
||||
func (m *Memory) Upsert(obj *cozyv1alpha1.CozystackResourceDefinition) {
|
||||
func (m *Memory) Upsert(obj *cozyv1alpha1.ApplicationDefinition) {
|
||||
if obj == nil {
|
||||
return
|
||||
}
|
||||
@@ -45,10 +45,10 @@ func (m *Memory) Delete(name string) {
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func (m *Memory) Snapshot() []cozyv1alpha1.CozystackResourceDefinition {
|
||||
func (m *Memory) Snapshot() []cozyv1alpha1.ApplicationDefinition {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
out := make([]cozyv1alpha1.CozystackResourceDefinition, 0, len(m.data))
|
||||
out := make([]cozyv1alpha1.ApplicationDefinition, 0, len(m.data))
|
||||
for _, v := range m.data {
|
||||
out = append(out, v)
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func (m *Memory) EnsurePrimingWithManager(mgr ctrl.Manager) error {
|
||||
if ok := mgr.GetCache().WaitForCacheSync(ctx); !ok {
|
||||
return nil
|
||||
}
|
||||
var list cozyv1alpha1.CozystackResourceDefinitionList
|
||||
var list cozyv1alpha1.ApplicationDefinitionList
|
||||
if err := mgr.GetClient().List(ctx, &list); err == nil {
|
||||
for i := range list.Items {
|
||||
m.Upsert(&list.Items[i])
|
||||
@@ -87,11 +87,11 @@ func (m *Memory) EnsurePrimingWithManager(mgr ctrl.Manager) error {
|
||||
return errOut
|
||||
}
|
||||
|
||||
func (m *Memory) ListFromCacheOrAPI(ctx context.Context, c client.Client) ([]cozyv1alpha1.CozystackResourceDefinition, error) {
|
||||
func (m *Memory) ListFromCacheOrAPI(ctx context.Context, c client.Client) ([]cozyv1alpha1.ApplicationDefinition, error) {
|
||||
if m.IsPrimed() {
|
||||
return m.Snapshot(), nil
|
||||
}
|
||||
var list cozyv1alpha1.CozystackResourceDefinitionList
|
||||
var list cozyv1alpha1.ApplicationDefinitionList
|
||||
if err := c.List(ctx, &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -120,16 +121,50 @@ func (c *Collector) collect(ctx context.Context) {
|
||||
|
||||
clusterID := string(kubeSystemNS.UID)
|
||||
|
||||
var cozystackCM corev1.ConfigMap
|
||||
if err := c.client.Get(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack"}, &cozystackCM); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to get cozystack configmap in cozy-system namespace: %v", err))
|
||||
return
|
||||
}
|
||||
// Get all Bundles
|
||||
var bundleList cozyv1alpha1.BundleList
|
||||
bundleNameStr := ""
|
||||
bundleEnable := ""
|
||||
bundleDisable := ""
|
||||
oidcEnabled := "false"
|
||||
|
||||
oidcEnabled := cozystackCM.Data["oidc-enabled"]
|
||||
bundle := cozystackCM.Data["bundle-name"]
|
||||
bundleEnable := cozystackCM.Data["bundle-enable"]
|
||||
bundleDisable := cozystackCM.Data["bundle-disable"]
|
||||
if err := c.client.List(ctx, &bundleList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list Bundles: %v", err))
|
||||
// Continue with empty bundle data instead of returning
|
||||
} else {
|
||||
// Collect bundle names (sorted alphabetically)
|
||||
bundleNames := make([]string, 0, len(bundleList.Items))
|
||||
for _, bundle := range bundleList.Items {
|
||||
bundleNames = append(bundleNames, bundle.Name)
|
||||
}
|
||||
sort.Strings(bundleNames)
|
||||
bundleNameStr = strings.Join(bundleNames, ",")
|
||||
|
||||
// Collect all packages from all bundles
|
||||
var allEnabledPackages []string
|
||||
var allDisabledPackages []string
|
||||
|
||||
for _, bundle := range bundleList.Items {
|
||||
for _, pkg := range bundle.Spec.Packages {
|
||||
if pkg.Disabled {
|
||||
allDisabledPackages = append(allDisabledPackages, pkg.Name)
|
||||
} else {
|
||||
allEnabledPackages = append(allEnabledPackages, pkg.Name)
|
||||
// Check if keycloak package is enabled
|
||||
if pkg.Name == "keycloak" {
|
||||
oidcEnabled = "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort package lists alphabetically
|
||||
sort.Strings(allEnabledPackages)
|
||||
sort.Strings(allDisabledPackages)
|
||||
|
||||
bundleEnable = strings.Join(allEnabledPackages, ",")
|
||||
bundleDisable = strings.Join(allDisabledPackages, ",")
|
||||
}
|
||||
|
||||
// Get Kubernetes version from nodes
|
||||
var nodeList corev1.NodeList
|
||||
@@ -143,32 +178,41 @@ func (c *Collector) collect(ctx context.Context) {
|
||||
|
||||
// Add Cozystack info metric
|
||||
if len(nodeList.Items) > 0 {
|
||||
k8sVersion, _ := c.discoveryClient.ServerVersion()
|
||||
k8sVersion := "unknown"
|
||||
if version, err := c.discoveryClient.ServerVersion(); err == nil && version != nil {
|
||||
k8sVersion = version.String()
|
||||
}
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_cluster_info{cozystack_version=\"%s\",kubernetes_version=\"%s\",oidc_enabled=\"%s\",bundle_name=\"%s\",bunde_enable=\"%s\",bunde_disable=\"%s\"} 1\n",
|
||||
"cozy_cluster_info{cozystack_version=\"%s\",kubernetes_version=\"%s\",oidc_enabled=\"%s\",bundle_name=\"%s\",bundle_enable=\"%s\",bundle_disable=\"%s\"} 1\n",
|
||||
c.config.CozystackVersion,
|
||||
k8sVersion,
|
||||
oidcEnabled,
|
||||
bundle,
|
||||
bundleNameStr,
|
||||
bundleEnable,
|
||||
bundleDisable,
|
||||
))
|
||||
}
|
||||
|
||||
// Collect node metrics
|
||||
if len(nodeList.Items) > 0 {
|
||||
nodeOSCount := make(map[string]int)
|
||||
kernelVersion := "unknown"
|
||||
for _, node := range nodeList.Items {
|
||||
key := fmt.Sprintf("%s (%s)", node.Status.NodeInfo.OperatingSystem, node.Status.NodeInfo.OSImage)
|
||||
nodeOSCount[key] = nodeOSCount[key] + 1
|
||||
if kernelVersion == "unknown" && node.Status.NodeInfo.KernelVersion != "" {
|
||||
kernelVersion = node.Status.NodeInfo.KernelVersion
|
||||
}
|
||||
}
|
||||
|
||||
for osKey, count := range nodeOSCount {
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_nodes_count{os=\"%s\",kernel=\"%s\"} %d\n",
|
||||
osKey,
|
||||
nodeList.Items[0].Status.NodeInfo.KernelVersion,
|
||||
kernelVersion,
|
||||
count,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Collect LoadBalancer services metrics
|
||||
@@ -248,9 +292,8 @@ func (c *Collector) collect(ctx context.Context) {
|
||||
var monitorList cozyv1alpha1.WorkloadMonitorList
|
||||
if err := c.client.List(ctx, &monitorList); err != nil {
|
||||
logger.Info(fmt.Sprintf("Failed to list WorkloadMonitors: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Continue without workload metrics instead of returning
|
||||
} else {
|
||||
for _, monitor := range monitorList.Items {
|
||||
metrics.WriteString(fmt.Sprintf(
|
||||
"cozy_workloads_count{uid=\"%s\",kind=\"%s\",type=\"%s\",version=\"%s\"} %d\n",
|
||||
@@ -260,6 +303,7 @@ func (c *Collector) collect(ctx context.Context) {
|
||||
monitor.Spec.Version,
|
||||
monitor.Status.ObservedReplicas,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Send metrics
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
OUT=../../_out/repos/apps
|
||||
CHARTS := $(shell find . -maxdepth 2 -name Chart.yaml | awk -F/ '{print $$2}')
|
||||
|
||||
include ../../scripts/common-envs.mk
|
||||
|
||||
repo:
|
||||
rm -rf "$(OUT)"
|
||||
helm package -d "$(OUT)" $(CHARTS) --version $(COZYSTACK_VERSION)
|
||||
helm repo index "$(OUT)"
|
||||
include ../../hack/common-envs.mk
|
||||
|
||||
fix-charts:
|
||||
find . -maxdepth 2 -name Chart.yaml | awk -F/ '{print $$2}' | while read i; do sed -i -e "s/^name: .*/name: $$i/" -e "s/^version: .*/version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process/g" "$$i/Chart.yaml"; done
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
### How to test packages local
|
||||
|
||||
```bash
|
||||
cd packages/core/installer
|
||||
make image-cozystack REGISTRY=YOUR_CUSTOM_REGISTRY
|
||||
make apply
|
||||
kubectl delete po -l app=source-controller -n cozy-fluxcd
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
include ../../../scripts/package.mk
|
||||
include ../../../hack/package.mk
|
||||
|
||||
generate:
|
||||
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{{- $seaweedfs := .Values._namespace.seaweedfs }}
|
||||
{{- $cozystack := .Values._cozystack | default dict }}
|
||||
{{- $namespace := .Values._namespace | default dict }}
|
||||
{{- $seaweedfs := dig "seaweedfs" "" $namespace }}
|
||||
apiVersion: objectstorage.k8s.io/v1alpha1
|
||||
kind: BucketClaim
|
||||
metadata:
|
||||
|
||||
@@ -3,15 +3,10 @@ kind: HelmRelease
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-system
|
||||
spec:
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-bucket
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-iaas-bucket
|
||||
namespace: cozy-system
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
@@ -21,8 +16,5 @@ spec:
|
||||
force: true
|
||||
remediation:
|
||||
retries: -1
|
||||
valuesFrom:
|
||||
- kind: Secret
|
||||
name: cozystack-values
|
||||
values:
|
||||
bucketName: {{ .Release.Name }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
CLICKHOUSE_BACKUP_TAG = $(shell awk '$$0 ~ /^version:/ {print $$2}' Chart.yaml)
|
||||
|
||||
include ../../../scripts/common-envs.mk
|
||||
include ../../../scripts/package.mk
|
||||
include ../../../hack/common-envs.mk
|
||||
include ../../../hack/package.mk
|
||||
|
||||
generate:
|
||||
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
|
||||
{{- $cozystack := .Values._cozystack | default dict }}
|
||||
{{- $clusterDomain := dig "networking" "clusterDomain" "cozy.local" $cozystack }}
|
||||
|
||||
{{- if .Values.clickhouseKeeper.enabled }}
|
||||
apiVersion: "clickhouse-keeper.altinity.com/v1"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
|
||||
{{- $cozystack := .Values._cozystack | default dict }}
|
||||
{{- $clusterDomain := dig "networking" "clusterDomain" "cozy.local" $cozystack }}
|
||||
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }}
|
||||
{{- $passwords := dict }}
|
||||
{{- $users := .Values.users }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
include ../../../scripts/package.mk
|
||||
include ../../../hack/package.mk
|
||||
|
||||
generate:
|
||||
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
|
||||
|
||||
@@ -50,14 +50,13 @@ spec:
|
||||
postgresUID: 999
|
||||
postgresGID: 999
|
||||
enableSuperuserAccess: true
|
||||
{{- if .Values._cluster.scheduling }}
|
||||
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
|
||||
{{- if $rawConstraints }}
|
||||
{{- $rawConstraints | fromYaml | toYaml | nindent 2 }}
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
cnpg.io/cluster: {{ .Release.Name }}-postgres
|
||||
{{- end }}
|
||||
{{- $cozystack := .Values._cozystack | default dict }}
|
||||
{{- $topologySpreadConstraints := dig "scheduling" "topologySpreadConstraints" (list) $cozystack }}
|
||||
{{- if $topologySpreadConstraints }}
|
||||
topologySpreadConstraints:
|
||||
{{- range $topologySpreadConstraints }}
|
||||
- {{- mergeOverwrite (dict "labelSelector" (dict "matchLabels" (dict "cnpg.io/cluster" (printf "%s-postgres" $.Release.Name)))) . | toYaml | nindent 6 | trim }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
minSyncReplicas: {{ .Values.quorum.minSyncReplicas }}
|
||||
maxSyncReplicas: {{ .Values.quorum.maxSyncReplicas }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
include ../../../scripts/package.mk
|
||||
include ../../../hack/package.mk
|
||||
|
||||
generate:
|
||||
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- $clusterDomain := index .Values._cluster "cluster-domain" | default "cozy.local" }}
|
||||
{{- $cozystack := .Values._cozystack | default dict }}
|
||||
{{- $clusterDomain := dig "networking" "clusterDomain" "cozy.local" $cozystack }}
|
||||
---
|
||||
apiVersion: apps.foundationdb.org/v1beta2
|
||||
kind: FoundationDBCluster
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
NGINX_CACHE_TAG = $(shell awk '$$1 == "version:" {print $$2}' Chart.yaml)
|
||||
|
||||
include ../../../scripts/common-envs.mk
|
||||
include ../../../scripts/package.mk
|
||||
include ../../../hack/common-envs.mk
|
||||
include ../../../hack/package.mk
|
||||
|
||||
image: image-nginx
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:9e34fd50393b418d9516aadb488067a3a63675b045811beb1c0afc9c61e149e8
|
||||
ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:e0a07082bb6fc6aeaae2315f335386f1705a646c72f9e0af512aebbca5cb2b15
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
include ../../../scripts/package.mk
|
||||
include ../../../hack/package.mk
|
||||
PRESET_ENUM := ["nano","micro","small","medium","large","xlarge","2xlarge"]
|
||||
|
||||
generate:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
.helmignore
|
||||
/logos
|
||||
/Makefile
|
||||
/hack
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
KUBERNETES_VERSION = v1.33
|
||||
KUBERNETES_PKG_TAG = $(shell awk '$$1 == "version:" {print $$2}' Chart.yaml)
|
||||
|
||||
include ../../../scripts/common-envs.mk
|
||||
include ../../../scripts/package.mk
|
||||
include ../../../hack/common-envs.mk
|
||||
include ../../../hack/package.mk
|
||||
|
||||
generate:
|
||||
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
|
||||
yq -o=json -i '.properties.version.enum = (load("files/versions.yaml") | keys)' values.schema.json
|
||||
../../../hack/update-crd.sh
|
||||
|
||||
update:
|
||||
hack/update-versions.sh
|
||||
make generate
|
||||
|
||||
image: image-ubuntu-container-disk image-kubevirt-cloud-provider image-kubevirt-csi-driver image-cluster-autoscaler
|
||||
|
||||
image-ubuntu-container-disk:
|
||||
|
||||
@@ -104,7 +104,7 @@ See the reference for components utilized in this service:
|
||||
| `nodeGroups[name].resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `nodeGroups[name].gpus` | List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). | `[]object` | `[]` |
|
||||
| `nodeGroups[name].gpus[i].name` | Name of GPU, such as "nvidia.com/AD102GL_L40S". | `string` | `""` |
|
||||
| `version` | Kubernetes major.minor version to deploy | `string` | `v1.33` |
|
||||
| `version` | Kubernetes version (vMAJOR.MINOR). Supported: 1.28–1.33. | `string` | `v1.33` |
|
||||
| `host` | External hostname for Kubernetes cluster. Defaults to `<cluster-name>.<tenant-host>` if empty. | `string` | `""` |
|
||||
|
||||
|
||||
@@ -145,31 +145,31 @@ See the reference for components utilized in this service:
|
||||
|
||||
### Kubernetes Control Plane Configuration
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| --------------------------------------------------- | ------------------------------------------------ | ---------- | ------- |
|
||||
| `controlPlane` | Kubernetes control-plane configuration. | `object` | `{}` |
|
||||
| `controlPlane.replicas` | Number of control-plane replicas. | `int` | `2` |
|
||||
| `controlPlane.apiServer` | API Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources` | CPU and memory resources for API Server. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resourcesPreset` | Preset if `resources` omitted. | `string` | `large` |
|
||||
| `controlPlane.controllerManager` | Controller Manager configuration. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources` | CPU and memory resources for Controller Manager. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.scheduler` | Scheduler configuration. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources` | CPU and memory resources for Scheduler. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.konnectivity` | Konnectivity configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server` | Konnectivity Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources` | CPU and memory resources for Konnectivity. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| Name | Description | Type | Value |
|
||||
| --------------------------------------------------- | ------------------------------------------------ | ---------- | -------- |
|
||||
| `controlPlane` | Kubernetes control-plane configuration. | `object` | `{}` |
|
||||
| `controlPlane.replicas` | Number of control-plane replicas. | `int` | `2` |
|
||||
| `controlPlane.apiServer` | API Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources` | CPU and memory resources for API Server. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resourcesPreset` | Preset if `resources` omitted. | `string` | `medium` |
|
||||
| `controlPlane.controllerManager` | Controller Manager configuration. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources` | CPU and memory resources for Controller Manager. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.scheduler` | Scheduler configuration. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources` | CPU and memory resources for Scheduler. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.konnectivity` | Konnectivity configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server` | Konnectivity Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources` | CPU and memory resources for Konnectivity. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
|
||||
|
||||
## Parameter examples and reference
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"v1.33": "v1.33.0"
|
||||
"v1.32": "v1.32.10"
|
||||
"v1.31": "v1.31.14"
|
||||
"v1.30": "v1.30.14"
|
||||
"v1.29": "v1.29.15"
|
||||
"v1.28": "v1.28.15"
|
||||
"v1.29": "v1.29.15"
|
||||
"v1.30": "v1.30.14"
|
||||
"v1.31": "v1.31.10"
|
||||
"v1.32": "v1.32.6"
|
||||
"v1.33": "v1.33.0"
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -o errexit
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
KUBERNETES_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
VALUES_FILE="${KUBERNETES_DIR}/values.yaml"
|
||||
VERSIONS_FILE="${KUBERNETES_DIR}/files/versions.yaml"
|
||||
MAKEFILE="${KUBERNETES_DIR}/Makefile"
|
||||
KAMAJI_DOCKERFILE="${KUBERNETES_DIR}/../../system/kamaji/images/kamaji/Dockerfile"
|
||||
|
||||
# Check if skopeo is installed
|
||||
if ! command -v skopeo &> /dev/null; then
|
||||
echo "Error: skopeo is not installed. Please install skopeo and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if jq is installed
|
||||
if ! command -v jq &> /dev/null; then
|
||||
echo "Error: jq is not installed. Please install jq and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get kamaji version from Dockerfile
|
||||
echo "Reading kamaji version from Dockerfile..."
|
||||
if [ ! -f "$KAMAJI_DOCKERFILE" ]; then
|
||||
echo "Error: Kamaji Dockerfile not found at $KAMAJI_DOCKERFILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
KAMAJI_VERSION=$(grep "^ARG VERSION=" "$KAMAJI_DOCKERFILE" | cut -d= -f2 | tr -d '"')
|
||||
if [ -z "$KAMAJI_VERSION" ]; then
|
||||
echo "Error: Could not extract kamaji version from Dockerfile" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Kamaji version: $KAMAJI_VERSION"
|
||||
|
||||
# Get Kubernetes version from kamaji repository
|
||||
echo "Fetching Kubernetes version from kamaji repository..."
|
||||
KUBERNETES_VERSION_FROM_KAMAJI=$(curl -sSL "https://raw.githubusercontent.com/clastix/kamaji/${KAMAJI_VERSION}/internal/upgrade/kubeadm_version.go" | grep "KubeadmVersion" | sed -E 's/.*KubeadmVersion = "([^"]+)".*/\1/')
|
||||
|
||||
if [ -z "$KUBERNETES_VERSION_FROM_KAMAJI" ]; then
|
||||
echo "Error: Could not fetch Kubernetes version from kamaji repository" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Kubernetes version from kamaji: $KUBERNETES_VERSION_FROM_KAMAJI"
|
||||
|
||||
# Extract major.minor version (e.g., "1.33" from "v1.33.0")
|
||||
KUBERNETES_MAJOR_MINOR=$(echo "$KUBERNETES_VERSION_FROM_KAMAJI" | sed -E 's/v([0-9]+)\.([0-9]+)\.[0-9]+/\1.\2/')
|
||||
KUBERNETES_MAJOR=$(echo "$KUBERNETES_MAJOR_MINOR" | cut -d. -f1)
|
||||
KUBERNETES_MINOR=$(echo "$KUBERNETES_MAJOR_MINOR" | cut -d. -f2)
|
||||
|
||||
echo "Kubernetes major.minor: $KUBERNETES_MAJOR_MINOR"
|
||||
|
||||
# Get available image tags
|
||||
echo "Fetching available image tags from registry..."
|
||||
AVAILABLE_TAGS=$(skopeo list-tags docker://registry.k8s.io/kube-apiserver | jq -r '.Tags[] | select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+$"))' | sort -V)
|
||||
|
||||
if [ -z "$AVAILABLE_TAGS" ]; then
|
||||
echo "Error: Could not fetch available image tags" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Filter out versions higher than KUBERNETES_VERSION_FROM_KAMAJI
|
||||
echo "Filtering versions above ${KUBERNETES_VERSION_FROM_KAMAJI}..."
|
||||
FILTERED_TAGS=$(echo "$AVAILABLE_TAGS" | while read tag; do
|
||||
if [ -n "$tag" ]; then
|
||||
# Compare tag with KUBERNETES_VERSION_FROM_KAMAJI using version sort
|
||||
# Include tag if it's less than or equal to KUBERNETES_VERSION_FROM_KAMAJI
|
||||
if [ "$(printf '%s\n%s\n' "$tag" "$KUBERNETES_VERSION_FROM_KAMAJI" | sort -V | head -1)" = "$tag" ] || [ "$tag" = "$KUBERNETES_VERSION_FROM_KAMAJI" ]; then
|
||||
echo "$tag"
|
||||
fi
|
||||
fi
|
||||
done)
|
||||
|
||||
if [ -z "$FILTERED_TAGS" ]; then
|
||||
echo "Error: No versions found after filtering" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
AVAILABLE_TAGS="$FILTERED_TAGS"
|
||||
echo "Filtered to $(echo "$AVAILABLE_TAGS" | wc -l | tr -d ' ') versions"
|
||||
|
||||
# Find the latest patch version for the supported major.minor version
|
||||
echo "Finding latest patch version for ${KUBERNETES_MAJOR_MINOR}..."
|
||||
SUPPORTED_PATCH_TAGS=$(echo "$AVAILABLE_TAGS" | grep "^v${KUBERNETES_MAJOR}\\.${KUBERNETES_MINOR}\\.")
|
||||
if [ -z "$SUPPORTED_PATCH_TAGS" ]; then
|
||||
echo "Error: Could not find any patch versions for ${KUBERNETES_MAJOR_MINOR}" >&2
|
||||
exit 1
|
||||
fi
|
||||
KUBERNETES_VERSION=$(echo "$SUPPORTED_PATCH_TAGS" | tail -n1)
|
||||
echo "Using latest patch version: $KUBERNETES_VERSION"
|
||||
|
||||
# Build versions map: major.minor -> latest patch version
|
||||
# First, collect all unique major.minor versions from available tags
|
||||
echo "Collecting all available major.minor versions..."
|
||||
ALL_MAJOR_MINOR_VERSIONS=$(echo "$AVAILABLE_TAGS" | sed -E 's/v([0-9]+)\.([0-9]+)\.[0-9]+/v\1.\2/' | sort -V -u)
|
||||
|
||||
# Find the position of the supported version in the sorted list
|
||||
SUPPORTED_MAJOR_MINOR="v${KUBERNETES_MAJOR}.${KUBERNETES_MINOR}"
|
||||
echo "Looking for supported version: $SUPPORTED_MAJOR_MINOR"
|
||||
|
||||
# Get all versions that are <= supported version
|
||||
# Create a temporary file for filtering
|
||||
TEMP_VERSIONS=$(mktemp)
|
||||
|
||||
echo "$ALL_MAJOR_MINOR_VERSIONS" | while read version; do
|
||||
# Compare versions using sort -V (version sort)
|
||||
# If version <= supported, include it
|
||||
if [ "$(printf '%s\n%s\n' "$version" "$SUPPORTED_MAJOR_MINOR" | sort -V | head -1)" = "$version" ] || [ "$version" = "$SUPPORTED_MAJOR_MINOR" ]; then
|
||||
echo "$version"
|
||||
fi
|
||||
done > "$TEMP_VERSIONS"
|
||||
|
||||
# Get the supported version and 5 previous versions (total 6 versions)
|
||||
# First, find the position of supported version
|
||||
SUPPORTED_POS=$(grep -n "^${SUPPORTED_MAJOR_MINOR}$" "$TEMP_VERSIONS" | cut -d: -f1)
|
||||
|
||||
if [ -z "$SUPPORTED_POS" ]; then
|
||||
echo "Error: Supported version $SUPPORTED_MAJOR_MINOR not found in available versions" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Calculate start position (5 versions before supported, or from beginning if less than 5 available)
|
||||
TOTAL_LINES=$(wc -l < "$TEMP_VERSIONS" | tr -d ' ')
|
||||
START_POS=$((SUPPORTED_POS - 5))
|
||||
if [ $START_POS -lt 1 ]; then
|
||||
START_POS=1
|
||||
fi
|
||||
|
||||
# Extract versions from START_POS to SUPPORTED_POS (inclusive)
|
||||
CANDIDATE_VERSIONS=$(sed -n "${START_POS},${SUPPORTED_POS}p" "$TEMP_VERSIONS")
|
||||
|
||||
if [ -z "$CANDIDATE_VERSIONS" ]; then
|
||||
echo "Error: Could not find supported version $SUPPORTED_MAJOR_MINOR in available versions" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
declare -A VERSION_MAP
|
||||
VERSIONS=()
|
||||
|
||||
# Process each candidate version
|
||||
for major_minor_key in $CANDIDATE_VERSIONS; do
|
||||
# Extract major and minor for matching
|
||||
major=$(echo "$major_minor_key" | sed -E 's/v([0-9]+)\.([0-9]+)/\1/')
|
||||
minor=$(echo "$major_minor_key" | sed -E 's/v([0-9]+)\.([0-9]+)/\2/')
|
||||
|
||||
# Find all tags that match this major.minor version
|
||||
matching_tags=$(echo "$AVAILABLE_TAGS" | grep "^v${major}\\.${minor}\\.")
|
||||
|
||||
if [ -n "$matching_tags" ]; then
|
||||
# Get the latest patch version for this major.minor version
|
||||
latest_tag=$(echo "$matching_tags" | tail -n1)
|
||||
|
||||
VERSION_MAP["${major_minor_key}"]="${latest_tag}"
|
||||
VERSIONS+=("${major_minor_key}")
|
||||
echo "Found version: ${major_minor_key} -> ${latest_tag}"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#VERSIONS[@]} -eq 0 ]; then
|
||||
echo "Error: No matching versions found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Sort versions in descending order (newest first)
|
||||
IFS=$'\n' VERSIONS=($(printf '%s\n' "${VERSIONS[@]}" | sort -V -r))
|
||||
unset IFS
|
||||
|
||||
echo "Versions to add: ${VERSIONS[*]}"
|
||||
|
||||
# Create/update versions.yaml file
|
||||
echo "Updating $VERSIONS_FILE..."
|
||||
{
|
||||
for ver in "${VERSIONS[@]}"; do
|
||||
echo "\"${ver}\": \"${VERSION_MAP[$ver]}\""
|
||||
done
|
||||
} > "$VERSIONS_FILE"
|
||||
|
||||
echo "Successfully updated $VERSIONS_FILE"
|
||||
|
||||
# Update values.yaml - enum with major.minor versions only
|
||||
TEMP_FILE=$(mktemp)
|
||||
trap "rm -f $TEMP_FILE $TEMP_VERSIONS" EXIT
|
||||
|
||||
# Build new version section
|
||||
NEW_VERSION_SECTION="## @enum {string} Version"
|
||||
for ver in "${VERSIONS[@]}"; do
|
||||
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
|
||||
## @value $ver"
|
||||
done
|
||||
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
|
||||
|
||||
## @param {Version} version - Kubernetes major.minor version to deploy
|
||||
version: \"${VERSIONS[0]}\""
|
||||
|
||||
# Check if version section already exists
|
||||
if grep -q "^## @enum {string} Version" "$VALUES_FILE"; then
|
||||
# Version section exists, update it using awk
|
||||
echo "Updating existing version section in $VALUES_FILE..."
|
||||
|
||||
# Use awk to replace the section from "## @enum {string} Version" to "version: " (inclusive)
|
||||
# Delete the old section and insert the new one
|
||||
awk -v new_section="$NEW_VERSION_SECTION" '
|
||||
/^## @enum {string} Version/ {
|
||||
in_section = 1
|
||||
print new_section
|
||||
next
|
||||
}
|
||||
in_section && /^version: / {
|
||||
in_section = 0
|
||||
next
|
||||
}
|
||||
in_section {
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
|
||||
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
|
||||
else
|
||||
# Version section doesn't exist, insert it before Application-specific parameters section
|
||||
echo "Inserting new version section in $VALUES_FILE..."
|
||||
|
||||
# Use awk to insert before "## @section Application-specific parameters"
|
||||
awk -v new_section="$NEW_VERSION_SECTION" '
|
||||
/^## @section Application-specific parameters/ {
|
||||
print new_section
|
||||
print ""
|
||||
}
|
||||
{ print }
|
||||
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
|
||||
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
|
||||
fi
|
||||
|
||||
echo "Successfully updated $VALUES_FILE with versions: ${VERSIONS[*]}"
|
||||
|
||||
# Update KUBERNETES_VERSION in Makefile
|
||||
# Extract major.minor from KUBERNETES_VERSION (e.g., "v1.33" from "v1.33.4")
|
||||
KUBERNETES_MAJOR_MINOR_FOR_MAKEFILE=$(echo "$KUBERNETES_VERSION" | sed -E 's/v([0-9]+)\.([0-9]+)\.[0-9]+/v\1.\2/')
|
||||
|
||||
if grep -q "^KUBERNETES_VERSION" "$MAKEFILE"; then
|
||||
# Update existing KUBERNETES_VERSION line using awk
|
||||
echo "Updating KUBERNETES_VERSION in $MAKEFILE..."
|
||||
awk -v new_version="${KUBERNETES_MAJOR_MINOR_FOR_MAKEFILE}" '
|
||||
/^KUBERNETES_VERSION = / {
|
||||
print "KUBERNETES_VERSION = " new_version
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$MAKEFILE" > "$TEMP_FILE.tmp"
|
||||
mv "$TEMP_FILE.tmp" "$MAKEFILE"
|
||||
echo "Successfully updated KUBERNETES_VERSION in $MAKEFILE to ${KUBERNETES_MAJOR_MINOR_FOR_MAKEFILE}"
|
||||
else
|
||||
echo "Warning: KUBERNETES_VERSION not found in $MAKEFILE" >&2
|
||||
fi
|
||||
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:6f2b1d6b0b2bdc66f1cbb30c59393369cbf070cb8f5fec748f176952273483cc
|
||||
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.0.0@sha256:2d39989846c3579dd020b9f6c77e6e314cc81aa344eaac0f6d633e723c17196d
|
||||
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:0.0.0@sha256:dee69d15fa8616aa6a1e5a67fc76370e7698a7f58b25e30650eb39c9fb826de8
|
||||
ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:0.0.0@sha256:5335c044313b69ee13b30ca4941687e509005e55f4ae25723861edbf2fbd6dd2
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/pkg/controller/kubevirteps/kubevirteps_controller.go b/pkg/controller/kubevirteps/kubevirteps_controller.go
|
||||
index 53388eb8e..873060251 100644
|
||||
index 53388eb8e..28644236f 100644
|
||||
--- a/pkg/controller/kubevirteps/kubevirteps_controller.go
|
||||
+++ b/pkg/controller/kubevirteps/kubevirteps_controller.go
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
@@ -10,17 +10,12 @@ index 53388eb8e..873060251 100644
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
@@ -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
|
||||
@@ -669,35 +668,50 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
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)
|
||||
@@ -28,13 +23,6 @@ index 53388eb8e..873060251 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) {
|
||||
@@ -80,7 +68,7 @@ index 53388eb8e..873060251 100644
|
||||
desiredEndpoints = append(desiredEndpoints, &discovery.Endpoint{
|
||||
Addresses: []string{i.IP},
|
||||
Conditions: discovery.EndpointConditions{
|
||||
@@ -705,9 +728,9 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
@@ -705,9 +719,9 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
|
||||
Serving: &serving,
|
||||
Terminating: &terminating,
|
||||
},
|
||||
@@ -92,71 +80,6 @@ index 53388eb8e..873060251 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 +1 @@
|
||||
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:31aee6f63c25a443974b56011d2907cedfeba245aa7caf732d7fc437b2b3ed50
|
||||
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:d5c836ba33cf5dbed7e6f866784f668f80ffe69179e7c75847b680111984eefb
|
||||
|
||||
@@ -1 +1 @@
|
||||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:71a74ca30f75967bae309be2758f19aa3d37c60b19426b9b622ff1c33a80362f
|
||||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:a09724a7f95283f9130b3da2a89d81c4c6051c6edf0392a81b6fc90f404b76b6
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{{- $etcd := .Values._namespace.etcd }}
|
||||
{{- $ingress := .Values._namespace.ingress }}
|
||||
{{- $host := .Values._namespace.host }}
|
||||
{{- $cozystack := .Values._cozystack | default dict }}
|
||||
{{- $namespace := .Values._namespace | default dict }}
|
||||
{{- $etcd := dig "etcd" "" $namespace }}
|
||||
{{- $ingress := dig "ingress" "" $namespace }}
|
||||
{{- $host := dig "host" "" $namespace }}
|
||||
{{- $kubevirtmachinetemplateNames := list }}
|
||||
{{- define "kubevirtmachinetemplate" -}}
|
||||
spec:
|
||||
@@ -30,13 +32,11 @@ spec:
|
||||
{{- end }}
|
||||
cluster.x-k8s.io/deployment-name: {{ $.Release.Name }}-{{ .groupName }}
|
||||
spec:
|
||||
{{- if .Values._cluster.scheduling }}
|
||||
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
|
||||
{{- if $rawConstraints }}
|
||||
{{- $rawConstraints | fromYaml | toYaml | nindent 10 }}
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
cluster.x-k8s.io/cluster-name: {{ $.Release.Name }}
|
||||
{{- $cozystack := $.Values._cozystack | default dict }}
|
||||
{{- $topologySpreadConstraints := dig "scheduling" "topologySpreadConstraints" (list) $cozystack }}
|
||||
{{- if $topologySpreadConstraints }}
|
||||
{{- range $topologySpreadConstraints }}
|
||||
- {{- mergeOverwrite (dict "labelSelector" (dict "matchLabels" (dict "cluster.x-k8s.io/cluster-name" $.Release.Name))) . | toYaml | nindent 12 | trim }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
domain:
|
||||
|
||||
@@ -5,8 +5,8 @@ metadata:
|
||||
annotations:
|
||||
"helm.sh/hook": pre-delete
|
||||
"helm.sh/hook-weight": "10"
|
||||
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation
|
||||
name: {{ .Release.Name }}-pre-cleanup
|
||||
"helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation,hook-failed
|
||||
name: {{ .Release.Name }}-cleanup
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
@@ -65,6 +65,7 @@ spec:
|
||||
|
||||
echo "Cleanup completed successfully"
|
||||
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
@@ -72,7 +73,7 @@ metadata:
|
||||
name: {{ .Release.Name }}-cleanup
|
||||
annotations:
|
||||
helm.sh/hook: pre-delete
|
||||
helm.sh/hook-delete-policy: hook-succeeded,before-hook-creation,hook-failed
|
||||
helm.sh/hook-delete-policy: before-hook-creation,hook-failed,hook-succeeded
|
||||
helm.sh/hook-weight: "0"
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
|
||||
@@ -8,15 +8,10 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cert-manager-crds
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-cert-manager-crds
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-iaas-kubernetes-cert-manager-crds
|
||||
namespace: cozy-system
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
@@ -45,6 +40,8 @@ spec:
|
||||
- name: {{ .Release.Name }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
- name: {{ .Release.Name }}-cilium
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- if .Values.addons.certManager.valuesOverride }}
|
||||
---
|
||||
apiVersion: v1
|
||||
|
||||
@@ -8,15 +8,10 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cert-manager
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-cert-manager
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-iaas-kubernetes-cert-manager
|
||||
namespace: cozy-system
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -22,15 +22,10 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cilium
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-cilium
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-iaas-kubernetes-cilium
|
||||
namespace: cozy-system
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -13,15 +13,10 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: coredns
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-coredns
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-iaas-kubernetes-coredns
|
||||
namespace: cozy-system
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,15 +8,10 @@ metadata:
|
||||
spec:
|
||||
interval: 5m
|
||||
releaseName: csi
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-kubevirt-csi-node
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-iaas-kubernetes-kubevirt-csi-node
|
||||
namespace: cozy-system
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
@@ -37,8 +32,6 @@ spec:
|
||||
dependsOn:
|
||||
- name: {{ .Release.Name }}-vsnap-crd
|
||||
namespace: {{ .Release.Namespace }}
|
||||
- name: {{ .Release.Name }}-cilium
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- if lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" .Release.Namespace .Release.Name }}
|
||||
- name: {{ .Release.Name }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
|
||||
@@ -8,15 +8,10 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: fluxcd-operator
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-fluxcd-operator
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-iaas-kubernetes-fluxcd-operator
|
||||
namespace: cozy-system
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
@@ -56,15 +51,10 @@ metadata:
|
||||
spec:
|
||||
interval: 5m
|
||||
releaseName: fluxcd
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-fluxcd
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-iaas-kubernetes-fluxcd
|
||||
namespace: cozy-system
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-kubeconfig
|
||||
|
||||
@@ -8,15 +8,10 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: gateway-api-crds
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-gateway-api-crds
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-iaas-kubernetes-gateway-api-crds
|
||||
namespace: cozy-system
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,15 +8,10 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: gpu-operator
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-gpu-operator
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-iaas-kubernetes-gpu-operator
|
||||
namespace: cozy-system
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user