mirror of
https://github.com/cozystack/cozystack.git
synced 2026-03-03 13:38:56 +00:00
Compare commits
419 Commits
v0.37.3
...
remove-ass
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
725f70399f | ||
|
|
49f7ac9192 | ||
|
|
ab8cc5ffd4 | ||
|
|
f5e6107e3a | ||
|
|
0fb02e6470 | ||
|
|
3a1e7fdd8f | ||
|
|
05b2244b36 | ||
|
|
d2fc6f8470 | ||
|
|
daa91fd2f3 | ||
|
|
43df3a1b70 | ||
|
|
7b75903cee | ||
|
|
07b406e9bc | ||
|
|
5f36396ccc | ||
|
|
811fde9993 | ||
|
|
695fc05dec | ||
|
|
2e61810547 | ||
|
|
36836fd84e | ||
|
|
bf1928c96f | ||
|
|
069a3ca9b0 | ||
|
|
f4228ffc20 | ||
|
|
bfafcaa3ab | ||
|
|
f59665208c | ||
|
|
66a756b606 | ||
|
|
a8688744e9 | ||
|
|
7a964eb7de | ||
|
|
3a5977ff60 | ||
|
|
bbeaaccd0c | ||
|
|
08e5a25ce7 | ||
|
|
dd0bbd375f | ||
|
|
d26d99c925 | ||
|
|
675eaa6178 | ||
|
|
5638a7eae9 | ||
|
|
1fc48da514 | ||
|
|
2d6e50bbeb | ||
|
|
dea42d035f | ||
|
|
f3722d0b2a | ||
|
|
f4e43469dc | ||
|
|
7e86a04fab | ||
|
|
935addebe9 | ||
|
|
a902eaec91 | ||
|
|
c200a45824 | ||
|
|
c06f399d20 | ||
|
|
f28d639aea | ||
|
|
63baadfd9a | ||
|
|
2d4e568efe | ||
|
|
b94fc6c9c8 | ||
|
|
36ef62af52 | ||
|
|
715509f57e | ||
|
|
db8a006565 | ||
|
|
7fab919e07 | ||
|
|
c43db3b7cc | ||
|
|
14803a4162 | ||
|
|
3a1f4ab854 | ||
|
|
307f20f8af | ||
|
|
5af0d132a5 | ||
|
|
a03042e017 | ||
|
|
4dfdbfeb62 | ||
|
|
da56c88aa7 | ||
|
|
e9cad4507b | ||
|
|
19e06f7beb | ||
|
|
0b16f83eae | ||
|
|
8cf68149cb | ||
|
|
cbc998b2ec | ||
|
|
fe9fca6abf | ||
|
|
d3f1b821f7 | ||
|
|
7165cea57a | ||
|
|
2990f0520a | ||
|
|
d7931fdb20 | ||
|
|
0697c0221b | ||
|
|
59f62b7834 | ||
|
|
f28b53d8dc | ||
|
|
e5c3492089 | ||
|
|
9d85cfb647 | ||
|
|
db800b510e | ||
|
|
202410a438 | ||
|
|
f7877b9bce | ||
|
|
ab6c6bad16 | ||
|
|
09e0b5f4ec | ||
|
|
e31ee932c0 | ||
|
|
517369629f | ||
|
|
00cef35214 | ||
|
|
e213b068e8 | ||
|
|
98488100e4 | ||
|
|
0ed52f670d | ||
|
|
b6900a258a | ||
|
|
c919ea6e39 | ||
|
|
7fa875db52 | ||
|
|
cf15ad1073 | ||
|
|
ba50d01877 | ||
|
|
7e7716aa44 | ||
|
|
669bf3d2f5 | ||
|
|
8d43f993e4 | ||
|
|
fe7bdcf06b | ||
|
|
733221286d | ||
|
|
9820d0c4b3 | ||
|
|
1f0f14cf18 | ||
|
|
df3a409142 | ||
|
|
97e8d2aa49 | ||
|
|
1805be3c48 | ||
|
|
c0e0ef0f7c | ||
|
|
1f0b5ff9ac | ||
|
|
1ec14d6bd6 | ||
|
|
03a71eb8de | ||
|
|
ee2a34ca81 | ||
|
|
0f7bd3e395 | ||
|
|
0d71525f7e | ||
|
|
8866a307bf | ||
|
|
10d35742e2 | ||
|
|
61ec812a3e | ||
|
|
373a0d1359 | ||
|
|
680f70c03a | ||
|
|
b1ba1f2172 | ||
|
|
e3b96e12be | ||
|
|
4f5ae287f5 | ||
|
|
6b8c490b1d | ||
|
|
90c725194f | ||
|
|
a05cc3512e | ||
|
|
8513dd6b3f | ||
|
|
0bab895026 | ||
|
|
349677ffe9 | ||
|
|
1f47fbc3dd | ||
|
|
d079dd4731 | ||
|
|
f3207fcd10 | ||
|
|
650e5290ea | ||
|
|
3975da93c6 | ||
|
|
19586e1eec | ||
|
|
578a810413 | ||
|
|
89897914fa | ||
|
|
892855276b | ||
|
|
58dd1f5881 | ||
|
|
67ecf3d0f6 | ||
|
|
38d6b98a70 | ||
|
|
0a93972c4f | ||
|
|
da4d6053bb | ||
|
|
28c933161a | ||
|
|
e50950b7a1 | ||
|
|
b1a55f5a38 | ||
|
|
1ee6eb8482 | ||
|
|
cbfa99148b | ||
|
|
54d0f52245 | ||
|
|
d0bfb6e2fc | ||
|
|
f86896eceb | ||
|
|
cb320f9d48 | ||
|
|
a7b423934f | ||
|
|
b1a7e9560e | ||
|
|
fe9d334880 | ||
|
|
1e2b66131c | ||
|
|
8d14bcb598 | ||
|
|
825390c209 | ||
|
|
09fd7c4094 | ||
|
|
8928731abf | ||
|
|
153635379a | ||
|
|
fbb2ea095a | ||
|
|
50c1d1a067 | ||
|
|
52ebcae8a2 | ||
|
|
5314d61987 | ||
|
|
8b29c53a45 | ||
|
|
4e4a5606d7 | ||
|
|
f2a4f1b1c8 | ||
|
|
33128748e6 | ||
|
|
7e1cad26e7 | ||
|
|
3d5118f5b3 | ||
|
|
06a25c1c45 | ||
|
|
13e0501acd | ||
|
|
ca29fc855a | ||
|
|
c7f478fc7d | ||
|
|
d53506ae2a | ||
|
|
8bc62d4c71 | ||
|
|
0b27f634c0 | ||
|
|
67e47256e2 | ||
|
|
df277b350c | ||
|
|
644d71eef7 | ||
|
|
9d1fb4ccf2 | ||
|
|
27efd3ad5e | ||
|
|
7b20e3f4cc | ||
|
|
5d354a07d6 | ||
|
|
aa8062c41c | ||
|
|
9ceb59e74c | ||
|
|
0df528a89d | ||
|
|
d70197c825 | ||
|
|
f2f8da0be1 | ||
|
|
094ee6da55 | ||
|
|
f256575fce | ||
|
|
d1ad38dd01 | ||
|
|
bc1fed4079 | ||
|
|
0b29ffefe0 | ||
|
|
c72a9333e9 | ||
|
|
d46cccda71 | ||
|
|
b5b12d0684 | ||
|
|
8283714930 | ||
|
|
aa428457db | ||
|
|
975011e04e | ||
|
|
0d88aa394a | ||
|
|
ec1a150d2c | ||
|
|
cbc6cd2567 | ||
|
|
fb7e39eaab | ||
|
|
9cc348733f | ||
|
|
00e0f45de3 | ||
|
|
b5c264de7d | ||
|
|
4ff60e4539 | ||
|
|
294458e7c4 | ||
|
|
42cb0e6974 | ||
|
|
73bf0e5f7e | ||
|
|
f512061a1c | ||
|
|
12db4fc520 | ||
|
|
8e351f1827 | ||
|
|
38a4adfaa3 | ||
|
|
91ddbb06ef | ||
|
|
7d2250be4d | ||
|
|
a070573af9 | ||
|
|
492aef93f5 | ||
|
|
23e6cf735a | ||
|
|
c5b1177149 | ||
|
|
84133ef2d3 | ||
|
|
1c9ae2bec5 | ||
|
|
03885f5ae2 | ||
|
|
bdff61eaed | ||
|
|
3d4ad39bce | ||
|
|
f2f575b450 | ||
|
|
aba4d2c977 | ||
|
|
e4021bbf57 | ||
|
|
ef8612e882 | ||
|
|
32b58dec5f | ||
|
|
1bafb7fb4f | ||
|
|
bc61d13ad3 | ||
|
|
972548cab4 | ||
|
|
bb8d07d384 | ||
|
|
6fdc9b0bad | ||
|
|
9c040cd42f | ||
|
|
5414d37376 | ||
|
|
a9818a7ce7 | ||
|
|
1651d94291 | ||
|
|
2b4afde373 | ||
|
|
a5c9bfabee | ||
|
|
143832c0b4 | ||
|
|
298206efc7 | ||
|
|
c81b222cf6 | ||
|
|
9d6af84449 | ||
|
|
7ddd9cf4a8 | ||
|
|
a861814c24 | ||
|
|
d65d293fbc | ||
|
|
523510469c | ||
|
|
cf5b2f2bbb | ||
|
|
4e5343e36c | ||
|
|
d8237b4321 | ||
|
|
83c3b0ca12 | ||
|
|
e1590aad1b | ||
|
|
304338d697 | ||
|
|
b65d639ecb | ||
|
|
339e71331f | ||
|
|
08be385665 | ||
|
|
2f0657f8ba | ||
|
|
a64ba184ce | ||
|
|
00328c8a31 | ||
|
|
7009c8da37 | ||
|
|
63db8ca009 | ||
|
|
369384f5ec | ||
|
|
4278692763 | ||
|
|
edc942b6c1 | ||
|
|
4c71e7fe57 | ||
|
|
627022972d | ||
|
|
1e8a9ee980 | ||
|
|
b45f4a6545 | ||
|
|
5b96190be8 | ||
|
|
8849570f74 | ||
|
|
b6958320b2 | ||
|
|
0a210bf5d3 | ||
|
|
90d50fef48 | ||
|
|
19ed058897 | ||
|
|
6438ce98b1 | ||
|
|
523d8ea638 | ||
|
|
e89896fdba | ||
|
|
ab5101a713 | ||
|
|
af460f1c41 | ||
|
|
634649f9ec | ||
|
|
df782fec9c | ||
|
|
172774b6cd | ||
|
|
62119eb761 | ||
|
|
48c6e23ca0 | ||
|
|
9064a72c92 | ||
|
|
dc06b16d11 | ||
|
|
739a74dc28 | ||
|
|
723eefea66 | ||
|
|
1d10907168 | ||
|
|
c19cddf08e | ||
|
|
4c08caafe1 | ||
|
|
be58047aba | ||
|
|
f60e2555c9 | ||
|
|
6443a1264e | ||
|
|
52a23eacfc | ||
|
|
2634b01465 | ||
|
|
15a3636d5f | ||
|
|
ef43ef6753 | ||
|
|
ba804b7c52 | ||
|
|
9c5abf49ca | ||
|
|
10e79651ef | ||
|
|
965818efd4 | ||
|
|
b1ebc9cc85 | ||
|
|
667c778f27 | ||
|
|
77d95e3b91 | ||
|
|
a8d3cbce82 | ||
|
|
eea685065a | ||
|
|
480f8027d7 | ||
|
|
19b56414a6 | ||
|
|
0f9806e9b0 | ||
|
|
177073596c | ||
|
|
93a9241899 | ||
|
|
5401ae9734 | ||
|
|
b78d97f374 | ||
|
|
8b95db06ee | ||
|
|
5a2d4d7e66 | ||
|
|
42e6f0e3f2 | ||
|
|
e2eb1e267b | ||
|
|
2ac533f2f6 | ||
|
|
ae9f9c57b1 | ||
|
|
18f253f77a | ||
|
|
bd9dcb52a3 | ||
|
|
be473a12be | ||
|
|
8f5adcccf5 | ||
|
|
08bd918a10 | ||
|
|
023276ebab | ||
|
|
19c4674ebb | ||
|
|
202da193c0 | ||
|
|
cc9687707c | ||
|
|
ac10e35272 | ||
|
|
fc7d5ee71f | ||
|
|
9d90503fb7 | ||
|
|
4be1c257d6 | ||
|
|
f3ba8eca8e | ||
|
|
0f286ee7ba | ||
|
|
5acf62824a | ||
|
|
93e33a0921 | ||
|
|
c4fa795491 | ||
|
|
f93042499b | ||
|
|
7cbe564ff1 | ||
|
|
62ff0c0b39 | ||
|
|
198b30887a | ||
|
|
9632772337 | ||
|
|
992c7d54fe | ||
|
|
4e3c8eafa1 | ||
|
|
05cd1a1c82 | ||
|
|
ee1c83ec85 | ||
|
|
1f784db3f7 | ||
|
|
f4e0145c1c | ||
|
|
efd96877ab | ||
|
|
5a20693d67 | ||
|
|
448fc61570 | ||
|
|
dc0eebd81e | ||
|
|
a545ff3781 | ||
|
|
82cebe3ad7 | ||
|
|
184441d82f | ||
|
|
ebbc76582c | ||
|
|
8e57ac487e | ||
|
|
766f6e9a9e | ||
|
|
ea74d7d59a | ||
|
|
74262977f6 | ||
|
|
d1fa0e6586 | ||
|
|
3e41504b2d | ||
|
|
06f68d28d9 | ||
|
|
21de4f7584 | ||
|
|
840c264e86 | ||
|
|
bbb92ba497 | ||
|
|
b163a5913f | ||
|
|
d26d3e1f40 | ||
|
|
ba8a9cc1f7 | ||
|
|
b858745cdd | ||
|
|
d9d6fa1a5a | ||
|
|
9e635fcc3f | ||
|
|
2791e3e96a | ||
|
|
1a977bd4b4 | ||
|
|
10516c38ec | ||
|
|
1e36722ab8 | ||
|
|
d57f9acc7a | ||
|
|
466f0fed52 | ||
|
|
c1edc5d711 | ||
|
|
354507a4ea | ||
|
|
a9c2bfb33b | ||
|
|
85c9da58de | ||
|
|
b1d5de1006 | ||
|
|
af96e3da94 | ||
|
|
bf1ece5f7c | ||
|
|
c3f70abc99 | ||
|
|
ea85856b1a | ||
|
|
e9bdbcf60d | ||
|
|
43ec8f0877 | ||
|
|
1f0edc5f79 | ||
|
|
fe8ec75ac7 | ||
|
|
671e13df70 | ||
|
|
36df9bda05 | ||
|
|
2a82273902 | ||
|
|
bb8f2047bf | ||
|
|
2a508c4f29 | ||
|
|
a4cbc7341d | ||
|
|
fdc2b8f3ec | ||
|
|
e6070210f6 | ||
|
|
e1e9eef63b | ||
|
|
f2a8e2d45d | ||
|
|
1a49cbef2d | ||
|
|
346dce83d4 | ||
|
|
4d8dca8049 | ||
|
|
7a19215f0a | ||
|
|
2d9dc9fe01 | ||
|
|
d86742eb36 | ||
|
|
857416d1d2 | ||
|
|
6bd7d3add5 | ||
|
|
2fcf975e6a | ||
|
|
b7bebecb64 | ||
|
|
edc12e3f7e | ||
|
|
27b06f4fbd | ||
|
|
3f6888a470 | ||
|
|
1a4e979e63 | ||
|
|
a3be02132d | ||
|
|
c6ec3168f7 | ||
|
|
5b58ec5cdd | ||
|
|
7e622181ed | ||
|
|
bf38316163 | ||
|
|
1475196437 | ||
|
|
076d69a10b | ||
|
|
5654ac4e3d |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
||||
* @kvaps @lllamnyp @klinch0
|
||||
* @kvaps @lllamnyp @nbykov0
|
||||
|
||||
188
.github/workflows/auto-release.yaml
vendored
Normal file
188
.github/workflows/auto-release.yaml
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
name: Auto Patch Release
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at 2:00 AM CET (1:00 UTC in winter, 0:00 UTC in summer)
|
||||
# Using 1:00 UTC to approximate 2:00 AM CET
|
||||
- cron: '0 1 * * *'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
concurrency:
|
||||
group: auto-release-${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
auto-release:
|
||||
name: Auto Patch Release
|
||||
runs-on: [self-hosted]
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Configure git
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
git config user.name "cozystack-bot"
|
||||
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
|
||||
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
|
||||
git config --unset-all http.https://github.com/.extraheader || true
|
||||
|
||||
- name: Process release branches
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_PAT }}
|
||||
script: |
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// Configure git to use PAT for authentication
|
||||
execSync('git config user.name "cozystack-bot"', { encoding: 'utf8' });
|
||||
execSync('git config user.email "217169706+cozystack-bot@users.noreply.github.com"', { encoding: 'utf8' });
|
||||
execSync(`git remote set-url origin https://cozystack-bot:${process.env.GH_PAT}@github.com/${process.env.GITHUB_REPOSITORY}`, { encoding: 'utf8' });
|
||||
// Remove GITHUB_TOKEN extraheader to ensure PAT is used (needed to trigger other workflows)
|
||||
execSync('git config --unset-all http.https://github.com/.extraheader || true', { encoding: 'utf8' });
|
||||
|
||||
// Get all release-X.Y branches
|
||||
const branches = execSync('git branch -r | grep -E "origin/release-[0-9]+\\.[0-9]+$" | sed "s|origin/||" | tr -d " "', { encoding: 'utf8' })
|
||||
.split('\n')
|
||||
.filter(b => b.trim())
|
||||
.filter(b => /^release-\d+\.\d+$/.test(b));
|
||||
|
||||
console.log(`Found ${branches.length} release branches: ${branches.join(', ')}`);
|
||||
|
||||
// Get all published releases (not draft)
|
||||
const allReleases = await github.rest.repos.listReleases({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
// Filter to only published releases (not draft) with tags matching vX.Y.Z (no suffixes)
|
||||
const publishedReleases = allReleases.data
|
||||
.filter(r => !r.draft)
|
||||
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name));
|
||||
|
||||
console.log(`Found ${publishedReleases.length} published releases without suffixes`);
|
||||
|
||||
for (const branch of branches) {
|
||||
console.log(`\n=== Processing branch: ${branch} ===`);
|
||||
|
||||
// Extract X.Y from branch name (release-X.Y)
|
||||
const match = branch.match(/^release-(\d+\.\d+)$/);
|
||||
if (!match) {
|
||||
console.log(` ⚠️ Branch ${branch} doesn't match pattern, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [major, minor] = match[1].split('.');
|
||||
const versionPrefix = `v${major}.${minor}.`;
|
||||
|
||||
console.log(` Looking for releases with prefix: ${versionPrefix}`);
|
||||
|
||||
// Find the latest published release for this branch (vX.Y.Z without suffixes)
|
||||
const branchReleases = publishedReleases
|
||||
.filter(r => r.tag_name.startsWith(versionPrefix))
|
||||
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name)); // Ensure no suffixes
|
||||
|
||||
if (branchReleases.length === 0) {
|
||||
console.log(` ⚠️ No published releases found for ${branch}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sort by version (descending) to get the latest
|
||||
branchReleases.sort((a, b) => {
|
||||
const aVersion = a.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
|
||||
const bVersion = b.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!aVersion || !bVersion) return 0;
|
||||
|
||||
const aNum = parseInt(aVersion[1]) * 10000 + parseInt(aVersion[2]) * 100 + parseInt(aVersion[3]);
|
||||
const bNum = parseInt(bVersion[1]) * 10000 + parseInt(bVersion[2]) * 100 + parseInt(bVersion[3]);
|
||||
return bNum - aNum;
|
||||
});
|
||||
|
||||
const latestRelease = branchReleases[0];
|
||||
console.log(` ✅ Latest published release: ${latestRelease.tag_name}`);
|
||||
|
||||
// Get the commit SHA for this release tag
|
||||
let releaseCommitSha;
|
||||
try {
|
||||
releaseCommitSha = execSync(`git rev-list -n 1 ${latestRelease.tag_name}`, { encoding: 'utf8' }).trim();
|
||||
console.log(` Release commit SHA: ${releaseCommitSha}`);
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ Could not find commit for tag ${latestRelease.tag_name}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Checkout the branch
|
||||
execSync(`git fetch origin ${branch}:${branch}`, { encoding: 'utf8' });
|
||||
execSync(`git checkout ${branch}`, { encoding: 'utf8' });
|
||||
|
||||
// Get the latest commit on the branch
|
||||
const latestBranchCommit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
|
||||
console.log(` Latest branch commit: ${latestBranchCommit}`);
|
||||
|
||||
// Check if there are new commits after the release
|
||||
const commitsAfterRelease = execSync(
|
||||
`git rev-list ${releaseCommitSha}..HEAD --oneline`,
|
||||
{ encoding: 'utf8' }
|
||||
).trim();
|
||||
|
||||
if (!commitsAfterRelease) {
|
||||
console.log(` ℹ️ No new commits after ${latestRelease.tag_name}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(` ✅ Found new commits after release:`);
|
||||
console.log(commitsAfterRelease);
|
||||
|
||||
// Calculate next version (Z+1)
|
||||
const versionMatch = latestRelease.tag_name.match(/^v(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (!versionMatch) {
|
||||
console.log(` ❌ Could not parse version from ${latestRelease.tag_name}, skipping`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextPatch = parseInt(versionMatch[3]) + 1;
|
||||
const nextTag = `v${versionMatch[1]}.${versionMatch[2]}.${nextPatch}`;
|
||||
|
||||
console.log(` 🏷️ Creating new tag: ${nextTag} on commit ${latestBranchCommit}`);
|
||||
|
||||
// Create and push the tag with base_ref for workflow triggering
|
||||
try {
|
||||
// Delete local tag if exists to force update
|
||||
try {
|
||||
execSync(`git tag -d ${nextTag}`, { encoding: 'utf8' });
|
||||
} catch (e) {
|
||||
// Tag doesn't exist locally, that's fine
|
||||
}
|
||||
|
||||
// Delete remote tag if exists
|
||||
try {
|
||||
execSync(`git push origin :refs/tags/${nextTag}`, { encoding: 'utf8' });
|
||||
} catch (e) {
|
||||
// Tag doesn't exist remotely, that's fine
|
||||
}
|
||||
|
||||
// Create tag locally
|
||||
execSync(`git tag ${nextTag} ${latestBranchCommit}`, { encoding: 'utf8' });
|
||||
|
||||
// Push tag with HEAD reference to preserve base_ref
|
||||
execSync(`git push origin HEAD:refs/tags/${nextTag}`, { encoding: 'utf8' });
|
||||
console.log(` ✅ Successfully created and pushed tag ${nextTag}`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error creating/pushing tag ${nextTag}: ${error.message}`);
|
||||
core.setFailed(`Failed to create tag ${nextTag} for branch ${branch}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Finished processing all release branches`);
|
||||
|
||||
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] # fires when PR is closed (merged)
|
||||
types: [closed, labeled] # fires when PR is closed (merged) or labeled
|
||||
|
||||
concurrency:
|
||||
group: backport-${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
@@ -13,22 +13,46 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
# Determine which backports are needed
|
||||
prepare:
|
||||
if: |
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'backport')
|
||||
(
|
||||
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'))
|
||||
)
|
||||
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:
|
||||
# 1. Decide which maintenance branch should receive the back‑port
|
||||
- name: Determine target maintenance branch
|
||||
id: target
|
||||
- name: Check which labels are present
|
||||
id: labels
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let rel;
|
||||
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;
|
||||
try {
|
||||
rel = await github.rest.repos.getLatestRelease({
|
||||
latestRelease = await github.rest.repos.getLatestRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
@@ -36,18 +60,70 @@ jobs:
|
||||
core.setFailed('No existing releases found; cannot determine backport target.');
|
||||
return;
|
||||
}
|
||||
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}`);
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
uses: korthout/backport-action@v3
|
||||
id: backport
|
||||
if: steps.target.outcome == 'success'
|
||||
uses: korthout/backport-action@v3.2.1
|
||||
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/v0.9.0/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.6/cozyvalues-gen-linux-amd64.tar.gz | tar -xzvf- -C /usr/local/bin/ cozyvalues-gen
|
||||
|
||||
- name: Run pre-commit hooks
|
||||
run: |
|
||||
|
||||
143
.github/workflows/pull-requests-release.yaml
vendored
143
.github/workflows/pull-requests-release.yaml
vendored
@@ -46,7 +46,12 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create tag on merge commit
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
git config user.name "cozystack-bot"
|
||||
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
|
||||
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
|
||||
git tag -f ${{ steps.get_tag.outputs.tag }} ${{ github.sha }}
|
||||
git push -f origin ${{ steps.get_tag.outputs.tag }}
|
||||
|
||||
@@ -105,67 +110,95 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
# Get the latest published release
|
||||
- name: Get the latest published release
|
||||
id: latest_release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
const rel = await github.rest.repos.getLatestRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
core.setOutput('tag', rel.data.tag_name);
|
||||
} catch (_) {
|
||||
core.setOutput('tag', '');
|
||||
}
|
||||
|
||||
# Compare current tag vs latest using semver-utils
|
||||
- name: Semver compare
|
||||
id: semver
|
||||
uses: madhead/semver-utils@v4.3.0
|
||||
with:
|
||||
version: ${{ steps.get_tag.outputs.tag }}
|
||||
compare-to: ${{ steps.latest_release.outputs.tag }}
|
||||
|
||||
# Derive flags: prerelease? make_latest?
|
||||
- name: Calculate publish flags
|
||||
id: flags
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const tag = '${{ steps.get_tag.outputs.tag }}'; // v0.31.5-rc.1
|
||||
const m = tag.match(/^v(\d+\.\d+\.\d+)(-(?:alpha|beta|rc)\.\d+)?$/);
|
||||
if (!m) {
|
||||
core.setFailed(`❌ tag '${tag}' must match 'vX.Y.Z' or 'vX.Y.Z-(alpha|beta|rc).N'`);
|
||||
return;
|
||||
}
|
||||
const version = m[1] + (m[2] ?? ''); // 0.31.5-rc.1
|
||||
const isRc = Boolean(m[2]);
|
||||
core.setOutput('is_rc', isRc);
|
||||
const outdated = '${{ steps.semver.outputs.comparison-result }}' === '<';
|
||||
core.setOutput('make_latest', isRc || outdated ? 'false' : 'legacy');
|
||||
|
||||
# Publish draft release with correct flags
|
||||
# Publish draft release and ensure correct latest flag
|
||||
- name: Publish draft release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const tag = '${{ steps.get_tag.outputs.tag }}';
|
||||
const m = tag.match(/^v(\d+\.\d+\.\d+)(-(?:alpha|beta|rc)\.\d+)?$/);
|
||||
if (!m) {
|
||||
core.setFailed(`❌ tag '${tag}' must match 'vX.Y.Z' or 'vX.Y.Z-(alpha|beta|rc).N'`);
|
||||
return;
|
||||
}
|
||||
const isRc = Boolean(m[2]);
|
||||
|
||||
// Parse semver string to comparable numbers
|
||||
function parseSemver(v) {
|
||||
const match = v.replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
major: parseInt(match[1]),
|
||||
minor: parseInt(match[2]),
|
||||
patch: parseInt(match[3])
|
||||
};
|
||||
}
|
||||
|
||||
// Compare two semver objects
|
||||
function compareSemver(a, b) {
|
||||
if (a.major !== b.major) return a.major - b.major;
|
||||
if (a.minor !== b.minor) return a.minor - b.minor;
|
||||
return a.patch - b.patch;
|
||||
}
|
||||
|
||||
const currentSemver = parseSemver(tag);
|
||||
|
||||
// Get all releases
|
||||
const releases = await github.rest.repos.listReleases({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
const draft = releases.data.find(r => r.tag_name === tag && r.draft);
|
||||
if (!draft) throw new Error(`Draft release for ${tag} not found`);
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: draft.id,
|
||||
draft: false,
|
||||
prerelease: ${{ steps.flags.outputs.is_rc }},
|
||||
make_latest: '${{ steps.flags.outputs.make_latest }}'
|
||||
repo: context.repo.repo,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
console.log(`🚀 Published release for ${tag}`);
|
||||
// Find draft release to publish
|
||||
const draft = releases.data.find(r => r.tag_name === tag && r.draft);
|
||||
if (!draft) throw new Error(`Draft release for ${tag} not found`);
|
||||
|
||||
// Find max semver among published releases (excluding current draft)
|
||||
const publishedReleases = releases.data
|
||||
.filter(r => !r.draft && !r.prerelease)
|
||||
.filter(r => /^v\d+\.\d+\.\d+$/.test(r.tag_name))
|
||||
.map(r => ({ id: r.id, tag: r.tag_name, semver: parseSemver(r.tag_name) }))
|
||||
.filter(r => r.semver !== null);
|
||||
|
||||
let maxRelease = null;
|
||||
for (const rel of publishedReleases) {
|
||||
if (!maxRelease || compareSemver(rel.semver, maxRelease.semver) > 0) {
|
||||
maxRelease = rel;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if this release should be latest
|
||||
const isOutdated = maxRelease && compareSemver(currentSemver, maxRelease.semver) < 0;
|
||||
const makeLatest = (isRc || isOutdated) ? 'false' : 'true';
|
||||
|
||||
if (isRc) {
|
||||
console.log(`🏷️ ${tag} is a prerelease, make_latest: false`);
|
||||
} else if (isOutdated) {
|
||||
console.log(`🏷️ ${tag} < ${maxRelease.tag} (max semver), make_latest: false`);
|
||||
} else {
|
||||
console.log(`🏷️ ${tag} is the highest version, make_latest: true`);
|
||||
}
|
||||
|
||||
// Publish the release
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: draft.id,
|
||||
draft: false,
|
||||
prerelease: isRc,
|
||||
make_latest: makeLatest
|
||||
});
|
||||
console.log(`🚀 Published release ${tag}`);
|
||||
|
||||
// If this is a backport/outdated release, ensure the correct release is marked as latest
|
||||
if (isOutdated && maxRelease) {
|
||||
console.log(`🔧 Ensuring ${maxRelease.tag} remains the latest release...`);
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: maxRelease.id,
|
||||
make_latest: 'true'
|
||||
});
|
||||
console.log(`✅ Restored ${maxRelease.tag} as latest release`);
|
||||
}
|
||||
|
||||
8
.github/workflows/pull-requests.yaml
vendored
8
.github/workflows/pull-requests.yaml
vendored
@@ -1,7 +1,8 @@
|
||||
name: Pull Request
|
||||
|
||||
env:
|
||||
REGISTRY: ${{ vars.OCIR_REPO }}
|
||||
# TODO: unhardcode this
|
||||
REGISTRY: iad.ocir.io/idyksih5sir9/cozystack
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
@@ -32,6 +33,9 @@ jobs:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Run unit tests
|
||||
run: make unit-tests
|
||||
|
||||
- name: Set up Docker config
|
||||
run: |
|
||||
if [ -d ~/.docker ]; then
|
||||
@@ -54,7 +58,7 @@ jobs:
|
||||
DOCKER_CONFIG: ${{ runner.temp }}/.docker
|
||||
|
||||
- name: Build Talos image
|
||||
run: make -C packages/core/installer talos-nocloud
|
||||
run: make -C packages/core/talos talos-nocloud
|
||||
|
||||
- name: Save git diff as patch
|
||||
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
|
||||
|
||||
78
.github/workflows/retest.yaml
vendored
Normal file
78
.github/workflows/retest.yaml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Retest
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
retest:
|
||||
name: Retest PR
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/retest')
|
||||
permissions:
|
||||
actions: write
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Rerun from Prepare environment
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.issue.number;
|
||||
|
||||
// Get the PR to find the head SHA
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
// Find the latest workflow run for this PR
|
||||
const runs = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'pull-requests.yaml',
|
||||
head_sha: pr.data.head.sha
|
||||
});
|
||||
|
||||
if (runs.data.workflow_runs.length === 0) {
|
||||
core.setFailed('No workflow runs found for this PR');
|
||||
return;
|
||||
}
|
||||
|
||||
const latestRun = runs.data.workflow_runs[0];
|
||||
console.log(`Found workflow run: ${latestRun.id} (${latestRun.status})`);
|
||||
|
||||
// Check if workflow is waiting for approval (fork PRs)
|
||||
if (latestRun.conclusion === 'action_required') {
|
||||
core.setFailed('Workflow is waiting for approval. A maintainer must approve the workflow first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get jobs for this run
|
||||
const jobs = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: latestRun.id
|
||||
});
|
||||
|
||||
// Find "Prepare environment" job
|
||||
const prepareJob = jobs.data.jobs.find(j => j.name === 'Prepare environment');
|
||||
|
||||
if (!prepareJob) {
|
||||
core.setFailed('Could not find "Prepare environment" job');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found job: ${prepareJob.name} (id: ${prepareJob.id}, status: ${prepareJob.status})`);
|
||||
|
||||
// Rerun the job
|
||||
await github.rest.actions.reRunJobForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
job_id: prepareJob.id
|
||||
});
|
||||
|
||||
console.log(`✅ Triggered rerun of job "${prepareJob.name}" (${prepareJob.id})`);
|
||||
26
.github/workflows/tags.yaml
vendored
26
.github/workflows/tags.yaml
vendored
@@ -123,32 +123,6 @@ jobs:
|
||||
git commit -m "Prepare release ${GITHUB_REF#refs/tags/}" -s || echo "No changes to commit"
|
||||
git push origin HEAD || true
|
||||
|
||||
# Get `latest_version` from latest published release
|
||||
- name: Get latest published release
|
||||
if: steps.check_release.outputs.skip == 'false'
|
||||
id: latest_release
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
try {
|
||||
const rel = await github.rest.repos.getLatestRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo
|
||||
});
|
||||
core.setOutput('tag', rel.data.tag_name);
|
||||
} catch (_) {
|
||||
core.setOutput('tag', '');
|
||||
}
|
||||
|
||||
# Compare tag (A) with latest (B)
|
||||
- name: Semver compare
|
||||
if: steps.check_release.outputs.skip == 'false'
|
||||
id: semver
|
||||
uses: madhead/semver-utils@v4.3.0
|
||||
with:
|
||||
version: ${{ steps.tag.outputs.tag }} # A
|
||||
compare-to: ${{ steps.latest_release.outputs.tag }} # B
|
||||
|
||||
# Create or reuse draft release
|
||||
- name: Create / reuse draft release
|
||||
if: steps.check_release.outputs.skip == 'false'
|
||||
|
||||
92
.github/workflows/update-releasenotes.yaml
vendored
Normal file
92
.github/workflows/update-releasenotes.yaml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
name: Update Release Notes
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: update-releasenotes-${{ github.workflow }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
update-releasenotes:
|
||||
name: Update Release Notes
|
||||
runs-on: [self-hosted]
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Update release notes from changelogs
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const changelogDir = 'docs/changelogs';
|
||||
|
||||
// Get releases from first page
|
||||
const releases = await github.rest.repos.listReleases({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
per_page: 30
|
||||
});
|
||||
|
||||
console.log(`Found ${releases.data.length} releases (first page only)`);
|
||||
|
||||
// Process each release
|
||||
for (const release of releases.data) {
|
||||
const tag = release.tag_name;
|
||||
const changelogFile = `${tag}.md`;
|
||||
const changelogPath = path.join(changelogDir, changelogFile);
|
||||
|
||||
console.log(`\nProcessing release: ${tag}`);
|
||||
|
||||
// Check if changelog file exists
|
||||
if (!fs.existsSync(changelogPath)) {
|
||||
console.log(` ⚠️ Changelog file ${changelogFile} does not exist, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read changelog file content
|
||||
let changelogContent;
|
||||
try {
|
||||
changelogContent = fs.readFileSync(changelogPath, 'utf8');
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error reading file ${changelogPath}: ${error.message}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!changelogContent.trim()) {
|
||||
console.log(` ⚠️ Changelog file ${changelogFile} is empty, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if content is already up to date
|
||||
const currentBody = release.body || '';
|
||||
if (currentBody.trim() === changelogContent.trim()) {
|
||||
console.log(` ✓ Content is already up to date, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update release notes
|
||||
try {
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: release.id,
|
||||
body: changelogContent
|
||||
});
|
||||
console.log(` ✅ Successfully updated release notes for ${tag}`);
|
||||
} catch (error) {
|
||||
console.log(` ❌ Error updating release ${tag}: ${error.message}`);
|
||||
core.setFailed(`Failed to update release notes for ${tag}`);
|
||||
}
|
||||
}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
_out
|
||||
_repos
|
||||
.git
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
@@ -31,4 +31,5 @@ This list is sorted in chronological order, based on the submission date.
|
||||
| [gohost](https://gohost.kz/) | @karabass_off | 2024-02-01 | Our company has been working in the market of Kazakhstan for more than 15 years, providing clients with a standard set of services: VPS/VDC, IaaS, shared hosting, etc. Now we are expanding the lineup by introducing Bare Metal Kubenetes cluster under Cozystack management. |
|
||||
| [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. |
|
||||
|
||||
62
AGENTS.md
Normal file
62
AGENTS.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# AI Agents Overview
|
||||
|
||||
This file provides structured guidance for AI coding assistants and agents
|
||||
working with the **Cozystack** project.
|
||||
|
||||
## Activation
|
||||
|
||||
**CRITICAL**: When the user asks you to do something that matches the scope of a documented process, you MUST read the corresponding documentation file and follow the instructions exactly as written.
|
||||
|
||||
- **Commits, PRs, git operations** (e.g., "create a commit", "make a PR", "fix review comments", "rebase", "cherry-pick")
|
||||
- Read: [`contributing.md`](./docs/agents/contributing.md)
|
||||
- Action: Read the entire file and follow ALL instructions step-by-step
|
||||
|
||||
- **Changelog generation** (e.g., "generate changelog", "create changelog", "prepare changelog for version X")
|
||||
- Read: [`changelog.md`](./docs/agents/changelog.md)
|
||||
- Action: Read the entire file and follow ALL steps in the checklist. Do NOT skip any mandatory steps
|
||||
|
||||
- **Release creation** (e.g., "create release", "prepare release", "tag release", "make a release")
|
||||
- Read: [`releasing.md`](./docs/agents/releasing.md)
|
||||
- Action: Read the file and follow the referenced release process in `docs/release.md`
|
||||
|
||||
- **Project structure, conventions, code layout** (e.g., "where should I put X", "what's the convention for Y", "how is the project organized")
|
||||
- Read: [`overview.md`](./docs/agents/overview.md)
|
||||
- Action: Read relevant sections to understand project structure and conventions
|
||||
|
||||
- **General questions about contributing**
|
||||
- Read: [`contributing.md`](./docs/agents/contributing.md)
|
||||
- Action: Read the file to understand git workflow, commit format, PR process
|
||||
|
||||
**Important rules:**
|
||||
- ✅ **ONLY read the file if the task matches the documented process scope** - do not read files for tasks that don't match their purpose
|
||||
- ✅ **ALWAYS read the file FIRST** before starting the task (when applicable)
|
||||
- ✅ **Follow instructions EXACTLY** as written in the documentation
|
||||
- ✅ **Do NOT skip mandatory steps** (especially in changelog.md)
|
||||
- ✅ **Do NOT assume** you know the process - always check the documentation when the task matches
|
||||
- ❌ **Do NOT read files** for tasks that are outside their documented scope
|
||||
- 📖 **Note**: [`overview.md`](./docs/agents/overview.md) can be useful as a reference to understand project structure and conventions, even when not explicitly required by the task
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Cozystack** is a Kubernetes-based platform for building cloud infrastructure with managed services (databases, VMs, K8s clusters), multi-tenancy, and GitOps delivery.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Code Structure
|
||||
- `packages/core/` - Core platform charts (installer, platform)
|
||||
- `packages/system/` - System components (CSI, CNI, operators)
|
||||
- `packages/apps/` - User-facing applications in catalog
|
||||
- `packages/extra/` - Tenant-specific modules
|
||||
- `cmd/`, `internal/`, `pkg/` - Go code
|
||||
- `api/` - Kubernetes CRDs
|
||||
|
||||
### Conventions
|
||||
- **Helm Charts**: Umbrella pattern, vendored upstream charts in `charts/`
|
||||
- **Go Code**: Controller-runtime patterns, kubebuilder style
|
||||
- **Git Commits**: `[component] Description` format with `--signoff`
|
||||
|
||||
### What NOT to Do
|
||||
- ❌ Edit `/vendor/`, `zz_generated.*.go`, upstream charts directly
|
||||
- ❌ Modify `go.mod`/`go.sum` manually (use `go get`)
|
||||
- ❌ Force push to main/master
|
||||
- ❌ Commit built artifacts from `_out`
|
||||
23
Makefile
23
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: manifests repos assets
|
||||
.PHONY: manifests assets unit-tests helm-unit-tests
|
||||
|
||||
build-deps:
|
||||
@command -V find docker skopeo jq gh helm > /dev/null
|
||||
@@ -15,8 +15,10 @@ build: build-deps
|
||||
make -C packages/extra/monitoring image
|
||||
make -C packages/system/cozystack-api image
|
||||
make -C packages/system/cozystack-controller image
|
||||
make -C packages/system/backup-controller image
|
||||
make -C packages/system/lineage-controller-webhook image
|
||||
make -C packages/system/cilium image
|
||||
make -C packages/system/kubeovn image
|
||||
make -C packages/system/linstor image
|
||||
make -C packages/system/kubeovn-webhook image
|
||||
make -C packages/system/kubeovn-plunger image
|
||||
make -C packages/system/dashboard image
|
||||
@@ -24,27 +26,28 @@ build: build-deps
|
||||
make -C packages/system/kamaji image
|
||||
make -C packages/system/bucket image
|
||||
make -C packages/system/objectstorage-controller image
|
||||
make -C packages/system/grafana-operator image
|
||||
make -C packages/core/testing image
|
||||
make -C packages/core/talos image
|
||||
make -C packages/core/installer image
|
||||
make manifests
|
||||
|
||||
repos:
|
||||
rm -rf _out
|
||||
make -C packages/system repo
|
||||
make -C packages/apps repo
|
||||
make -C packages/extra repo
|
||||
|
||||
manifests:
|
||||
mkdir -p _out/assets
|
||||
(cd packages/core/installer/; helm template -n cozy-installer installer .) > _out/assets/cozystack-installer.yaml
|
||||
(cd packages/core/installer/; helm template --namespace cozy-installer installer .) > _out/assets/cozystack-installer.yaml
|
||||
|
||||
assets:
|
||||
make -C packages/core/installer assets
|
||||
make -C packages/core/talos assets
|
||||
|
||||
test:
|
||||
make -C packages/core/testing apply
|
||||
make -C packages/core/testing test
|
||||
|
||||
unit-tests: helm-unit-tests
|
||||
|
||||
helm-unit-tests:
|
||||
hack/helm-unit-tests.sh
|
||||
|
||||
prepare-env:
|
||||
make -C packages/core/testing apply
|
||||
make -C packages/core/testing prepare-cluster
|
||||
|
||||
1
api/.gitattributes
vendored
Normal file
1
api/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
zz_generated_deepcopy.go linguist-generated
|
||||
37
api/backups/strategy/v1alpha1/groupversion_info.go
Normal file
37
api/backups/strategy/v1alpha1/groupversion_info.go
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
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 contains API Schema definitions for the v1alpha1 API group.
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=strategy.backups.cozystack.io
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
var (
|
||||
GroupVersion = schema.GroupVersion{Group: "strategy.backups.cozystack.io", Version: "v1alpha1"}
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addGroupVersion)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func addGroupVersion(scheme *runtime.Scheme) error {
|
||||
metav1.AddToGroupVersion(scheme, GroupVersion)
|
||||
return nil
|
||||
}
|
||||
63
api/backups/strategy/v1alpha1/job_types.go
Normal file
63
api/backups/strategy/v1alpha1/job_types.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines strategy.backups.cozystack.io API types.
|
||||
//
|
||||
// Group: strategy.backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&Job{},
|
||||
&JobList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
JobStrategyKind = "Job"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
|
||||
// Job defines a backup strategy using a one-shot Job
|
||||
type Job struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec JobSpec `json:"spec,omitempty"`
|
||||
Status JobStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// JobList contains a list of backup Jobs.
|
||||
type JobList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Job `json:"items"`
|
||||
}
|
||||
|
||||
// JobSpec specifies the desired behavior of a backup job.
|
||||
type JobSpec struct {
|
||||
// Template holds a PodTemplateSpec with the right shape to
|
||||
// run a single pod to completion and create a tarball with
|
||||
// a given apps data. Helm-like Go templates are supported.
|
||||
// The values of the source application are available under
|
||||
// `.Values`. `.Release.Name` and `.Release.Namespace` are
|
||||
// also exported.
|
||||
Template corev1.PodTemplateSpec `json:"template"`
|
||||
}
|
||||
|
||||
type JobStatus struct {
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
63
api/backups/strategy/v1alpha1/velero_types.go
Normal file
63
api/backups/strategy/v1alpha1/velero_types.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines strategy.backups.cozystack.io API types.
|
||||
//
|
||||
// Group: strategy.backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&Velero{},
|
||||
&VeleroList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
VeleroStrategyKind = "Velero"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:resource:scope=Cluster
|
||||
|
||||
// Velero defines a backup strategy using Velero as the driver.
|
||||
type Velero struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec VeleroSpec `json:"spec,omitempty"`
|
||||
Status VeleroStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// VeleroList contains a list of Velero backup strategies.
|
||||
type VeleroList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Velero `json:"items"`
|
||||
}
|
||||
|
||||
// VeleroSpec specifies the desired strategy for backing up with Velero.
|
||||
type VeleroSpec struct {
|
||||
Template VeleroTemplate `json:"template"`
|
||||
}
|
||||
|
||||
// VeleroTemplate describes the data a backup.velero.io should have when
|
||||
// templated from a Velero backup strategy.
|
||||
type VeleroTemplate struct {
|
||||
Spec velerov1.BackupSpec `json:"spec"`
|
||||
}
|
||||
|
||||
type VeleroStatus struct {
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
236
api/backups/strategy/v1alpha1/zz_generated.deepcopy.go
Normal file
236
api/backups/strategy/v1alpha1/zz_generated.deepcopy.go
Normal file
@@ -0,0 +1,236 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Job) DeepCopyInto(out *Job) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Job.
|
||||
func (in *Job) DeepCopy() *Job {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Job)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Job) 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 *JobList) DeepCopyInto(out *JobList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Job, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobList.
|
||||
func (in *JobList) DeepCopy() *JobList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(JobList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *JobList) 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 *JobSpec) DeepCopyInto(out *JobSpec) {
|
||||
*out = *in
|
||||
in.Template.DeepCopyInto(&out.Template)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobSpec.
|
||||
func (in *JobSpec) DeepCopy() *JobSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(JobSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *JobStatus) DeepCopyInto(out *JobStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobStatus.
|
||||
func (in *JobStatus) DeepCopy() *JobStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(JobStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Velero) DeepCopyInto(out *Velero) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Velero.
|
||||
func (in *Velero) DeepCopy() *Velero {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Velero)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Velero) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VeleroList) DeepCopyInto(out *VeleroList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Velero, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroList.
|
||||
func (in *VeleroList) DeepCopy() *VeleroList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VeleroList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *VeleroList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VeleroSpec) DeepCopyInto(out *VeleroSpec) {
|
||||
*out = *in
|
||||
in.Template.DeepCopyInto(&out.Template)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroSpec.
|
||||
func (in *VeleroSpec) DeepCopy() *VeleroSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VeleroSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VeleroStatus) DeepCopyInto(out *VeleroStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]v1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroStatus.
|
||||
func (in *VeleroStatus) DeepCopy() *VeleroStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VeleroStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VeleroTemplate) DeepCopyInto(out *VeleroTemplate) {
|
||||
*out = *in
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VeleroTemplate.
|
||||
func (in *VeleroTemplate) DeepCopy() *VeleroTemplate {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(VeleroTemplate)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
421
api/backups/v1alpha1/DESIGN.md
Normal file
421
api/backups/v1alpha1/DESIGN.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# Cozystack Backups – Core API & Contracts (Draft)
|
||||
|
||||
## 1. Overview
|
||||
|
||||
Cozystack’s backup subsystem provides a generic, composable way to back up and restore managed applications:
|
||||
|
||||
* Every **application instance** can have one or more **backup plans**.
|
||||
* Backups are stored in configurable **storage locations**.
|
||||
* The mechanics of *how* a backup/restore is performed are delegated to **strategy drivers**, each implementing driver-specific **BackupStrategy** CRDs.
|
||||
|
||||
The core API:
|
||||
|
||||
* Orchestrates **when** backups happen and **where** they’re stored.
|
||||
* Tracks **what** backups exist and their status.
|
||||
* Defines contracts with drivers via shared resources (`BackupJob`, `Backup`, `RestoreJob`).
|
||||
|
||||
It does **not** implement the backup logic itself.
|
||||
|
||||
This document covers only the **core** API and its contracts with drivers, not driver implementations.
|
||||
|
||||
---
|
||||
|
||||
## 2. Goals and non-goals
|
||||
|
||||
### Goals
|
||||
|
||||
* Provide a **stable core API** for:
|
||||
|
||||
* Declaring **backup plans** per application.
|
||||
* Configuring **storage targets** (S3, in-cluster bucket, etc.).
|
||||
* Tracking **backup artifacts**.
|
||||
* Initiating and tracking **restores**.
|
||||
* Allow multiple **strategy drivers** to plug in, each supporting specific kinds of applications and strategies.
|
||||
* Let application/product authors implement backup for their kinds by:
|
||||
|
||||
* Creating **Plan** objects referencing a **driver-specific strategy**.
|
||||
* Not having to write a backup engine themselves.
|
||||
|
||||
### Non-goals
|
||||
|
||||
* Implement backup logic for any specific application or storage backend.
|
||||
* Define the internal structure of driver-specific strategy CRDs.
|
||||
* Handle tenant-facing UI/UX (that’s built on top of these APIs).
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
High-level components:
|
||||
|
||||
* **Core backups controller(s)** (Cozystack-owned):
|
||||
|
||||
* Group: `backups.cozystack.io`
|
||||
* Own:
|
||||
|
||||
* `Plan`
|
||||
* `BackupJob`
|
||||
* `Backup`
|
||||
* `RestoreJob`
|
||||
* Responsibilities:
|
||||
|
||||
* Schedule backups based on `Plan`.
|
||||
* Create `BackupJob` objects when due.
|
||||
* Provide stable contracts for drivers to:
|
||||
|
||||
* Perform backups and create `Backup`s.
|
||||
* Perform restores based on `Backup`s.
|
||||
|
||||
* **Strategy drivers** (pluggable, possibly third-party):
|
||||
|
||||
* Their own API groups, e.g. `jobdriver.backups.cozystack.io`.
|
||||
* Own **strategy CRDs** (e.g. `JobBackupStrategy`).
|
||||
* Implement controllers that:
|
||||
|
||||
* Watch `BackupJob` / `RestoreJob`.
|
||||
* Match runs whose `strategyRef` GVK they support.
|
||||
* Execute backup/restore logic.
|
||||
* Create and update `Backup` and run statuses.
|
||||
|
||||
Strategy drivers and core communicate entirely via Kubernetes objects; there are no webhook/HTTP calls between them.
|
||||
|
||||
* **Storage drivers** (pluggable, possibly third-party):
|
||||
|
||||
* **TBD**
|
||||
|
||||
---
|
||||
|
||||
## 4. Core API resources
|
||||
|
||||
### 4.1 Plan
|
||||
|
||||
**Group/Kind**
|
||||
`backups.cozystack.io/v1alpha1, Kind=Plan`
|
||||
|
||||
**Purpose**
|
||||
Describe **when**, **how**, and **where** to back up a specific managed application.
|
||||
|
||||
**Key fields (spec)**
|
||||
|
||||
```go
|
||||
type PlanSpec struct {
|
||||
// Application to back up.
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
|
||||
// Where backups should be stored.
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
|
||||
// Driver-specific BackupStrategy to use.
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
|
||||
// When backups should run.
|
||||
Schedule PlanSchedule `json:"schedule"`
|
||||
}
|
||||
```
|
||||
|
||||
`PlanSchedule` (initially) supports only cron:
|
||||
|
||||
```go
|
||||
type PlanScheduleType string
|
||||
|
||||
const (
|
||||
PlanScheduleTypeEmpty PlanScheduleType = ""
|
||||
PlanScheduleTypeCron PlanScheduleType = "cron"
|
||||
)
|
||||
```
|
||||
|
||||
```go
|
||||
type PlanSchedule struct {
|
||||
// Type is the schedule type. Currently only "cron" is supported.
|
||||
// Defaults to "cron".
|
||||
Type PlanScheduleType `json:"type,omitempty"`
|
||||
|
||||
// Cron expression (required for cron type).
|
||||
Cron string `json:"cron,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**Plan reconciliation contract**
|
||||
|
||||
Core Plan controller:
|
||||
|
||||
1. **Read schedule** from `spec.schedule` and compute the next fire time.
|
||||
2. When due:
|
||||
|
||||
* Create a `BackupJob` in the same namespace:
|
||||
|
||||
* `spec.planRef.name = plan.Name`
|
||||
* `spec.applicationRef = plan.spec.applicationRef`
|
||||
* `spec.storageRef = plan.spec.storageRef`
|
||||
* `spec.strategyRef = plan.spec.strategyRef`
|
||||
* `spec.triggeredBy = "Plan"`
|
||||
* Set `ownerReferences` so the `BackupJob` is owned by the `Plan`.
|
||||
|
||||
The Plan controller does **not**:
|
||||
|
||||
* Execute backups itself.
|
||||
* Modify driver resources or `Backup` objects.
|
||||
* Touch `BackupJob.spec` after creation.
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Storage
|
||||
|
||||
**API Shape**
|
||||
|
||||
TBD
|
||||
|
||||
**Storage usage**
|
||||
|
||||
* `Plan` and `BackupJob` reference `Storage` via `TypedLocalObjectReference`.
|
||||
* Drivers read `Storage` to know how/where to store or read artifacts.
|
||||
* Core treats `Storage` spec as opaque; it does not directly talk to S3 or buckets.
|
||||
|
||||
---
|
||||
|
||||
### 4.3 BackupJob
|
||||
|
||||
**Group/Kind**
|
||||
`backups.cozystack.io/v1alpha1, Kind=BackupJob`
|
||||
|
||||
**Purpose**
|
||||
Represent a single **execution** of a backup operation, typically created when a `Plan` fires or when a user triggers an ad-hoc backup.
|
||||
|
||||
**Key fields (spec)**
|
||||
|
||||
```go
|
||||
type BackupJobSpec struct {
|
||||
// Plan that triggered this run, if any.
|
||||
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
|
||||
|
||||
// Application to back up.
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
|
||||
// Storage to use.
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
|
||||
// Driver-specific BackupStrategy to use.
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
|
||||
// Informational: what triggered this run ("Plan", "Manual", etc.).
|
||||
TriggeredBy string `json:"triggeredBy,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields (status)**
|
||||
|
||||
```go
|
||||
type BackupJobStatus struct {
|
||||
Phase BackupJobPhase `json:"phase,omitempty"`
|
||||
BackupRef *corev1.LocalObjectReference `json:"backupRef,omitempty"`
|
||||
StartedAt *metav1.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
`BackupJobPhase` is one of: `Pending`, `Running`, `Succeeded`, `Failed`.
|
||||
|
||||
**BackupJob contract with drivers**
|
||||
|
||||
* Core **creates** `BackupJob` and must treat `spec` as immutable afterwards.
|
||||
* Each driver controller:
|
||||
|
||||
* Watches `BackupJob`.
|
||||
* Reconciles runs where `spec.strategyRef.apiGroup/kind` matches its **strategy type(s)**.
|
||||
* Driver responsibilities:
|
||||
|
||||
1. On first reconcile:
|
||||
|
||||
* Set `status.startedAt` if unset.
|
||||
* Set `status.phase = Running`.
|
||||
2. Resolve inputs:
|
||||
|
||||
* Read `Strategy` (driver-owned CRD), `Storage`, `Application`, optionally `Plan`.
|
||||
3. Execute backup logic (implementation-specific).
|
||||
4. On success:
|
||||
|
||||
* Create a `Backup` resource (see below).
|
||||
* Set `status.backupRef` to the created `Backup`.
|
||||
* Set `status.completedAt`.
|
||||
* Set `status.phase = Succeeded`.
|
||||
5. On failure:
|
||||
|
||||
* Set `status.completedAt`.
|
||||
* Set `status.phase = Failed`.
|
||||
* Set `status.message` and conditions.
|
||||
|
||||
Drivers must **not** modify `BackupJob.spec` or delete `BackupJob` themselves.
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Backup
|
||||
|
||||
**Group/Kind**
|
||||
`backups.cozystack.io/v1alpha1, Kind=Backup`
|
||||
|
||||
**Purpose**
|
||||
Represent a single **backup artifact** for a given application, decoupled from a particular run. usable as a stable, listable “thing you can restore from”.
|
||||
|
||||
**Key fields (spec)**
|
||||
|
||||
```go
|
||||
type BackupSpec struct {
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
TakenAt metav1.Time `json:"takenAt"`
|
||||
DriverMetadata map[string]string `json:"driverMetadata,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields (status)**
|
||||
|
||||
```go
|
||||
type BackupStatus struct {
|
||||
Phase BackupPhase `json:"phase,omitempty"` // Pending, Ready, Failed, etc.
|
||||
Artifact *BackupArtifact `json:"artifact,omitempty"`
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
`BackupArtifact` describes the artifact (URI, size, checksum).
|
||||
|
||||
**Backup contract with drivers**
|
||||
|
||||
* On successful completion of a `BackupJob`, the **driver**:
|
||||
|
||||
* Creates a `Backup` in the same namespace (typically owned by the `BackupJob`).
|
||||
* Populates `spec` fields with:
|
||||
|
||||
* The application, storage, strategy references.
|
||||
* `takenAt`.
|
||||
* Optional `driverMetadata`.
|
||||
* Sets `status` with:
|
||||
|
||||
* `phase = Ready` (or equivalent when fully usable).
|
||||
* `artifact` describing the stored object.
|
||||
* Core:
|
||||
|
||||
* Treats `Backup` spec as mostly immutable and opaque.
|
||||
* Uses it to:
|
||||
|
||||
* List backups for a given application/plan.
|
||||
* Anchor `RestoreJob` operations.
|
||||
* Implement higher-level policies (retention) if needed.
|
||||
|
||||
---
|
||||
|
||||
### 4.5 RestoreJob
|
||||
|
||||
**Group/Kind**
|
||||
`backups.cozystack.io/v1alpha1, Kind=RestoreJob`
|
||||
|
||||
**Purpose**
|
||||
Represent a single **restore operation** from a `Backup`, either back into the same application or into a new target application.
|
||||
|
||||
**Key fields (spec)**
|
||||
|
||||
```go
|
||||
type RestoreJobSpec struct {
|
||||
// Backup to restore from.
|
||||
BackupRef corev1.LocalObjectReference `json:"backupRef"`
|
||||
|
||||
// Target application; if omitted, drivers SHOULD restore into
|
||||
// backup.spec.applicationRef.
|
||||
TargetApplicationRef *corev1.TypedLocalObjectReference `json:"targetApplicationRef,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields (status)**
|
||||
|
||||
```go
|
||||
type RestoreJobStatus struct {
|
||||
Phase RestoreJobPhase `json:"phase,omitempty"` // Pending, Running, Succeeded, Failed
|
||||
StartedAt *metav1.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
**RestoreJob contract with drivers**
|
||||
|
||||
* RestoreJob is created either manually or by core.
|
||||
* Driver controller:
|
||||
|
||||
1. Watches `RestoreJob`.
|
||||
2. On reconcile:
|
||||
|
||||
* Fetches the referenced `Backup`.
|
||||
* Determines effective:
|
||||
|
||||
* **Strategy**: `backup.spec.strategyRef`.
|
||||
* **Storage**: `backup.spec.storageRef`.
|
||||
* **Target application**: `spec.targetApplicationRef` or `backup.spec.applicationRef`.
|
||||
* If effective strategy’s GVK is one of its supported strategy types → driver is responsible.
|
||||
3. Behaviour:
|
||||
|
||||
* On first reconcile, set `status.startedAt` and `phase = Running`.
|
||||
* Resolve `Backup`, `Storage`, `Strategy`, target application.
|
||||
* Execute restore logic (implementation-specific).
|
||||
* On success:
|
||||
|
||||
* Set `status.completedAt`.
|
||||
* Set `status.phase = Succeeded`.
|
||||
* On failure:
|
||||
|
||||
* Set `status.completedAt`.
|
||||
* Set `status.phase = Failed`.
|
||||
* Set `status.message` and conditions.
|
||||
|
||||
Drivers must not modify `RestoreJob.spec` or delete `RestoreJob`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Strategy drivers (high-level)
|
||||
|
||||
Strategy drivers are separate controllers that:
|
||||
|
||||
* Define their own **strategy CRDs** (e.g. `JobBackupStrategy`) in their own API groups:
|
||||
|
||||
* e.g. `jobdriver.backups.cozystack.io/v1alpha1, Kind=JobBackupStrategy`
|
||||
* Implement the **BackupJob contract**:
|
||||
|
||||
* Watch `BackupJob`.
|
||||
* Filter by `spec.strategyRef.apiGroup/kind`.
|
||||
* Execute backup logic.
|
||||
* Create/update `Backup`.
|
||||
* Implement the **RestoreJob contract**:
|
||||
|
||||
* Watch `RestoreJob`.
|
||||
* Resolve `Backup`, then effective `strategyRef`.
|
||||
* Filter by effective strategy GVK.
|
||||
* Execute restore logic.
|
||||
|
||||
The core backups API **does not** dictate:
|
||||
|
||||
* The fields and structure of driver strategy specs.
|
||||
* How drivers implement backup/restore internally (Jobs, snapshots, native operator CRDs, etc.).
|
||||
|
||||
Drivers are interchangeable as long as they respect:
|
||||
|
||||
* The `BackupJob` and `RestoreJob` contracts.
|
||||
* The shapes and semantics of `Backup` objects.
|
||||
|
||||
---
|
||||
|
||||
## 6. Summary
|
||||
|
||||
The Cozystack backups core API:
|
||||
|
||||
* Uses a single group, `backups.cozystack.io`, for all core CRDs.
|
||||
* Cleanly separates:
|
||||
|
||||
* **When & where** (Plan + Storage) – core-owned.
|
||||
* **What backup artifacts exist** (Backup) – driver-created but cluster-visible.
|
||||
* **Execution lifecycle** (BackupJob, RestoreJob) – shared contract boundary.
|
||||
* Allows multiple strategy drivers to implement backup/restore logic without entangling their implementation with the core API.
|
||||
|
||||
118
api/backups/v1alpha1/backup_types.go
Normal file
118
api/backups/v1alpha1/backup_types.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines backups.cozystack.io API types.
|
||||
//
|
||||
// Group: backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&Backup{},
|
||||
&BackupList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// BackupPhase represents the lifecycle phase of a Backup.
|
||||
type BackupPhase string
|
||||
|
||||
const (
|
||||
BackupPhaseEmpty BackupPhase = ""
|
||||
BackupPhasePending BackupPhase = "Pending"
|
||||
BackupPhaseReady BackupPhase = "Ready"
|
||||
BackupPhaseFailed BackupPhase = "Failed"
|
||||
)
|
||||
|
||||
// BackupArtifact describes the stored backup object (tarball, snapshot, etc.).
|
||||
type BackupArtifact struct {
|
||||
// URI is a driver-/storage-specific URI pointing to the backup artifact.
|
||||
// For example: s3://bucket/prefix/file.tar.gz
|
||||
URI string `json:"uri"`
|
||||
|
||||
// SizeBytes is the size of the artifact in bytes, if known.
|
||||
// +optional
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
|
||||
// Checksum is the checksum of the artifact, if computed.
|
||||
// For example: "sha256:<hex>".
|
||||
// +optional
|
||||
Checksum string `json:"checksum,omitempty"`
|
||||
}
|
||||
|
||||
// BackupSpec describes an immutable backup artifact produced by a BackupJob.
|
||||
type BackupSpec struct {
|
||||
// ApplicationRef refers to the application that was backed up.
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
|
||||
// PlanRef refers to the Plan that produced this backup, if any.
|
||||
// For manually triggered backups, this can be omitted.
|
||||
// +optional
|
||||
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
|
||||
|
||||
// StorageRef refers to the Storage object that describes where the backup
|
||||
// artifact is stored.
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
|
||||
// StrategyRef refers to the driver-specific BackupStrategy that was used
|
||||
// to create this backup. This allows the driver to later perform restores.
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
|
||||
// TakenAt is the time at which the backup was taken (as reported by the
|
||||
// driver). It may differ slightly from metadata.creationTimestamp.
|
||||
TakenAt metav1.Time `json:"takenAt"`
|
||||
|
||||
// DriverMetadata holds driver-specific, opaque metadata associated with
|
||||
// this backup (for example snapshot IDs, schema versions, etc.).
|
||||
// This data is not interpreted by the core backup controllers.
|
||||
// +optional
|
||||
DriverMetadata map[string]string `json:"driverMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// BackupStatus represents the observed state of a Backup.
|
||||
type BackupStatus struct {
|
||||
// Phase is a simple, high-level summary of the backup's state.
|
||||
// Typical values are: Pending, Ready, Failed.
|
||||
// +optional
|
||||
Phase BackupPhase `json:"phase,omitempty"`
|
||||
|
||||
// Artifact describes the stored backup object, if available.
|
||||
// +optional
|
||||
Artifact *BackupArtifact `json:"artifact,omitempty"`
|
||||
|
||||
// Conditions represents the latest available observations of a Backup's state.
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// The field indexing on applicationRef will be needed later to display per-app backup resources.
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
|
||||
|
||||
// Backup represents a single backup artifact for a given application.
|
||||
type Backup struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec BackupSpec `json:"spec,omitempty"`
|
||||
Status BackupStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// BackupList contains a list of Backups.
|
||||
type BackupList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Backup `json:"items"`
|
||||
}
|
||||
116
api/backups/v1alpha1/backupjob_types.go
Normal file
116
api/backups/v1alpha1/backupjob_types.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines backups.cozystack.io API types.
|
||||
//
|
||||
// Group: backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&BackupJob{},
|
||||
&BackupJobList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
const (
|
||||
OwningJobNameLabel = thisGroup + "/owned-by.BackupJobName"
|
||||
OwningJobNamespaceLabel = thisGroup + "/owned-by.BackupJobNamespace"
|
||||
)
|
||||
|
||||
// BackupJobPhase represents the lifecycle phase of a BackupJob.
|
||||
type BackupJobPhase string
|
||||
|
||||
const (
|
||||
BackupJobPhaseEmpty BackupJobPhase = ""
|
||||
BackupJobPhasePending BackupJobPhase = "Pending"
|
||||
BackupJobPhaseRunning BackupJobPhase = "Running"
|
||||
BackupJobPhaseSucceeded BackupJobPhase = "Succeeded"
|
||||
BackupJobPhaseFailed BackupJobPhase = "Failed"
|
||||
)
|
||||
|
||||
// BackupJobSpec describes the execution of a single backup operation.
|
||||
type BackupJobSpec struct {
|
||||
// PlanRef refers to the Plan that requested this backup run.
|
||||
// For ad-hoc/manual backups, this can be omitted.
|
||||
// +optional
|
||||
PlanRef *corev1.LocalObjectReference `json:"planRef,omitempty"`
|
||||
|
||||
// ApplicationRef holds a reference to the managed application whose state
|
||||
// is being backed up.
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
|
||||
// StorageRef holds a reference to the Storage object that describes where
|
||||
// the backup will be stored.
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
|
||||
// StrategyRef holds a reference to the driver-specific BackupStrategy object
|
||||
// that describes how the backup should be created.
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
}
|
||||
|
||||
// BackupJobStatus represents the observed state of a BackupJob.
|
||||
type BackupJobStatus struct {
|
||||
// Phase is a high-level summary of the run's state.
|
||||
// Typical values: Pending, Running, Succeeded, Failed.
|
||||
// +optional
|
||||
Phase BackupJobPhase `json:"phase,omitempty"`
|
||||
|
||||
// BackupRef refers to the Backup object created by this run, if any.
|
||||
// +optional
|
||||
BackupRef *corev1.LocalObjectReference `json:"backupRef,omitempty"`
|
||||
|
||||
// StartedAt is the time at which the backup run started.
|
||||
// +optional
|
||||
StartedAt *metav1.Time `json:"startedAt,omitempty"`
|
||||
|
||||
// CompletedAt is the time at which the backup run completed (successfully
|
||||
// or otherwise).
|
||||
// +optional
|
||||
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
|
||||
|
||||
// Message is a human-readable message indicating details about why the
|
||||
// backup run is in its current phase, if any.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
// Conditions represents the latest available observations of a BackupJob's state.
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// The field indexing on applicationRef will be needed later to display per-app backup resources.
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",priority=0
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
|
||||
|
||||
// BackupJob represents a single execution of a backup.
|
||||
// It is typically created by a Plan controller when a schedule fires.
|
||||
type BackupJob struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec BackupJobSpec `json:"spec,omitempty"`
|
||||
Status BackupJobStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// BackupJobList contains a list of BackupJobs.
|
||||
type BackupJobList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []BackupJob `json:"items"`
|
||||
}
|
||||
42
api/backups/v1alpha1/groupversion_info.go
Normal file
42
api/backups/v1alpha1/groupversion_info.go
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
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 contains API Schema definitions for the v1alpha1 API group.
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=backups.cozystack.io
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
thisGroup = "backups.cozystack.io"
|
||||
thisVersion = "v1alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
GroupVersion = schema.GroupVersion{Group: thisGroup, Version: thisVersion}
|
||||
SchemeBuilder = runtime.NewSchemeBuilder(addGroupVersion)
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
func addGroupVersion(scheme *runtime.Scheme) error {
|
||||
metav1.AddToGroupVersion(scheme, GroupVersion)
|
||||
return nil
|
||||
}
|
||||
98
api/backups/v1alpha1/plan_types.go
Normal file
98
api/backups/v1alpha1/plan_types.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines backups.cozystack.io API types.
|
||||
//
|
||||
// Group: backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&Plan{},
|
||||
&PlanList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type PlanScheduleType string
|
||||
|
||||
const (
|
||||
PlanScheduleTypeEmpty PlanScheduleType = ""
|
||||
PlanScheduleTypeCron PlanScheduleType = "cron"
|
||||
)
|
||||
|
||||
// Condtions
|
||||
const (
|
||||
PlanConditionError = "Error"
|
||||
)
|
||||
|
||||
// The field indexing on applicationRef will be needed later to display per-app backup resources.
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.apiGroup`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.kind`
|
||||
// +kubebuilder:selectablefield:JSONPath=`.spec.applicationRef.name`
|
||||
|
||||
// Plan describes the schedule, method and storage location for the
|
||||
// backup of a given target application.
|
||||
type Plan struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec PlanSpec `json:"spec,omitempty"`
|
||||
Status PlanStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// PlanList contains a list of backup Plans.
|
||||
type PlanList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Plan `json:"items"`
|
||||
}
|
||||
|
||||
// PlanSpec references the storage, the strategy, the application to be
|
||||
// backed up and specifies the timetable on which the backups will run.
|
||||
type PlanSpec struct {
|
||||
// ApplicationRef holds a reference to the managed application,
|
||||
// whose state and configuration must be backed up.
|
||||
ApplicationRef corev1.TypedLocalObjectReference `json:"applicationRef"`
|
||||
|
||||
// StorageRef holds a reference to the Storage object that
|
||||
// describes the location where the backup will be stored.
|
||||
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`
|
||||
|
||||
// StrategyRef holds a reference to the Strategy object that
|
||||
// describes, how a backup copy is to be created.
|
||||
StrategyRef corev1.TypedLocalObjectReference `json:"strategyRef"`
|
||||
|
||||
// Schedule specifies when backup copies are created.
|
||||
Schedule PlanSchedule `json:"schedule"`
|
||||
}
|
||||
|
||||
// PlanSchedule specifies when backup copies are created.
|
||||
type PlanSchedule struct {
|
||||
// Type is the type of schedule specification. Supported values are
|
||||
// [`cron`]. If omitted, defaults to `cron`.
|
||||
// +optional
|
||||
Type PlanScheduleType `json:"type,omitempty"`
|
||||
|
||||
// Cron contains the cron spec for scheduling backups. Must be
|
||||
// specified if the schedule type is `cron`. Since only `cron` is
|
||||
// supported, omitting this field is not allowed.
|
||||
// +optional
|
||||
Cron string `json:"cron,omitempty"`
|
||||
}
|
||||
|
||||
type PlanStatus struct {
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
91
api/backups/v1alpha1/restorejob_types.go
Normal file
91
api/backups/v1alpha1/restorejob_types.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Package v1alpha1 defines backups.cozystack.io API types.
|
||||
//
|
||||
// Group: backups.cozystack.io
|
||||
// Version: v1alpha1
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(GroupVersion,
|
||||
&RestoreJob{},
|
||||
&RestoreJobList{},
|
||||
)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RestoreJobPhase represents the lifecycle phase of a RestoreJob.
|
||||
type RestoreJobPhase string
|
||||
|
||||
const (
|
||||
RestoreJobPhaseEmpty RestoreJobPhase = ""
|
||||
RestoreJobPhasePending RestoreJobPhase = "Pending"
|
||||
RestoreJobPhaseRunning RestoreJobPhase = "Running"
|
||||
RestoreJobPhaseSucceeded RestoreJobPhase = "Succeeded"
|
||||
RestoreJobPhaseFailed RestoreJobPhase = "Failed"
|
||||
)
|
||||
|
||||
// RestoreJobSpec describes the execution of a single restore operation.
|
||||
type RestoreJobSpec struct {
|
||||
// BackupRef refers to the Backup that should be restored.
|
||||
BackupRef corev1.LocalObjectReference `json:"backupRef"`
|
||||
|
||||
// TargetApplicationRef refers to the application into which the backup
|
||||
// should be restored. If omitted, the driver SHOULD restore into the same
|
||||
// application as referenced by backup.spec.applicationRef.
|
||||
// +optional
|
||||
TargetApplicationRef *corev1.TypedLocalObjectReference `json:"targetApplicationRef,omitempty"`
|
||||
}
|
||||
|
||||
// RestoreJobStatus represents the observed state of a RestoreJob.
|
||||
type RestoreJobStatus struct {
|
||||
// Phase is a high-level summary of the run's state.
|
||||
// Typical values: Pending, Running, Succeeded, Failed.
|
||||
// +optional
|
||||
Phase RestoreJobPhase `json:"phase,omitempty"`
|
||||
|
||||
// StartedAt is the time at which the restore run started.
|
||||
// +optional
|
||||
StartedAt *metav1.Time `json:"startedAt,omitempty"`
|
||||
|
||||
// CompletedAt is the time at which the restore run completed (successfully
|
||||
// or otherwise).
|
||||
// +optional
|
||||
CompletedAt *metav1.Time `json:"completedAt,omitempty"`
|
||||
|
||||
// Message is a human-readable message indicating details about why the
|
||||
// restore run is in its current phase, if any.
|
||||
// +optional
|
||||
Message string `json:"message,omitempty"`
|
||||
|
||||
// Conditions represents the latest available observations of a RestoreJob's state.
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// RestoreJob represents a single execution of a restore from a Backup.
|
||||
type RestoreJob struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec RestoreJobSpec `json:"spec,omitempty"`
|
||||
Status RestoreJobStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// RestoreJobList contains a list of RestoreJobs.
|
||||
type RestoreJobList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []RestoreJob `json:"items"`
|
||||
}
|
||||
501
api/backups/v1alpha1/zz_generated.deepcopy.go
Normal file
501
api/backups/v1alpha1/zz_generated.deepcopy.go
Normal file
@@ -0,0 +1,501 @@
|
||||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Backup) DeepCopyInto(out *Backup) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Backup.
|
||||
func (in *Backup) DeepCopy() *Backup {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Backup)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Backup) 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 *BackupArtifact) DeepCopyInto(out *BackupArtifact) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupArtifact.
|
||||
func (in *BackupArtifact) DeepCopy() *BackupArtifact {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupArtifact)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupJob) DeepCopyInto(out *BackupJob) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJob.
|
||||
func (in *BackupJob) DeepCopy() *BackupJob {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupJob)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BackupJob) 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 *BackupJobList) DeepCopyInto(out *BackupJobList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]BackupJob, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJobList.
|
||||
func (in *BackupJobList) DeepCopy() *BackupJobList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupJobList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BackupJobList) 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 *BackupJobSpec) DeepCopyInto(out *BackupJobSpec) {
|
||||
*out = *in
|
||||
if in.PlanRef != nil {
|
||||
in, out := &in.PlanRef, &out.PlanRef
|
||||
*out = new(v1.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
in.ApplicationRef.DeepCopyInto(&out.ApplicationRef)
|
||||
in.StorageRef.DeepCopyInto(&out.StorageRef)
|
||||
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJobSpec.
|
||||
func (in *BackupJobSpec) DeepCopy() *BackupJobSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupJobSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupJobStatus) DeepCopyInto(out *BackupJobStatus) {
|
||||
*out = *in
|
||||
if in.BackupRef != nil {
|
||||
in, out := &in.BackupRef, &out.BackupRef
|
||||
*out = new(v1.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.StartedAt != nil {
|
||||
in, out := &in.StartedAt, &out.StartedAt
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.CompletedAt != nil {
|
||||
in, out := &in.CompletedAt, &out.CompletedAt
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]metav1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupJobStatus.
|
||||
func (in *BackupJobStatus) DeepCopy() *BackupJobStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupJobStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupList) DeepCopyInto(out *BackupList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Backup, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupList.
|
||||
func (in *BackupList) DeepCopy() *BackupList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *BackupList) 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 *BackupSpec) DeepCopyInto(out *BackupSpec) {
|
||||
*out = *in
|
||||
in.ApplicationRef.DeepCopyInto(&out.ApplicationRef)
|
||||
if in.PlanRef != nil {
|
||||
in, out := &in.PlanRef, &out.PlanRef
|
||||
*out = new(v1.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
in.StorageRef.DeepCopyInto(&out.StorageRef)
|
||||
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
|
||||
in.TakenAt.DeepCopyInto(&out.TakenAt)
|
||||
if in.DriverMetadata != nil {
|
||||
in, out := &in.DriverMetadata, &out.DriverMetadata
|
||||
*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 BackupSpec.
|
||||
func (in *BackupSpec) DeepCopy() *BackupSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BackupStatus) DeepCopyInto(out *BackupStatus) {
|
||||
*out = *in
|
||||
if in.Artifact != nil {
|
||||
in, out := &in.Artifact, &out.Artifact
|
||||
*out = new(BackupArtifact)
|
||||
**out = **in
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]metav1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupStatus.
|
||||
func (in *BackupStatus) DeepCopy() *BackupStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(BackupStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Plan) DeepCopyInto(out *Plan) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Plan.
|
||||
func (in *Plan) DeepCopy() *Plan {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Plan)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Plan) 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 *PlanList) DeepCopyInto(out *PlanList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Plan, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanList.
|
||||
func (in *PlanList) DeepCopy() *PlanList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PlanList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *PlanList) 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 *PlanSchedule) DeepCopyInto(out *PlanSchedule) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanSchedule.
|
||||
func (in *PlanSchedule) DeepCopy() *PlanSchedule {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PlanSchedule)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PlanSpec) DeepCopyInto(out *PlanSpec) {
|
||||
*out = *in
|
||||
in.ApplicationRef.DeepCopyInto(&out.ApplicationRef)
|
||||
in.StorageRef.DeepCopyInto(&out.StorageRef)
|
||||
in.StrategyRef.DeepCopyInto(&out.StrategyRef)
|
||||
out.Schedule = in.Schedule
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanSpec.
|
||||
func (in *PlanSpec) DeepCopy() *PlanSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PlanSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PlanStatus) DeepCopyInto(out *PlanStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]metav1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PlanStatus.
|
||||
func (in *PlanStatus) DeepCopy() *PlanStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PlanStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RestoreJob) DeepCopyInto(out *RestoreJob) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJob.
|
||||
func (in *RestoreJob) DeepCopy() *RestoreJob {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RestoreJob)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *RestoreJob) 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 *RestoreJobList) DeepCopyInto(out *RestoreJobList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]RestoreJob, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJobList.
|
||||
func (in *RestoreJobList) DeepCopy() *RestoreJobList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RestoreJobList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *RestoreJobList) 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 *RestoreJobSpec) DeepCopyInto(out *RestoreJobSpec) {
|
||||
*out = *in
|
||||
out.BackupRef = in.BackupRef
|
||||
if in.TargetApplicationRef != nil {
|
||||
in, out := &in.TargetApplicationRef, &out.TargetApplicationRef
|
||||
*out = new(v1.TypedLocalObjectReference)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJobSpec.
|
||||
func (in *RestoreJobSpec) DeepCopy() *RestoreJobSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RestoreJobSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RestoreJobStatus) DeepCopyInto(out *RestoreJobStatus) {
|
||||
*out = *in
|
||||
if in.StartedAt != nil {
|
||||
in, out := &in.StartedAt, &out.StartedAt
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.CompletedAt != nil {
|
||||
in, out := &in.CompletedAt, &out.CompletedAt
|
||||
*out = (*in).DeepCopy()
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]metav1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJobStatus.
|
||||
func (in *RestoreJobStatus) DeepCopy() *RestoreJobStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RestoreJobStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
@@ -17,6 +17,7 @@ limitations under the License.
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
@@ -61,24 +62,6 @@ type CozystackResourceDefinitionSpec struct {
|
||||
Dashboard *CozystackResourceDefinitionDashboard `json:"dashboard,omitempty"`
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionChart struct {
|
||||
// Name of the Helm chart
|
||||
Name string `json:"name"`
|
||||
// Source reference for the Helm chart
|
||||
SourceRef SourceRef `json:"sourceRef"`
|
||||
}
|
||||
|
||||
type SourceRef struct {
|
||||
// Kind of the source reference
|
||||
// +kubebuilder:default:="HelmRepository"
|
||||
Kind string `json:"kind"`
|
||||
// Name of the source reference
|
||||
Name string `json:"name"`
|
||||
// Namespace of the source reference
|
||||
// +kubebuilder:default:="cozy-public"
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionApplication struct {
|
||||
// Kind of the application, used for UI and API
|
||||
Kind string `json:"kind"`
|
||||
@@ -91,8 +74,8 @@ type CozystackResourceDefinitionApplication struct {
|
||||
}
|
||||
|
||||
type CozystackResourceDefinitionRelease struct {
|
||||
// Helm chart configuration
|
||||
Chart CozystackResourceDefinitionChart `json:"chart"`
|
||||
// Reference to the chart source
|
||||
ChartRef *helmv2.CrossNamespaceSourceReference `json:"chartRef"`
|
||||
// Labels for the release
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
// Prefix for the release name
|
||||
@@ -110,17 +93,18 @@ type CozystackResourceDefinitionRelease struct {
|
||||
// - {{ .namespace }}: The namespace of the resource being processed
|
||||
//
|
||||
// Example YAML:
|
||||
// secrets:
|
||||
// include:
|
||||
// - matchExpressions:
|
||||
// - key: badlabel
|
||||
// operator: DoesNotExist
|
||||
// matchLabels:
|
||||
// goodlabel: goodvalue
|
||||
// resourceNames:
|
||||
// - "{{ .name }}-secret"
|
||||
// - "{{ .kind }}-{{ .name }}-tls"
|
||||
// - "specificname"
|
||||
//
|
||||
// secrets:
|
||||
// include:
|
||||
// - matchExpressions:
|
||||
// - key: badlabel
|
||||
// operator: DoesNotExist
|
||||
// matchLabels:
|
||||
// goodlabel: goodvalue
|
||||
// resourceNames:
|
||||
// - "{{ .name }}-secret"
|
||||
// - "{{ .kind }}-{{ .name }}-tls"
|
||||
// - "specificname"
|
||||
type CozystackResourceDefinitionResourceSelector struct {
|
||||
metav1.LabelSelector `json:",inline"`
|
||||
// ResourceNames is a list of resource names to match
|
||||
|
||||
100
api/v1alpha1/package_types.go
Normal file
100
api/v1alpha1/package_types.go
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Cluster,shortName={pkg,pkgs}
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Variant",type="string",JSONPath=".spec.variant",description="Selected variant"
|
||||
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Ready status"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message",description="Ready message"
|
||||
|
||||
// Package is the Schema for the packages API
|
||||
type Package struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec PackageSpec `json:"spec,omitempty"`
|
||||
Status PackageStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// PackageList contains a list of Packages
|
||||
type PackageList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []Package `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&Package{}, &PackageList{})
|
||||
}
|
||||
|
||||
// PackageSpec defines the desired state of Package
|
||||
type PackageSpec struct {
|
||||
// Variant is the name of the variant to use from the PackageSource
|
||||
// If not specified, defaults to "default"
|
||||
// +optional
|
||||
Variant string `json:"variant,omitempty"`
|
||||
|
||||
// IgnoreDependencies is a list of package source dependencies to ignore
|
||||
// Dependencies listed here will not be installed even if they are specified in the PackageSource
|
||||
// +optional
|
||||
IgnoreDependencies []string `json:"ignoreDependencies,omitempty"`
|
||||
|
||||
// Components is a map of release name to component overrides
|
||||
// Allows overriding values and enabling/disabling specific components from the PackageSource
|
||||
// +optional
|
||||
Components map[string]PackageComponent `json:"components,omitempty"`
|
||||
}
|
||||
|
||||
// PackageComponent defines overrides for a specific component
|
||||
type PackageComponent struct {
|
||||
// Enabled indicates whether this component should be installed
|
||||
// If false, the component will be disabled even if it's defined in the PackageSource
|
||||
// +optional
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
|
||||
// Values contains Helm chart values as a JSON object
|
||||
// These values will be merged with the default values from the PackageSource
|
||||
// +optional
|
||||
Values *apiextensionsv1.JSON `json:"values,omitempty"`
|
||||
}
|
||||
|
||||
// PackageStatus defines the observed state of Package
|
||||
type PackageStatus struct {
|
||||
// Conditions represents the latest available observations of a Package's state
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
|
||||
// Dependencies tracks the readiness status of each dependency
|
||||
// Key is the dependency package name, value indicates if the dependency is ready
|
||||
// +optional
|
||||
Dependencies map[string]DependencyStatus `json:"dependencies,omitempty"`
|
||||
}
|
||||
|
||||
// DependencyStatus represents the readiness status of a dependency
|
||||
type DependencyStatus struct {
|
||||
// Ready indicates whether the dependency is ready
|
||||
Ready bool `json:"ready"`
|
||||
}
|
||||
171
api/v1alpha1/packagesource_types.go
Normal file
171
api/v1alpha1/packagesource_types.go
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:resource:scope=Cluster,shortName={pks}
|
||||
// +kubebuilder:subresource:status
|
||||
// +kubebuilder:printcolumn:name="Variants",type="string",JSONPath=".status.variants",description="Package variants (comma-separated)"
|
||||
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="Ready status"
|
||||
// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message",description="Ready message"
|
||||
|
||||
// PackageSource is the Schema for the packagesources API
|
||||
type PackageSource struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec PackageSourceSpec `json:"spec,omitempty"`
|
||||
Status PackageSourceStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// PackageSourceList contains a list of PackageSources
|
||||
type PackageSourceList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []PackageSource `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&PackageSource{}, &PackageSourceList{})
|
||||
}
|
||||
|
||||
// PackageSourceSpec defines the desired state of PackageSource
|
||||
type PackageSourceSpec struct {
|
||||
// SourceRef is the source reference for the package source charts
|
||||
// +optional
|
||||
SourceRef *PackageSourceRef `json:"sourceRef,omitempty"`
|
||||
|
||||
// Variants is a list of package source variants
|
||||
// Each variant defines components, applications, dependencies, and libraries for a specific configuration
|
||||
// +optional
|
||||
Variants []Variant `json:"variants,omitempty"`
|
||||
}
|
||||
|
||||
// Variant defines a single variant configuration
|
||||
type Variant struct {
|
||||
// Name is the unique identifier for this variant
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// DependsOn is a list of package source dependencies
|
||||
// For example: "cozystack.networking"
|
||||
// +optional
|
||||
DependsOn []string `json:"dependsOn,omitempty"`
|
||||
|
||||
// Libraries is a list of Helm library charts used by components in this variant
|
||||
// +optional
|
||||
Libraries []Library `json:"libraries,omitempty"`
|
||||
|
||||
// Components is a list of Helm releases to be installed as part of this variant
|
||||
// +optional
|
||||
Components []Component `json:"components,omitempty"`
|
||||
}
|
||||
|
||||
// Library defines a Helm library chart
|
||||
type Library struct {
|
||||
// Name is the optional name for library placed in charts
|
||||
// +optional
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// Path is the path to the library chart directory
|
||||
// +required
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// PackageSourceRef defines the source reference for package source charts
|
||||
type PackageSourceRef struct {
|
||||
// Kind of the source reference
|
||||
// +kubebuilder:validation:Enum=GitRepository;OCIRepository
|
||||
// +required
|
||||
Kind string `json:"kind"`
|
||||
|
||||
// Name of the source reference
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// Namespace of the source reference
|
||||
// +required
|
||||
Namespace string `json:"namespace"`
|
||||
|
||||
// Path is the base path where packages are located in the source.
|
||||
// For GitRepository, defaults to "packages" if not specified.
|
||||
// For OCIRepository, defaults to empty string (root) if not specified.
|
||||
// +optional
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// ComponentInstall defines installation parameters for a component
|
||||
type ComponentInstall struct {
|
||||
// ReleaseName is the name of the HelmRelease resource that will be created
|
||||
// If not specified, defaults to the component Name field
|
||||
// +optional
|
||||
ReleaseName string `json:"releaseName,omitempty"`
|
||||
|
||||
// Namespace is the Kubernetes namespace where the release will be installed
|
||||
// +optional
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
|
||||
// Privileged indicates whether this release requires privileged access
|
||||
// +optional
|
||||
Privileged bool `json:"privileged,omitempty"`
|
||||
|
||||
// DependsOn is a list of component names that must be installed before this component
|
||||
// +optional
|
||||
DependsOn []string `json:"dependsOn,omitempty"`
|
||||
}
|
||||
|
||||
// Component defines a single Helm release component within a package source
|
||||
type Component struct {
|
||||
// Name is the unique identifier for this component within the package source
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// Path is the path to the Helm chart directory
|
||||
// +required
|
||||
Path string `json:"path"`
|
||||
|
||||
// Install defines installation parameters for this component
|
||||
// +optional
|
||||
Install *ComponentInstall `json:"install,omitempty"`
|
||||
|
||||
// Libraries is a list of library names that this component depends on
|
||||
// These libraries must be defined at the variant level
|
||||
// +optional
|
||||
Libraries []string `json:"libraries,omitempty"`
|
||||
|
||||
// ValuesFiles is a list of values file names to use
|
||||
// +optional
|
||||
ValuesFiles []string `json:"valuesFiles,omitempty"`
|
||||
}
|
||||
|
||||
// PackageSourceStatus defines the observed state of PackageSource
|
||||
type PackageSourceStatus struct {
|
||||
// Variants is a comma-separated list of package variant names
|
||||
// This field is populated by the controller based on spec.variants keys
|
||||
// +optional
|
||||
Variants string `json:"variants,omitempty"`
|
||||
|
||||
// Conditions represents the latest available observations of a PackageSource's state
|
||||
// +optional
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
@@ -21,10 +21,63 @@ limitations under the License.
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"github.com/fluxcd/helm-controller/api/v2"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Component) DeepCopyInto(out *Component) {
|
||||
*out = *in
|
||||
if in.Install != nil {
|
||||
in, out := &in.Install, &out.Install
|
||||
*out = new(ComponentInstall)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Libraries != nil {
|
||||
in, out := &in.Libraries, &out.Libraries
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.ValuesFiles != nil {
|
||||
in, out := &in.ValuesFiles, &out.ValuesFiles
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Component.
|
||||
func (in *Component) DeepCopy() *Component {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Component)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ComponentInstall) DeepCopyInto(out *ComponentInstall) {
|
||||
*out = *in
|
||||
if in.DependsOn != nil {
|
||||
in, out := &in.DependsOn, &out.DependsOn
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentInstall.
|
||||
func (in *ComponentInstall) DeepCopy() *ComponentInstall {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ComponentInstall)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinition) DeepCopyInto(out *CozystackResourceDefinition) {
|
||||
*out = *in
|
||||
@@ -66,22 +119,6 @@ func (in *CozystackResourceDefinitionApplication) DeepCopy() *CozystackResourceD
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionChart) DeepCopyInto(out *CozystackResourceDefinitionChart) {
|
||||
*out = *in
|
||||
out.SourceRef = in.SourceRef
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionChart.
|
||||
func (in *CozystackResourceDefinitionChart) DeepCopy() *CozystackResourceDefinitionChart {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CozystackResourceDefinitionChart)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionDashboard) DeepCopyInto(out *CozystackResourceDefinitionDashboard) {
|
||||
*out = *in
|
||||
@@ -153,7 +190,11 @@ func (in *CozystackResourceDefinitionList) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CozystackResourceDefinitionRelease) DeepCopyInto(out *CozystackResourceDefinitionRelease) {
|
||||
*out = *in
|
||||
out.Chart = in.Chart
|
||||
if in.ChartRef != nil {
|
||||
in, out := &in.ChartRef, &out.ChartRef
|
||||
*out = new(v2.CrossNamespaceSourceReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
@@ -256,6 +297,299 @@ func (in *CozystackResourceDefinitionSpec) DeepCopy() *CozystackResourceDefiniti
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DependencyStatus) DeepCopyInto(out *DependencyStatus) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyStatus.
|
||||
func (in *DependencyStatus) DeepCopy() *DependencyStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DependencyStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Library) DeepCopyInto(out *Library) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Library.
|
||||
func (in *Library) DeepCopy() *Library {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Library)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Package) DeepCopyInto(out *Package) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Package.
|
||||
func (in *Package) DeepCopy() *Package {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Package)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *Package) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageComponent) DeepCopyInto(out *PackageComponent) {
|
||||
*out = *in
|
||||
if in.Enabled != nil {
|
||||
in, out := &in.Enabled, &out.Enabled
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.Values != nil {
|
||||
in, out := &in.Values, &out.Values
|
||||
*out = new(v1.JSON)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageComponent.
|
||||
func (in *PackageComponent) DeepCopy() *PackageComponent {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageComponent)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageList) DeepCopyInto(out *PackageList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Package, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageList.
|
||||
func (in *PackageList) DeepCopy() *PackageList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *PackageList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageSource) DeepCopyInto(out *PackageSource) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSource.
|
||||
func (in *PackageSource) DeepCopy() *PackageSource {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSource)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *PackageSource) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageSourceList) DeepCopyInto(out *PackageSourceList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]PackageSource, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceList.
|
||||
func (in *PackageSourceList) DeepCopy() *PackageSourceList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSourceList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *PackageSourceList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageSourceRef) DeepCopyInto(out *PackageSourceRef) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceRef.
|
||||
func (in *PackageSourceRef) DeepCopy() *PackageSourceRef {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSourceRef)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageSourceSpec) DeepCopyInto(out *PackageSourceSpec) {
|
||||
*out = *in
|
||||
if in.SourceRef != nil {
|
||||
in, out := &in.SourceRef, &out.SourceRef
|
||||
*out = new(PackageSourceRef)
|
||||
**out = **in
|
||||
}
|
||||
if in.Variants != nil {
|
||||
in, out := &in.Variants, &out.Variants
|
||||
*out = make([]Variant, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceSpec.
|
||||
func (in *PackageSourceSpec) DeepCopy() *PackageSourceSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSourceSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageSourceStatus) DeepCopyInto(out *PackageSourceStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]metav1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSourceStatus.
|
||||
func (in *PackageSourceStatus) DeepCopy() *PackageSourceStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSourceStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageSpec) DeepCopyInto(out *PackageSpec) {
|
||||
*out = *in
|
||||
if in.IgnoreDependencies != nil {
|
||||
in, out := &in.IgnoreDependencies, &out.IgnoreDependencies
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Components != nil {
|
||||
in, out := &in.Components, &out.Components
|
||||
*out = make(map[string]PackageComponent, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = *val.DeepCopy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSpec.
|
||||
func (in *PackageSpec) DeepCopy() *PackageSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *PackageStatus) DeepCopyInto(out *PackageStatus) {
|
||||
*out = *in
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]metav1.Condition, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Dependencies != nil {
|
||||
in, out := &in.Dependencies, &out.Dependencies
|
||||
*out = make(map[string]DependencyStatus, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageStatus.
|
||||
func (in *PackageStatus) DeepCopy() *PackageStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(PackageStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in Selector) DeepCopyInto(out *Selector) {
|
||||
{
|
||||
@@ -278,16 +612,33 @@ func (in Selector) DeepCopy() Selector {
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SourceRef) DeepCopyInto(out *SourceRef) {
|
||||
func (in *Variant) DeepCopyInto(out *Variant) {
|
||||
*out = *in
|
||||
if in.DependsOn != nil {
|
||||
in, out := &in.DependsOn, &out.DependsOn
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Libraries != nil {
|
||||
in, out := &in.Libraries, &out.Libraries
|
||||
*out = make([]Library, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Components != nil {
|
||||
in, out := &in.Components, &out.Components
|
||||
*out = make([]Component, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceRef.
|
||||
func (in *SourceRef) DeepCopy() *SourceRef {
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Variant.
|
||||
func (in *Variant) DeepCopy() *Variant {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SourceRef)
|
||||
out := new(Variant)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
187
cmd/backup-controller/main.go
Normal file
187
cmd/backup-controller/main.go
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/backupcontroller"
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(backupsv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(strategyv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(velerov1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
opts := zap.Options{
|
||||
Development: false,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||
// Rapid Reset CVEs. For more information see:
|
||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||
disableHTTP2 := func(c *tls.Config) {
|
||||
setupLog.Info("disabling http/2")
|
||||
c.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
if !enableHTTP2 {
|
||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||
}
|
||||
|
||||
webhookServer := webhook.NewServer(webhook.Options{
|
||||
TLSOpts: tlsOpts,
|
||||
})
|
||||
|
||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||
// More info:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
|
||||
// - https://book.kubebuilder.io/reference/metrics.html
|
||||
metricsServerOptions := metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
TLSOpts: tlsOpts,
|
||||
}
|
||||
|
||||
if secureMetrics {
|
||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||
// These configurations ensure that only authorized users and service accounts
|
||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||
|
||||
// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
|
||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||
// this setup is not recommended for production.
|
||||
}
|
||||
|
||||
// Configure rate limiting for the Kubernetes client
|
||||
config := ctrl.GetConfigOrDie()
|
||||
config.QPS = 50.0 // Increased from default 5.0
|
||||
config.Burst = 100 // Increased from default 10
|
||||
|
||||
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsServerOptions,
|
||||
WebhookServer: webhookServer,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "core.backups.cozystack.io",
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||
// speeds up voluntary leader transitions as the new leader don't have to wait
|
||||
// LeaseDuration time first.
|
||||
//
|
||||
// In the default scaffold provided, the program ends immediately after
|
||||
// the manager stops, so would be fine to enable this option. However,
|
||||
// if you are doing or is intended to do any operation such as perform cleanups
|
||||
// after the manager stops then its usage might be unsafe.
|
||||
// LeaderElectionReleaseOnCancel: true,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&backupcontroller.PlanReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Plan")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&backupcontroller.BackupJobReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Recorder: mgr.GetEventRecorderFor("backup-controller"),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "BackupJob")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
174
cmd/backupstrategy-controller/main.go
Normal file
174
cmd/backupstrategy-controller/main.go
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/backupcontroller"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(backupsv1alpha1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
opts := zap.Options{
|
||||
Development: false,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||
// Rapid Reset CVEs. For more information see:
|
||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||
disableHTTP2 := func(c *tls.Config) {
|
||||
setupLog.Info("disabling http/2")
|
||||
c.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
if !enableHTTP2 {
|
||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||
}
|
||||
|
||||
webhookServer := webhook.NewServer(webhook.Options{
|
||||
TLSOpts: tlsOpts,
|
||||
})
|
||||
|
||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||
// More info:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
|
||||
// - https://book.kubebuilder.io/reference/metrics.html
|
||||
metricsServerOptions := metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
TLSOpts: tlsOpts,
|
||||
}
|
||||
|
||||
if secureMetrics {
|
||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||
// These configurations ensure that only authorized users and service accounts
|
||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||
|
||||
// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
|
||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||
// this setup is not recommended for production.
|
||||
}
|
||||
|
||||
// Configure rate limiting for the Kubernetes client
|
||||
config := ctrl.GetConfigOrDie()
|
||||
config.QPS = 50.0 // Increased from default 5.0
|
||||
config.Burst = 100 // Increased from default 10
|
||||
|
||||
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsServerOptions,
|
||||
WebhookServer: webhookServer,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "strategy.backups.cozystack.io",
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||
// speeds up voluntary leader transitions as the new leader don't have to wait
|
||||
// LeaseDuration time first.
|
||||
//
|
||||
// In the default scaffold provided, the program ends immediately after
|
||||
// the manager stops, so would be fine to enable this option. However,
|
||||
// if you are doing or is intended to do any operation such as perform cleanups
|
||||
// after the manager stops then its usage might be unsafe.
|
||||
// LeaderElectionReleaseOnCancel: true,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&backupcontroller.BackupJobReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Job")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
549
cmd/cozypkg/cmd/add.go
Normal file
549
cmd/cozypkg/cmd/add.go
Normal file
@@ -0,0 +1,549 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
var addCmdFlags struct {
|
||||
files []string
|
||||
kubeconfig string
|
||||
}
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add [package]...",
|
||||
Short: "Install PackageSource and its dependencies interactively",
|
||||
Long: `Install PackageSource and its dependencies interactively.
|
||||
|
||||
You can specify packages as arguments or use -f flag to read from files.
|
||||
Multiple -f flags can be specified, and they can point to files or directories.`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Collect package names from arguments and files
|
||||
packageNames := make(map[string]bool)
|
||||
packagesFromFiles := make(map[string]string) // packageName -> filePath
|
||||
|
||||
for _, arg := range args {
|
||||
packageNames[arg] = true
|
||||
}
|
||||
|
||||
// Read packages from files
|
||||
for _, filePath := range addCmdFlags.files {
|
||||
packages, err := readPackagesFromFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
packageNames[pkg] = true
|
||||
if oldPath, ok := packagesFromFiles[pkg]; ok {
|
||||
fmt.Fprintf(os.Stderr, "warning: package %q is defined in both %s and %s, using the latter\n", pkg, oldPath, filePath)
|
||||
}
|
||||
packagesFromFiles[pkg] = filePath
|
||||
}
|
||||
}
|
||||
|
||||
if len(packageNames) == 0 {
|
||||
return fmt.Errorf("no packages specified")
|
||||
}
|
||||
|
||||
// Create Kubernetes client config
|
||||
var config *rest.Config
|
||||
var err error
|
||||
|
||||
if addCmdFlags.kubeconfig != "" {
|
||||
config, err = clientcmd.BuildConfigFromFlags("", addCmdFlags.kubeconfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load kubeconfig from %s: %w", addCmdFlags.kubeconfig, err)
|
||||
}
|
||||
} else {
|
||||
config, err = ctrl.GetConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
|
||||
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create k8s client: %w", err)
|
||||
}
|
||||
|
||||
// Process each package
|
||||
for packageName := range packageNames {
|
||||
// Check if package comes from a file
|
||||
if filePath, fromFile := packagesFromFiles[packageName]; fromFile {
|
||||
// Try to create Package directly from file
|
||||
if err := createPackageFromFile(ctx, k8sClient, filePath, packageName); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "✓ Added Package %s\n", packageName)
|
||||
continue
|
||||
}
|
||||
// If failed, fall back to interactive installation
|
||||
}
|
||||
|
||||
// Interactive installation from PackageSource
|
||||
if err := installPackage(ctx, k8sClient, packageName); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func readPackagesFromFile(filePath string) ([]string, error) {
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var packages []string
|
||||
|
||||
if info.IsDir() {
|
||||
// Read all YAML files from directory
|
||||
err := filepath.Walk(filePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() || !strings.HasSuffix(path, ".yaml") && !strings.HasSuffix(path, ".yml") {
|
||||
return nil
|
||||
}
|
||||
|
||||
pkgs, err := readPackagesFromYAMLFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read %s: %w", path, err)
|
||||
}
|
||||
packages = append(packages, pkgs...)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
packages, err = readPackagesFromYAMLFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
func readPackagesFromYAMLFile(filePath string) ([]string, error) {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var packages []string
|
||||
|
||||
// Split YAML documents (in case of multiple resources)
|
||||
documents := strings.Split(string(data), "---")
|
||||
|
||||
for _, doc := range documents {
|
||||
doc = strings.TrimSpace(doc)
|
||||
if doc == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse using Kubernetes decoder
|
||||
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
||||
obj := &unstructured.Unstructured{}
|
||||
_, _, err := decoder.Decode([]byte(doc), nil, obj)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a Package
|
||||
if obj.GetKind() == "Package" {
|
||||
name := obj.GetName()
|
||||
if name != "" {
|
||||
packages = append(packages, name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a PackageSource
|
||||
if obj.GetKind() == "PackageSource" {
|
||||
name := obj.GetName()
|
||||
if name != "" {
|
||||
packages = append(packages, name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to parse as PackageList or PackageSourceList
|
||||
if obj.GetKind() == "PackageList" || obj.GetKind() == "PackageSourceList" {
|
||||
items, found, err := unstructured.NestedSlice(obj.Object, "items")
|
||||
if err == nil && found {
|
||||
for _, item := range items {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
if metadata, ok := itemMap["metadata"].(map[string]interface{}); ok {
|
||||
if name, ok := metadata["name"].(string); ok && name != "" {
|
||||
packages = append(packages, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Return empty list if no packages found - don't error out
|
||||
// The check for whether any packages were specified at all is handled later in RunE
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
// buildDependencyTree builds a dependency tree starting from the root PackageSource
|
||||
// Returns both the dependency tree and a map of dependencies to their requesters
|
||||
func buildDependencyTree(ctx context.Context, k8sClient client.Client, rootName string) (map[string][]string, map[string]string, error) {
|
||||
tree := make(map[string][]string)
|
||||
dependencyRequesters := make(map[string]string) // dep -> requester
|
||||
visited := make(map[string]bool)
|
||||
|
||||
// Ensure root is in tree even if it has no dependencies
|
||||
tree[rootName] = []string{}
|
||||
|
||||
var buildTree func(string) error
|
||||
buildTree = func(pkgName string) error {
|
||||
if visited[pkgName] {
|
||||
return nil
|
||||
}
|
||||
visited[pkgName] = true
|
||||
|
||||
// Get PackageSource
|
||||
ps := &cozyv1alpha1.PackageSource{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: pkgName}, ps); err != nil {
|
||||
// If PackageSource doesn't exist, just skip it
|
||||
return nil
|
||||
}
|
||||
|
||||
// Collect all dependencies from all variants
|
||||
deps := make(map[string]bool)
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
for _, dep := range variant.DependsOn {
|
||||
deps[dep] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add dependencies to tree
|
||||
for dep := range deps {
|
||||
if _, exists := tree[pkgName]; !exists {
|
||||
tree[pkgName] = []string{}
|
||||
}
|
||||
tree[pkgName] = append(tree[pkgName], dep)
|
||||
// Track who requested this dependency
|
||||
dependencyRequesters[dep] = pkgName
|
||||
// Recursively build tree for dependencies
|
||||
if err := buildTree(dep); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := buildTree(rootName); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return tree, dependencyRequesters, nil
|
||||
}
|
||||
|
||||
// topologicalSort performs topological sort on the dependency tree
|
||||
// Returns order from root to leaves (dependencies first)
|
||||
func topologicalSort(tree map[string][]string) ([]string, error) {
|
||||
// Build reverse graph (dependencies -> dependents)
|
||||
reverseGraph := make(map[string][]string)
|
||||
allNodes := make(map[string]bool)
|
||||
|
||||
for node, deps := range tree {
|
||||
allNodes[node] = true
|
||||
for _, dep := range deps {
|
||||
allNodes[dep] = true
|
||||
reverseGraph[dep] = append(reverseGraph[dep], node)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate in-degrees (how many dependencies a node has)
|
||||
inDegree := make(map[string]int)
|
||||
for node := range allNodes {
|
||||
inDegree[node] = 0
|
||||
}
|
||||
for node, deps := range tree {
|
||||
inDegree[node] = len(deps)
|
||||
}
|
||||
|
||||
// Kahn's algorithm - start with nodes that have no dependencies
|
||||
var queue []string
|
||||
for node, degree := range inDegree {
|
||||
if degree == 0 {
|
||||
queue = append(queue, node)
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for len(queue) > 0 {
|
||||
node := queue[0]
|
||||
queue = queue[1:]
|
||||
result = append(result, node)
|
||||
|
||||
// Process dependents (nodes that depend on this node)
|
||||
for _, dependent := range reverseGraph[node] {
|
||||
inDegree[dependent]--
|
||||
if inDegree[dependent] == 0 {
|
||||
queue = append(queue, dependent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for cycles
|
||||
if len(result) != len(allNodes) {
|
||||
return nil, fmt.Errorf("dependency cycle detected")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// createPackageFromFile creates a Package resource directly from a YAML file
|
||||
func createPackageFromFile(ctx context.Context, k8sClient client.Client, filePath string, packageName string) error {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Split YAML documents
|
||||
documents := strings.Split(string(data), "---")
|
||||
|
||||
for _, doc := range documents {
|
||||
doc = strings.TrimSpace(doc)
|
||||
if doc == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse using Kubernetes decoder
|
||||
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
||||
obj := &unstructured.Unstructured{}
|
||||
_, _, err := decoder.Decode([]byte(doc), nil, obj)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if it's a Package with matching name
|
||||
if obj.GetKind() == "Package" && obj.GetName() == packageName {
|
||||
// Convert to Package
|
||||
var pkg cozyv1alpha1.Package
|
||||
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &pkg); err != nil {
|
||||
return fmt.Errorf("failed to convert Package: %w", err)
|
||||
}
|
||||
|
||||
// Create Package
|
||||
if err := k8sClient.Create(ctx, &pkg); err != nil {
|
||||
return fmt.Errorf("failed to create Package: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("Package %s not found in file", packageName)
|
||||
}
|
||||
|
||||
func installPackage(ctx context.Context, k8sClient client.Client, packageSourceName string) error {
|
||||
// Get PackageSource
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: packageSourceName}, packageSource); err != nil {
|
||||
return fmt.Errorf("failed to get PackageSource %s: %w", packageSourceName, err)
|
||||
}
|
||||
|
||||
// Build dependency tree
|
||||
dependencyTree, dependencyRequesters, err := buildDependencyTree(ctx, k8sClient, packageSourceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build dependency tree: %w", err)
|
||||
}
|
||||
|
||||
// Topological sort (install from root to leaves)
|
||||
installOrder, err := topologicalSort(dependencyTree)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sort dependencies: %w", err)
|
||||
}
|
||||
|
||||
// Get all PackageSources for variant selection
|
||||
var allPackageSources cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &allPackageSources); err != nil {
|
||||
return fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
|
||||
packageSourceMap := make(map[string]*cozyv1alpha1.PackageSource)
|
||||
for i := range allPackageSources.Items {
|
||||
packageSourceMap[allPackageSources.Items[i].Name] = &allPackageSources.Items[i]
|
||||
}
|
||||
|
||||
// Get all installed Packages
|
||||
var installedPackages cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &installedPackages); err != nil {
|
||||
return fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
|
||||
installedMap := make(map[string]*cozyv1alpha1.Package)
|
||||
for i := range installedPackages.Items {
|
||||
installedMap[installedPackages.Items[i].Name] = &installedPackages.Items[i]
|
||||
}
|
||||
|
||||
// First, collect all variant selections
|
||||
fmt.Fprintf(os.Stderr, "Installing %s and its dependencies...\n\n", packageSourceName)
|
||||
packageVariants := make(map[string]string) // packageName -> variant
|
||||
|
||||
for _, pkgName := range installOrder {
|
||||
// Check if already installed
|
||||
if installed, exists := installedMap[pkgName]; exists {
|
||||
variant := installed.Spec.Variant
|
||||
if variant == "" {
|
||||
variant = "default"
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "✓ %s (already installed, variant: %s)\n", pkgName, variant)
|
||||
packageVariants[pkgName] = variant
|
||||
continue
|
||||
}
|
||||
|
||||
// Get PackageSource for this dependency
|
||||
ps, exists := packageSourceMap[pkgName]
|
||||
if !exists {
|
||||
requester := dependencyRequesters[pkgName]
|
||||
if requester != "" {
|
||||
return fmt.Errorf("PackageSource %s not found (required by %s)", pkgName, requester)
|
||||
}
|
||||
return fmt.Errorf("PackageSource %s not found", pkgName)
|
||||
}
|
||||
|
||||
// Select variant interactively
|
||||
variant, err := selectVariantInteractive(ps)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to select variant for %s: %w", pkgName, err)
|
||||
}
|
||||
|
||||
packageVariants[pkgName] = variant
|
||||
}
|
||||
|
||||
// Now create all Package resources
|
||||
for _, pkgName := range installOrder {
|
||||
// Skip if already installed
|
||||
if _, exists := installedMap[pkgName]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
variant := packageVariants[pkgName]
|
||||
|
||||
// Create Package
|
||||
pkg := &cozyv1alpha1.Package{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: pkgName,
|
||||
},
|
||||
Spec: cozyv1alpha1.PackageSpec{
|
||||
Variant: variant,
|
||||
},
|
||||
}
|
||||
|
||||
if err := k8sClient.Create(ctx, pkg); err != nil {
|
||||
return fmt.Errorf("failed to create Package %s: %w", pkgName, err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "✓ Added Package %s\n", pkgName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectVariantInteractive prompts user to select a variant
|
||||
func selectVariantInteractive(ps *cozyv1alpha1.PackageSource) (string, error) {
|
||||
if len(ps.Spec.Variants) == 0 {
|
||||
return "", fmt.Errorf("no variants available for PackageSource %s", ps.Name)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\nPackageSource: %s\n", ps.Name)
|
||||
fmt.Fprintf(os.Stderr, "Available variants:\n")
|
||||
for i, variant := range ps.Spec.Variants {
|
||||
fmt.Fprintf(os.Stderr, " %d. %s\n", i+1, variant.Name)
|
||||
}
|
||||
|
||||
// If only one variant, use it as default
|
||||
defaultVariant := ps.Spec.Variants[0].Name
|
||||
var prompt string
|
||||
if len(ps.Spec.Variants) == 1 {
|
||||
prompt = "Select variant [1]: "
|
||||
} else {
|
||||
prompt = fmt.Sprintf("Select variant (1-%d): ", len(ps.Spec.Variants))
|
||||
}
|
||||
|
||||
for {
|
||||
fmt.Fprintf(os.Stderr, prompt)
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read input: %w", err)
|
||||
}
|
||||
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
// If input is empty and there's a default variant, use it
|
||||
if input == "" && len(ps.Spec.Variants) == 1 {
|
||||
return defaultVariant, nil
|
||||
}
|
||||
|
||||
choice, err := strconv.Atoi(input)
|
||||
if err != nil || choice < 1 || choice > len(ps.Spec.Variants) {
|
||||
fmt.Fprintf(os.Stderr, "Invalid choice. Please enter a number between 1 and %d.\n", len(ps.Spec.Variants))
|
||||
continue
|
||||
}
|
||||
|
||||
return ps.Spec.Variants[choice-1].Name, nil
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(addCmd)
|
||||
addCmd.Flags().StringArrayVarP(&addCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
|
||||
addCmd.Flags().StringVar(&addCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
|
||||
}
|
||||
|
||||
396
cmd/cozypkg/cmd/del.go
Normal file
396
cmd/cozypkg/cmd/del.go
Normal file
@@ -0,0 +1,396 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
var delCmdFlags struct {
|
||||
files []string
|
||||
kubeconfig string
|
||||
}
|
||||
|
||||
var delCmd = &cobra.Command{
|
||||
Use: "del [package]...",
|
||||
Short: "Delete Package resources",
|
||||
Long: `Delete Package resources.
|
||||
|
||||
You can specify packages as arguments or use -f flag to read from files.
|
||||
Multiple -f flags can be specified, and they can point to files or directories.`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Collect package names from arguments and files
|
||||
packageNames := make(map[string]bool)
|
||||
packagesFromFiles := make(map[string]string) // packageName -> filePath
|
||||
|
||||
for _, arg := range args {
|
||||
packageNames[arg] = true
|
||||
}
|
||||
|
||||
// Read packages from files (reuse function from add.go)
|
||||
for _, filePath := range delCmdFlags.files {
|
||||
packages, err := readPackagesFromFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
packageNames[pkg] = true
|
||||
if oldPath, ok := packagesFromFiles[pkg]; ok {
|
||||
fmt.Fprintf(os.Stderr, "warning: package %q is defined in both %s and %s, using the latter\n", pkg, oldPath, filePath)
|
||||
}
|
||||
packagesFromFiles[pkg] = filePath
|
||||
}
|
||||
}
|
||||
|
||||
if len(packageNames) == 0 {
|
||||
return fmt.Errorf("no packages specified")
|
||||
}
|
||||
|
||||
// Create Kubernetes client config
|
||||
var config *rest.Config
|
||||
var err error
|
||||
|
||||
if delCmdFlags.kubeconfig != "" {
|
||||
config, err = clientcmd.BuildConfigFromFlags("", delCmdFlags.kubeconfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load kubeconfig from %s: %w", delCmdFlags.kubeconfig, err)
|
||||
}
|
||||
} else {
|
||||
config, err = ctrl.GetConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
|
||||
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create k8s client: %w", err)
|
||||
}
|
||||
|
||||
// Check which requested packages are installed
|
||||
var installedPackages cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &installedPackages); err != nil {
|
||||
return fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
installedMap := make(map[string]bool)
|
||||
for _, pkg := range installedPackages.Items {
|
||||
installedMap[pkg.Name] = true
|
||||
}
|
||||
|
||||
// Warn about requested packages that are not installed
|
||||
for pkgName := range packageNames {
|
||||
if !installedMap[pkgName] {
|
||||
fmt.Fprintf(os.Stderr, "⚠ Package %s is not installed, skipping\n", pkgName)
|
||||
}
|
||||
}
|
||||
|
||||
// Find all packages to delete (including dependents)
|
||||
packagesToDelete, err := findPackagesToDelete(ctx, k8sClient, packageNames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to analyze dependencies: %w", err)
|
||||
}
|
||||
|
||||
if len(packagesToDelete) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "No packages found to delete\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Show packages to be deleted and ask for confirmation
|
||||
if err := confirmDeletion(packagesToDelete, packageNames); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete packages in reverse topological order (dependents first, then dependencies)
|
||||
deleteOrder, err := getDeleteOrder(ctx, k8sClient, packagesToDelete)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine delete order: %w", err)
|
||||
}
|
||||
|
||||
// Delete each package
|
||||
for _, packageName := range deleteOrder {
|
||||
pkg := &cozyv1alpha1.Package{}
|
||||
pkg.Name = packageName
|
||||
if err := k8sClient.Delete(ctx, pkg); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
fmt.Fprintf(os.Stderr, "⚠ Package %s not found, skipping\n", packageName)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to delete Package %s: %w", packageName, err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "✓ Deleted Package %s\n", packageName)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// findPackagesToDelete finds all packages that need to be deleted, including dependents
|
||||
func findPackagesToDelete(ctx context.Context, k8sClient client.Client, requestedPackages map[string]bool) (map[string]bool, error) {
|
||||
// Get all installed Packages
|
||||
var installedPackages cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &installedPackages); err != nil {
|
||||
return nil, fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
|
||||
installedMap := make(map[string]bool)
|
||||
for _, pkg := range installedPackages.Items {
|
||||
installedMap[pkg.Name] = true
|
||||
}
|
||||
|
||||
// Get all PackageSources to build dependency graph
|
||||
var packageSources cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &packageSources); err != nil {
|
||||
return nil, fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
|
||||
// Build reverse dependency graph (dependents -> dependencies)
|
||||
// This tells us: for each package, which packages depend on it
|
||||
reverseDeps := make(map[string][]string)
|
||||
for _, ps := range packageSources.Items {
|
||||
// Only consider installed packages
|
||||
if !installedMap[ps.Name] {
|
||||
continue
|
||||
}
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
for _, dep := range variant.DependsOn {
|
||||
// Only consider installed dependencies
|
||||
if installedMap[dep] {
|
||||
reverseDeps[dep] = append(reverseDeps[dep], ps.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find all packages to delete (requested + their dependents)
|
||||
packagesToDelete := make(map[string]bool)
|
||||
visited := make(map[string]bool)
|
||||
|
||||
var findDependents func(string)
|
||||
findDependents = func(pkgName string) {
|
||||
if visited[pkgName] {
|
||||
return
|
||||
}
|
||||
visited[pkgName] = true
|
||||
|
||||
// Only add if it's installed
|
||||
if installedMap[pkgName] {
|
||||
packagesToDelete[pkgName] = true
|
||||
}
|
||||
|
||||
// Recursively find all dependents
|
||||
for _, dependent := range reverseDeps[pkgName] {
|
||||
if installedMap[dependent] {
|
||||
findDependents(dependent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start from requested packages
|
||||
for pkgName := range requestedPackages {
|
||||
if !installedMap[pkgName] {
|
||||
continue
|
||||
}
|
||||
findDependents(pkgName)
|
||||
}
|
||||
|
||||
return packagesToDelete, nil
|
||||
}
|
||||
|
||||
// confirmDeletion shows the list of packages to be deleted and asks for user confirmation
|
||||
func confirmDeletion(packagesToDelete map[string]bool, requestedPackages map[string]bool) error {
|
||||
// Separate requested packages from dependents
|
||||
var requested []string
|
||||
var dependents []string
|
||||
|
||||
for pkg := range packagesToDelete {
|
||||
if requestedPackages[pkg] {
|
||||
requested = append(requested, pkg)
|
||||
} else {
|
||||
dependents = append(dependents, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "\nThe following packages will be deleted:\n\n")
|
||||
|
||||
if len(requested) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Requested packages:\n")
|
||||
for _, pkg := range requested {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
}
|
||||
|
||||
if len(dependents) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Dependent packages (will also be deleted):\n")
|
||||
for _, pkg := range dependents {
|
||||
fmt.Fprintf(os.Stderr, " - %s\n", pkg)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Total: %d package(s)\n\n", len(packagesToDelete))
|
||||
fmt.Fprintf(os.Stderr, "Do you want to continue? [y/N]: ")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read input: %w", err)
|
||||
}
|
||||
|
||||
input = strings.TrimSpace(strings.ToLower(input))
|
||||
if input != "y" && input != "yes" {
|
||||
return fmt.Errorf("deletion cancelled")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDeleteOrder returns packages in reverse topological order (dependents first, then dependencies)
|
||||
// This ensures we delete dependents before their dependencies
|
||||
func getDeleteOrder(ctx context.Context, k8sClient client.Client, packagesToDelete map[string]bool) ([]string, error) {
|
||||
// Get all PackageSources to build dependency graph
|
||||
var packageSources cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &packageSources); err != nil {
|
||||
return nil, fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
|
||||
// Build forward dependency graph (package -> dependencies)
|
||||
dependencyGraph := make(map[string][]string)
|
||||
for _, ps := range packageSources.Items {
|
||||
if !packagesToDelete[ps.Name] {
|
||||
continue
|
||||
}
|
||||
deps := make(map[string]bool)
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
for _, dep := range variant.DependsOn {
|
||||
if packagesToDelete[dep] {
|
||||
deps[dep] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
var depList []string
|
||||
for dep := range deps {
|
||||
depList = append(depList, dep)
|
||||
}
|
||||
dependencyGraph[ps.Name] = depList
|
||||
}
|
||||
|
||||
// Build reverse graph for topological sort
|
||||
reverseGraph := make(map[string][]string)
|
||||
allNodes := make(map[string]bool)
|
||||
|
||||
for node, deps := range dependencyGraph {
|
||||
allNodes[node] = true
|
||||
for _, dep := range deps {
|
||||
allNodes[dep] = true
|
||||
reverseGraph[dep] = append(reverseGraph[dep], node)
|
||||
}
|
||||
}
|
||||
|
||||
// Add nodes that have no dependencies
|
||||
for pkg := range packagesToDelete {
|
||||
if !allNodes[pkg] {
|
||||
allNodes[pkg] = true
|
||||
dependencyGraph[pkg] = []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate in-degrees
|
||||
inDegree := make(map[string]int)
|
||||
for node := range allNodes {
|
||||
inDegree[node] = 0
|
||||
}
|
||||
for node, deps := range dependencyGraph {
|
||||
inDegree[node] = len(deps)
|
||||
}
|
||||
|
||||
// Kahn's algorithm - start with nodes that have no dependencies
|
||||
var queue []string
|
||||
for node, degree := range inDegree {
|
||||
if degree == 0 {
|
||||
queue = append(queue, node)
|
||||
}
|
||||
}
|
||||
|
||||
var result []string
|
||||
for len(queue) > 0 {
|
||||
node := queue[0]
|
||||
queue = queue[1:]
|
||||
result = append(result, node)
|
||||
|
||||
// Process dependents
|
||||
for _, dependent := range reverseGraph[node] {
|
||||
inDegree[dependent]--
|
||||
if inDegree[dependent] == 0 {
|
||||
queue = append(queue, dependent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for cycles: if not all nodes were processed, there's a cycle
|
||||
if len(result) != len(allNodes) {
|
||||
// Find unprocessed nodes
|
||||
processed := make(map[string]bool)
|
||||
for _, node := range result {
|
||||
processed[node] = true
|
||||
}
|
||||
var unprocessed []string
|
||||
for node := range allNodes {
|
||||
if !processed[node] {
|
||||
unprocessed = append(unprocessed, node)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("dependency cycle detected: the following packages form a cycle and cannot be deleted: %v", unprocessed)
|
||||
}
|
||||
|
||||
// Reverse the result to get dependents first, then dependencies
|
||||
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
|
||||
result[i], result[j] = result[j], result[i]
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(delCmd)
|
||||
delCmd.Flags().StringArrayVarP(&delCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
|
||||
delCmd.Flags().StringVar(&delCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
|
||||
}
|
||||
|
||||
564
cmd/cozypkg/cmd/dependencies.go
Normal file
564
cmd/cozypkg/cmd/dependencies.go
Normal file
@@ -0,0 +1,564 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"github.com/emicklei/dot"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
var dotCmdFlags struct {
|
||||
installed bool
|
||||
components bool
|
||||
files []string
|
||||
kubeconfig string
|
||||
}
|
||||
|
||||
var dotCmd = &cobra.Command{
|
||||
Use: "dot [package]...",
|
||||
Short: "Generate dependency graph as graphviz DOT format",
|
||||
Long: `Generate dependency graph as graphviz DOT format.
|
||||
|
||||
Pipe the output through the "dot" program (part of graphviz package) to render the graph:
|
||||
|
||||
cozypkg dot | dot -Tpng > graph.png
|
||||
|
||||
By default, shows dependencies for all PackageSource resources.
|
||||
Use --installed to show only installed Package resources.
|
||||
Specify packages as arguments or use -f flag to read from files.`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Collect package names from arguments and files
|
||||
packageNames := make(map[string]bool)
|
||||
for _, arg := range args {
|
||||
packageNames[arg] = true
|
||||
}
|
||||
|
||||
// Read packages from files (reuse function from add.go)
|
||||
for _, filePath := range dotCmdFlags.files {
|
||||
packages, err := readPackagesFromFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
packageNames[pkg] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to slice, empty means all packages
|
||||
var selectedPackages []string
|
||||
if len(packageNames) > 0 {
|
||||
for pkg := range packageNames {
|
||||
selectedPackages = append(selectedPackages, pkg)
|
||||
}
|
||||
}
|
||||
|
||||
// If multiple packages specified, show graph for all of them
|
||||
// If single package, use packageName for backward compatibility
|
||||
var packageName string
|
||||
if len(selectedPackages) == 1 {
|
||||
packageName = selectedPackages[0]
|
||||
} else if len(selectedPackages) > 1 {
|
||||
// Multiple packages - pass empty string to packageName, use selectedPackages
|
||||
packageName = ""
|
||||
}
|
||||
|
||||
// packagesOnly is inverse of components flag (if components=false, then packagesOnly=true)
|
||||
packagesOnly := !dotCmdFlags.components
|
||||
graph, allNodes, edgeVariants, packageNames, err := buildGraphFromCluster(ctx, dotCmdFlags.kubeconfig, packagesOnly, dotCmdFlags.installed, packageName, selectedPackages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting PackageSource dependencies: %w", err)
|
||||
}
|
||||
|
||||
dotGraph := generateDOTGraph(graph, allNodes, packagesOnly, edgeVariants, packageNames)
|
||||
dotGraph.Write(os.Stdout)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(dotCmd)
|
||||
dotCmd.Flags().BoolVarP(&dotCmdFlags.installed, "installed", "i", false, "show dependencies only for installed Package resources")
|
||||
dotCmd.Flags().BoolVar(&dotCmdFlags.components, "components", false, "show component-level dependencies")
|
||||
dotCmd.Flags().StringArrayVarP(&dotCmdFlags.files, "file", "f", []string{}, "Read packages from file or directory (can be specified multiple times)")
|
||||
dotCmd.Flags().StringVar(&dotCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
|
||||
}
|
||||
|
||||
var (
|
||||
dependenciesScheme = runtime.NewScheme()
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(dependenciesScheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(dependenciesScheme))
|
||||
}
|
||||
|
||||
// buildGraphFromCluster builds a dependency graph from PackageSource resources in the cluster.
|
||||
// Returns: graph, allNodes, edgeVariants (map[edgeKey]variants), packageNames, error
|
||||
func buildGraphFromCluster(ctx context.Context, kubeconfig string, packagesOnly bool, installedOnly bool, packageName string, selectedPackages []string) (map[string][]string, map[string]bool, map[string][]string, map[string]bool, error) {
|
||||
// Create Kubernetes client config
|
||||
var config *rest.Config
|
||||
var err error
|
||||
|
||||
if kubeconfig != "" {
|
||||
// Load kubeconfig from explicit path
|
||||
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to load kubeconfig from %s: %w", kubeconfig, err)
|
||||
}
|
||||
} else {
|
||||
// Use default kubeconfig loading (from env var or ~/.kube/config)
|
||||
config, err = ctrl.GetConfig()
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
k8sClient, err := client.New(config, client.Options{Scheme: dependenciesScheme})
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to create k8s client: %w", err)
|
||||
}
|
||||
|
||||
// Get installed Packages if needed
|
||||
installedPackages := make(map[string]bool)
|
||||
if installedOnly {
|
||||
var packageList cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &packageList); err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
for _, pkg := range packageList.Items {
|
||||
installedPackages[pkg.Name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// List all PackageSource resources
|
||||
var packageSourceList cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &packageSourceList); err != nil {
|
||||
return nil, nil, nil, nil, fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
|
||||
// Build map of existing packages and components
|
||||
packageNames := make(map[string]bool)
|
||||
allExistingComponents := make(map[string]bool) // "package.component" -> true
|
||||
for _, ps := range packageSourceList.Items {
|
||||
if ps.Name != "" {
|
||||
packageNames[ps.Name] = true
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
for _, component := range variant.Components {
|
||||
if component.Install != nil {
|
||||
componentFullName := fmt.Sprintf("%s.%s", ps.Name, component.Name)
|
||||
allExistingComponents[componentFullName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
graph := make(map[string][]string)
|
||||
allNodes := make(map[string]bool)
|
||||
edgeVariants := make(map[string][]string) // key: "source->target", value: list of variant names
|
||||
existingEdges := make(map[string]bool) // key: "source->target" to avoid duplicates
|
||||
componentHasLocalDeps := make(map[string]bool) // componentName -> has local component dependencies
|
||||
|
||||
// Process each PackageSource
|
||||
for _, ps := range packageSourceList.Items {
|
||||
psName := ps.Name
|
||||
if psName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by package name if specified
|
||||
if packageName != "" && psName != packageName {
|
||||
continue
|
||||
}
|
||||
|
||||
// Filter by selected packages if specified
|
||||
if len(selectedPackages) > 0 {
|
||||
found := false
|
||||
for _, selected := range selectedPackages {
|
||||
if psName == selected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by installed packages if flag is set
|
||||
if installedOnly && !installedPackages[psName] {
|
||||
continue
|
||||
}
|
||||
|
||||
allNodes[psName] = true
|
||||
|
||||
// Track package dependencies per variant
|
||||
packageDepVariants := make(map[string]map[string]bool) // dep -> variant -> true
|
||||
allVariantNames := make(map[string]bool)
|
||||
for _, v := range ps.Spec.Variants {
|
||||
allVariantNames[v.Name] = true
|
||||
}
|
||||
|
||||
// Track component dependencies per variant
|
||||
componentDepVariants := make(map[string]map[string]map[string]bool) // componentName -> dep -> variant -> true
|
||||
componentVariants := make(map[string]map[string]bool) // componentName -> variant -> true
|
||||
|
||||
// Extract dependencies from variants
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
// Variant-level dependencies (package-level)
|
||||
for _, dep := range variant.DependsOn {
|
||||
// If installedOnly is set, only include dependencies that are installed
|
||||
if installedOnly && !installedPackages[dep] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Track which variant this dependency comes from
|
||||
if packageDepVariants[dep] == nil {
|
||||
packageDepVariants[dep] = make(map[string]bool)
|
||||
}
|
||||
packageDepVariants[dep][variant.Name] = true
|
||||
|
||||
edgeKey := fmt.Sprintf("%s->%s", psName, dep)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[psName] = append(graph[psName], dep)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
|
||||
// Add to allNodes only if package exists
|
||||
if packageNames[dep] {
|
||||
allNodes[dep] = true
|
||||
}
|
||||
// If package doesn't exist, don't add to allNodes - it will be shown as missing (red)
|
||||
}
|
||||
|
||||
// Component-level dependencies
|
||||
if !packagesOnly {
|
||||
for _, component := range variant.Components {
|
||||
// Skip components without install section
|
||||
if component.Install == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
componentName := fmt.Sprintf("%s.%s", psName, component.Name)
|
||||
allNodes[componentName] = true
|
||||
|
||||
// Track which variants this component appears in
|
||||
if componentVariants[componentName] == nil {
|
||||
componentVariants[componentName] = make(map[string]bool)
|
||||
}
|
||||
componentVariants[componentName][variant.Name] = true
|
||||
|
||||
if component.Install != nil {
|
||||
if componentDepVariants[componentName] == nil {
|
||||
componentDepVariants[componentName] = make(map[string]map[string]bool)
|
||||
}
|
||||
|
||||
for _, dep := range component.Install.DependsOn {
|
||||
// Track which variant this dependency comes from
|
||||
if componentDepVariants[componentName][dep] == nil {
|
||||
componentDepVariants[componentName][dep] = make(map[string]bool)
|
||||
}
|
||||
componentDepVariants[componentName][dep][variant.Name] = true
|
||||
|
||||
// Check if it's a local component dependency or external
|
||||
if strings.Contains(dep, ".") {
|
||||
// External component dependency (package.component format)
|
||||
// Mark that this component has local dependencies (for edge to package logic)
|
||||
componentHasLocalDeps[componentName] = true
|
||||
|
||||
// Check if target component exists
|
||||
if allExistingComponents[dep] {
|
||||
// Component exists
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, dep)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[componentName] = append(graph[componentName], dep)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
allNodes[dep] = true
|
||||
} else {
|
||||
// Component doesn't exist - create missing component node
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, dep)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[componentName] = append(graph[componentName], dep)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
// Don't add to allNodes - will be shown as missing (red)
|
||||
|
||||
// Add edge from missing component to its package
|
||||
parts := strings.SplitN(dep, ".", 2)
|
||||
if len(parts) == 2 {
|
||||
depPackageName := parts[0]
|
||||
missingEdgeKey := fmt.Sprintf("%s->%s", dep, depPackageName)
|
||||
if !existingEdges[missingEdgeKey] {
|
||||
graph[dep] = append(graph[dep], depPackageName)
|
||||
existingEdges[missingEdgeKey] = true
|
||||
}
|
||||
// Add package to allNodes only if it exists
|
||||
if packageNames[depPackageName] {
|
||||
allNodes[depPackageName] = true
|
||||
}
|
||||
// If package doesn't exist, it will be shown as missing (red)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local component dependency (same package)
|
||||
// Mark that this component has local dependencies
|
||||
componentHasLocalDeps[componentName] = true
|
||||
|
||||
localDep := fmt.Sprintf("%s.%s", psName, dep)
|
||||
|
||||
// Check if target component exists
|
||||
if allExistingComponents[localDep] {
|
||||
// Component exists
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, localDep)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[componentName] = append(graph[componentName], localDep)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
allNodes[localDep] = true
|
||||
} else {
|
||||
// Component doesn't exist - create missing component node
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, localDep)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[componentName] = append(graph[componentName], localDep)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
// Don't add to allNodes - will be shown as missing (red)
|
||||
|
||||
// Add edge from missing component to its package
|
||||
missingEdgeKey := fmt.Sprintf("%s->%s", localDep, psName)
|
||||
if !existingEdges[missingEdgeKey] {
|
||||
graph[localDep] = append(graph[localDep], psName)
|
||||
existingEdges[missingEdgeKey] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store variant information for package dependencies that are not in all variants
|
||||
for dep, variants := range packageDepVariants {
|
||||
if len(variants) < len(allVariantNames) {
|
||||
var variantList []string
|
||||
for v := range variants {
|
||||
variantList = append(variantList, v)
|
||||
}
|
||||
edgeKey := fmt.Sprintf("%s->%s", psName, dep)
|
||||
edgeVariants[edgeKey] = variantList
|
||||
}
|
||||
}
|
||||
|
||||
// Add component->package edges for components without local dependencies
|
||||
if !packagesOnly {
|
||||
for componentName := range componentVariants {
|
||||
// Only add edge to package if component has no local component dependencies
|
||||
if !componentHasLocalDeps[componentName] {
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, psName)
|
||||
if !existingEdges[edgeKey] {
|
||||
graph[componentName] = append(graph[componentName], psName)
|
||||
existingEdges[edgeKey] = true
|
||||
}
|
||||
|
||||
// If component is not in all variants, store variant info for component->package edge
|
||||
componentAllVariants := componentVariants[componentName]
|
||||
if len(componentAllVariants) < len(allVariantNames) {
|
||||
var variantList []string
|
||||
for v := range componentAllVariants {
|
||||
variantList = append(variantList, v)
|
||||
}
|
||||
edgeVariants[edgeKey] = variantList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store variant information for component dependencies that are not in all variants
|
||||
for componentName, deps := range componentDepVariants {
|
||||
componentAllVariants := componentVariants[componentName]
|
||||
for dep, variants := range deps {
|
||||
if len(variants) < len(componentAllVariants) {
|
||||
var variantList []string
|
||||
for v := range variants {
|
||||
variantList = append(variantList, v)
|
||||
}
|
||||
// Determine the actual target name
|
||||
var targetName string
|
||||
if strings.Contains(dep, ".") {
|
||||
targetName = dep
|
||||
} else {
|
||||
targetName = fmt.Sprintf("%s.%s", psName, dep)
|
||||
}
|
||||
edgeKey := fmt.Sprintf("%s->%s", componentName, targetName)
|
||||
edgeVariants[edgeKey] = variantList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return graph, allNodes, edgeVariants, packageNames, nil
|
||||
}
|
||||
|
||||
// generateDOTGraph generates a DOT graph from the dependency graph.
|
||||
func generateDOTGraph(graph map[string][]string, allNodes map[string]bool, packagesOnly bool, edgeVariants map[string][]string, packageNames map[string]bool) *dot.Graph {
|
||||
g := dot.NewGraph(dot.Directed)
|
||||
g.Attr("rankdir", "RL")
|
||||
g.Attr("nodesep", "0.5")
|
||||
g.Attr("ranksep", "1.0")
|
||||
|
||||
// Helper function to check if a node is a package
|
||||
// A node is a package if:
|
||||
// 1. It's directly in packageNames
|
||||
// 2. It doesn't contain a dot (simple package name)
|
||||
// 3. It contains a dot but the part before the first dot is a package name
|
||||
isPackage := func(nodeName string) bool {
|
||||
if packageNames[nodeName] {
|
||||
return true
|
||||
}
|
||||
if !strings.Contains(nodeName, ".") {
|
||||
return true
|
||||
}
|
||||
// If it contains a dot, check if the part before the first dot is a package
|
||||
parts := strings.SplitN(nodeName, ".", 2)
|
||||
if len(parts) > 0 {
|
||||
return packageNames[parts[0]]
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Add nodes
|
||||
for node := range allNodes {
|
||||
if packagesOnly && !isPackage(node) {
|
||||
// Skip component nodes when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
|
||||
n := g.Node(node)
|
||||
|
||||
// Style nodes based on type
|
||||
if isPackage(node) {
|
||||
// Package node
|
||||
n.Attr("shape", "box")
|
||||
n.Attr("style", "rounded,filled")
|
||||
n.Attr("fillcolor", "lightblue")
|
||||
n.Attr("label", node)
|
||||
} else {
|
||||
// Component node
|
||||
n.Attr("shape", "box")
|
||||
n.Attr("style", "rounded,filled")
|
||||
n.Attr("fillcolor", "lightyellow")
|
||||
// Extract component name (part after last dot)
|
||||
parts := strings.Split(node, ".")
|
||||
if len(parts) > 0 {
|
||||
n.Attr("label", parts[len(parts)-1])
|
||||
} else {
|
||||
n.Attr("label", node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add edges
|
||||
for source, targets := range graph {
|
||||
if packagesOnly && !isPackage(source) {
|
||||
// Skip component edges when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
if packagesOnly && !isPackage(target) {
|
||||
// Skip component edges when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if target exists
|
||||
targetExists := allNodes[target]
|
||||
|
||||
// Determine edge type for coloring
|
||||
sourceIsPackage := isPackage(source)
|
||||
targetIsPackage := isPackage(target)
|
||||
|
||||
// Add edge
|
||||
edge := g.Edge(g.Node(source), g.Node(target))
|
||||
|
||||
// Set edge color based on type (if target exists)
|
||||
if targetExists {
|
||||
if sourceIsPackage && targetIsPackage {
|
||||
// Package -> Package: black (default)
|
||||
edge.Attr("color", "black")
|
||||
} else {
|
||||
// Component -> Package or Component -> Component: green
|
||||
edge.Attr("color", "green")
|
||||
}
|
||||
}
|
||||
|
||||
// If target doesn't exist, mark it as missing (red color)
|
||||
if !targetExists {
|
||||
edge.Attr("color", "red")
|
||||
edge.Attr("style", "dashed")
|
||||
|
||||
// Also add the missing node with red color
|
||||
missingNode := g.Node(target)
|
||||
missingNode.Attr("shape", "box")
|
||||
missingNode.Attr("style", "rounded,filled,dashed")
|
||||
missingNode.Attr("fillcolor", "lightcoral")
|
||||
|
||||
// Determine label based on node type
|
||||
if isPackage(target) {
|
||||
// Package node
|
||||
missingNode.Attr("label", target)
|
||||
} else {
|
||||
// Component node - extract component name
|
||||
parts := strings.Split(target, ".")
|
||||
if len(parts) > 0 {
|
||||
missingNode.Attr("label", parts[len(parts)-1])
|
||||
} else {
|
||||
missingNode.Attr("label", target)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Check if this edge has variant information (dependency not in all variants)
|
||||
edgeKey := fmt.Sprintf("%s->%s", source, target)
|
||||
if variants, hasVariants := edgeVariants[edgeKey]; hasVariants {
|
||||
// Add label with variant names
|
||||
edge.Attr("label", strings.Join(variants, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return g
|
||||
}
|
||||
220
cmd/cozypkg/cmd/list.go
Normal file
220
cmd/cozypkg/cmd/list.go
Normal file
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
var listCmdFlags struct {
|
||||
installed bool
|
||||
components bool
|
||||
kubeconfig string
|
||||
}
|
||||
|
||||
var listCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List PackageSource or Package resources",
|
||||
Long: `List PackageSource or Package resources in table format.
|
||||
|
||||
By default, lists PackageSource resources. Use --installed flag to list installed Package resources.
|
||||
Use --components flag to show components on separate lines.`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create Kubernetes client config
|
||||
var config *rest.Config
|
||||
var err error
|
||||
|
||||
if listCmdFlags.kubeconfig != "" {
|
||||
config, err = clientcmd.BuildConfigFromFlags("", listCmdFlags.kubeconfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load kubeconfig from %s: %w", listCmdFlags.kubeconfig, err)
|
||||
}
|
||||
} else {
|
||||
config, err = ctrl.GetConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
|
||||
k8sClient, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create k8s client: %w", err)
|
||||
}
|
||||
|
||||
if listCmdFlags.installed {
|
||||
return listPackages(ctx, k8sClient, listCmdFlags.components)
|
||||
}
|
||||
return listPackageSources(ctx, k8sClient, listCmdFlags.components)
|
||||
},
|
||||
}
|
||||
|
||||
func listPackageSources(ctx context.Context, k8sClient client.Client, showComponents bool) error {
|
||||
var psList cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &psList); err != nil {
|
||||
return fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
|
||||
// Use tabwriter for better column alignment
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||
defer w.Flush()
|
||||
|
||||
// Print header
|
||||
fmt.Fprintln(w, "NAME\tVARIANTS\tREADY\tSTATUS")
|
||||
|
||||
// Print rows
|
||||
for _, ps := range psList.Items {
|
||||
// Get variants
|
||||
var variants []string
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
variants = append(variants, variant.Name)
|
||||
}
|
||||
variantsStr := strings.Join(variants, ",")
|
||||
if len(variantsStr) > 28 {
|
||||
variantsStr = variantsStr[:25] + "..."
|
||||
}
|
||||
|
||||
// Get Ready condition
|
||||
ready := "Unknown"
|
||||
status := ""
|
||||
for _, condition := range ps.Status.Conditions {
|
||||
if condition.Type == "Ready" {
|
||||
ready = string(condition.Status)
|
||||
status = condition.Message
|
||||
if len(status) > 48 {
|
||||
status = status[:45] + "..."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", ps.Name, variantsStr, ready, status)
|
||||
|
||||
// Show components if requested
|
||||
if showComponents {
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
for _, component := range variant.Components {
|
||||
fmt.Fprintf(w, " %s\t%s\t\t\n",
|
||||
fmt.Sprintf("%s.%s", ps.Name, component.Name),
|
||||
variant.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listPackages(ctx context.Context, k8sClient client.Client, showComponents bool) error {
|
||||
var pkgList cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &pkgList); err != nil {
|
||||
return fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
|
||||
// Fetch all PackageSource resources once if components are requested
|
||||
var psMap map[string]*cozyv1alpha1.PackageSource
|
||||
if showComponents {
|
||||
var psList cozyv1alpha1.PackageSourceList
|
||||
if err := k8sClient.List(ctx, &psList); err != nil {
|
||||
return fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
psMap = make(map[string]*cozyv1alpha1.PackageSource)
|
||||
for i := range psList.Items {
|
||||
psMap[psList.Items[i].Name] = &psList.Items[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Use tabwriter for better column alignment
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
|
||||
defer w.Flush()
|
||||
|
||||
// Print header
|
||||
fmt.Fprintln(w, "NAME\tVARIANT\tREADY\tSTATUS")
|
||||
|
||||
// Print rows
|
||||
for _, pkg := range pkgList.Items {
|
||||
variant := pkg.Spec.Variant
|
||||
if variant == "" {
|
||||
variant = "default"
|
||||
}
|
||||
|
||||
// Get Ready condition
|
||||
ready := "Unknown"
|
||||
status := ""
|
||||
for _, condition := range pkg.Status.Conditions {
|
||||
if condition.Type == "Ready" {
|
||||
ready = string(condition.Status)
|
||||
status = condition.Message
|
||||
if len(status) > 48 {
|
||||
status = status[:45] + "..."
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", pkg.Name, variant, ready, status)
|
||||
|
||||
// Show components if requested
|
||||
if showComponents {
|
||||
// Look up PackageSource from map instead of making API call
|
||||
if ps, exists := psMap[pkg.Name]; exists {
|
||||
// Find the variant
|
||||
for _, v := range ps.Spec.Variants {
|
||||
if v.Name == variant {
|
||||
for _, component := range v.Components {
|
||||
fmt.Fprintf(w, " %s\t%s\t\t\n",
|
||||
fmt.Sprintf("%s.%s", pkg.Name, component.Name),
|
||||
variant)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(listCmd)
|
||||
listCmd.Flags().BoolVarP(&listCmdFlags.installed, "installed", "i", false, "list installed Package resources instead of PackageSource resources")
|
||||
listCmd.Flags().BoolVar(&listCmdFlags.components, "components", false, "show components on separate lines")
|
||||
listCmd.Flags().StringVar(&listCmdFlags.kubeconfig, "kubeconfig", "", "Path to kubeconfig file (defaults to ~/.kube/config or KUBECONFIG env var)")
|
||||
}
|
||||
|
||||
49
cmd/cozypkg/cmd/root.go
Normal file
49
cmd/cozypkg/cmd/root.go
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands.
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "cozypkg",
|
||||
Short: "A CLI for managing Cozystack packages",
|
||||
Long: ``,
|
||||
SilenceErrors: true,
|
||||
SilenceUsage: true,
|
||||
DisableAutoGenTag: true,
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() error {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Commands are registered in their respective init() functions
|
||||
}
|
||||
|
||||
30
cmd/cozypkg/main.go
Normal file
30
cmd/cozypkg/main.go
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/cozystack/cozystack/cmd/cozypkg/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,6 @@ import (
|
||||
cozystackiov1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/controller"
|
||||
"github.com/cozystack/cozystack/internal/controller/dashboard"
|
||||
lcw "github.com/cozystack/cozystack/internal/lineagecontrollerwebhook"
|
||||
"github.com/cozystack/cozystack/internal/telemetry"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
@@ -70,6 +69,7 @@ func main() {
|
||||
var telemetryEndpoint string
|
||||
var telemetryInterval string
|
||||
var cozystackVersion string
|
||||
var reconcileDeployment bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
@@ -89,6 +89,8 @@ func main() {
|
||||
"Interval between telemetry data collection (e.g. 15m, 1h)")
|
||||
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
|
||||
"Version of Cozystack")
|
||||
flag.BoolVar(&reconcileDeployment, "reconcile-deployment", false,
|
||||
"If set, the Cozystack API server is assumed to run as a Deployment, else as a DaemonSet.")
|
||||
opts := zap.Options{
|
||||
Development: false,
|
||||
}
|
||||
@@ -198,41 +200,33 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controller.TenantHelmReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "TenantHelmReconciler")
|
||||
os.Exit(1)
|
||||
cozyAPIKind := "DaemonSet"
|
||||
if reconcileDeployment {
|
||||
cozyAPIKind = "Deployment"
|
||||
}
|
||||
|
||||
if err = (&controller.CozystackConfigReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "CozystackConfigReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controller.CozystackResourceDefinitionReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
CozystackAPIKind: cozyAPIKind,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// special one that's both a webhook and a reconciler
|
||||
lineageControllerWebhook := &lcw.LineageControllerWebhook{
|
||||
if err = (&controller.CozystackResourceDefinitionHelmReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionHelmReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dashboardManager := &dashboard.Manager{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}
|
||||
if err := lineageControllerWebhook.SetupWithManagerAsController(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to setup controller", "controller", "LineageController")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := lineageControllerWebhook.SetupWithManagerAsWebhook(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to setup webhook", "webhook", "LineageWebhook")
|
||||
if err = dashboardManager.SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "DashboardReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -261,7 +255,9 @@ func main() {
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
ctx := ctrl.SetupSignalHandler()
|
||||
dashboardManager.InitializeStaticResources(ctx)
|
||||
if err := mgr.Start(ctx); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
502
cmd/cozystack-operator/main.go
Normal file
502
cmd/cozystack-operator/main.go
Normal file
@@ -0,0 +1,502 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
"github.com/cozystack/cozystack/internal/cozyvaluesreplicator"
|
||||
"github.com/cozystack/cozystack/internal/fluxinstall"
|
||||
"github.com/cozystack/cozystack/internal/operator"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(helmv2.AddToScheme(scheme))
|
||||
utilruntime.Must(sourcev1.AddToScheme(scheme))
|
||||
utilruntime.Must(sourcewatcherv1beta1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var installFlux bool
|
||||
var cozystackVersion string
|
||||
var cozyValuesSecretName string
|
||||
var cozyValuesSecretNamespace string
|
||||
var cozyValuesNamespaceSelector string
|
||||
var platformSourceURL string
|
||||
var platformSourceName string
|
||||
var platformSourceRef string
|
||||
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", false,
|
||||
"If set the metrics endpoint is served securely")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
flag.BoolVar(&installFlux, "install-flux", false, "Install Flux components before starting reconcile loop")
|
||||
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
|
||||
"Version of Cozystack")
|
||||
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
|
||||
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
|
||||
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
|
||||
flag.StringVar(&cozyValuesSecretName, "cozy-values-secret-name", "cozystack-values", "The name of the secret containing cluster-wide configuration values.")
|
||||
flag.StringVar(&cozyValuesSecretNamespace, "cozy-values-secret-namespace", "cozy-system", "The namespace of the secret containing cluster-wide configuration values.")
|
||||
flag.StringVar(&cozyValuesNamespaceSelector, "cozy-values-namespace-selector", "cozystack.io/system=true", "The label selector for namespaces where the cluster-wide configuration values must be replicated.")
|
||||
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
config := ctrl.GetConfigOrDie()
|
||||
|
||||
// Create a direct client (without cache) for pre-start operations
|
||||
directClient, err := client.New(config, client.Options{Scheme: scheme})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to create direct client")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
targetNSSelector, err := labels.Parse(cozyValuesNamespaceSelector)
|
||||
if err != nil {
|
||||
setupLog.Error(err, "could not parse namespace label selector")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Start the controller manager
|
||||
setupLog.Info("Starting controller manager")
|
||||
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Cache: cache.Options{
|
||||
ByObject: map[client.Object]cache.ByObject{
|
||||
// Cache only Secrets named <secretName> (in any namespace)
|
||||
&corev1.Secret{}: {
|
||||
Field: fields.OneTermEqualSelector("metadata.name", cozyValuesSecretName),
|
||||
},
|
||||
|
||||
// Cache only Namespaces that match a label selector
|
||||
&corev1.Namespace{}: {
|
||||
Label: targetNSSelector,
|
||||
},
|
||||
},
|
||||
},
|
||||
Metrics: metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
},
|
||||
WebhookServer: webhook.NewServer(webhook.Options{
|
||||
Port: 9443,
|
||||
}),
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "cozystack-operator.cozystack.io",
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, setting this significantly speeds up voluntary
|
||||
// leader transitions as the new leader don't have to wait LeaseDuration time first.
|
||||
//
|
||||
// In the default scaffold provided, the program ends immediately after
|
||||
// the manager stops, so would be fine to enable this option. However,
|
||||
// if you are doing or is intended to do any operation such as perform cleanups
|
||||
// after the manager stops then its usage might be unsafe.
|
||||
// LeaderElectionReleaseOnCancel: true,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
// Use direct client for pre-start operations (cache is not ready yet)
|
||||
if err := fluxinstall.Install(installCtx, directClient, fluxinstall.WriteEmbeddedManifests); err != nil {
|
||||
setupLog.Error(err, "failed to install Flux")
|
||||
os.Exit(1)
|
||||
}
|
||||
setupLog.Info("Flux installation completed successfully")
|
||||
}
|
||||
|
||||
// Generate and install platform source resource if specified
|
||||
if platformSourceURL != "" {
|
||||
setupLog.Info("Generating platform source resource", "url", platformSourceURL, "name", platformSourceName, "ref", platformSourceRef)
|
||||
installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer installCancel()
|
||||
|
||||
// Use direct client for pre-start operations (cache is not ready yet)
|
||||
if err := installPlatformSourceResource(installCtx, directClient, platformSourceURL, platformSourceName, platformSourceRef); err != nil {
|
||||
setupLog.Error(err, "failed to install platform source resource")
|
||||
os.Exit(1)
|
||||
} else {
|
||||
setupLog.Info("Platform source resource installation completed successfully")
|
||||
}
|
||||
}
|
||||
|
||||
// Setup PackageSource reconciler
|
||||
if err := (&operator.PackageSourceReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "PackageSource")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Setup Package reconciler
|
||||
if err := (&operator.PackageReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "Package")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Setup CozyValuesReplicator reconciler
|
||||
if err := (&cozyvaluesreplicator.SecretReplicatorReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
SourceNamespace: cozyValuesSecretNamespace,
|
||||
SecretName: cozyValuesSecretName,
|
||||
TargetNamespaceSelector: targetNSSelector,
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "CozyValuesReplicator")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("Starting controller manager")
|
||||
mgrCtx := ctrl.SetupSignalHandler()
|
||||
if err := mgr.Start(mgrCtx); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// installPlatformSourceResource generates and installs a Flux source resource (OCIRepository or GitRepository)
|
||||
// based on the platform source URL
|
||||
func installPlatformSourceResource(ctx context.Context, k8sClient client.Client, sourceURL, resourceName, refSpec string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Parse the source URL to determine type
|
||||
sourceType, repoURL, err := parsePlatformSourceURL(sourceURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse platform source URL: %w", err)
|
||||
}
|
||||
|
||||
// Parse reference specification
|
||||
refMap, err := parseRefSpec(refSpec)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse reference specification: %w", err)
|
||||
}
|
||||
|
||||
var obj client.Object
|
||||
switch sourceType {
|
||||
case "oci":
|
||||
obj, err = generateOCIRepository(resourceName, repoURL, refMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate OCIRepository: %w", err)
|
||||
}
|
||||
case "git":
|
||||
obj, err = generateGitRepository(resourceName, repoURL, refMap)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate GitRepository: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported source type: %s (expected oci:// or https://)", sourceType)
|
||||
}
|
||||
|
||||
// Apply the resource (create or update)
|
||||
logger.Info("Applying platform source resource",
|
||||
"apiVersion", obj.GetObjectKind().GroupVersionKind().GroupVersion().String(),
|
||||
"kind", obj.GetObjectKind().GroupVersionKind().Kind,
|
||||
"name", obj.GetName(),
|
||||
"namespace", obj.GetNamespace(),
|
||||
)
|
||||
|
||||
existing := obj.DeepCopyObject().(client.Object)
|
||||
key := client.ObjectKeyFromObject(obj)
|
||||
|
||||
err = k8sClient.Get(ctx, key, existing)
|
||||
if err != nil {
|
||||
if client.IgnoreNotFound(err) == nil {
|
||||
// Resource doesn't exist, create it
|
||||
if err := k8sClient.Create(ctx, obj); err != nil {
|
||||
return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)
|
||||
}
|
||||
logger.Info("Created platform source resource", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
|
||||
} else {
|
||||
return fmt.Errorf("failed to check if resource exists: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Resource exists, update it
|
||||
obj.SetResourceVersion(existing.GetResourceVersion())
|
||||
if err := k8sClient.Update(ctx, obj); err != nil {
|
||||
return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err)
|
||||
}
|
||||
logger.Info("Updated platform source resource", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsePlatformSourceURL parses the source URL and returns the source type and repository URL.
|
||||
// Supports formats:
|
||||
// - oci://registry.example.com/repo
|
||||
// - https://github.com/user/repo
|
||||
// - http://github.com/user/repo
|
||||
// - ssh://git@github.com/user/repo
|
||||
func parsePlatformSourceURL(sourceURL string) (sourceType, repoURL string, err error) {
|
||||
sourceURL = strings.TrimSpace(sourceURL)
|
||||
|
||||
if strings.HasPrefix(sourceURL, "oci://") {
|
||||
return "oci", sourceURL, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(sourceURL, "https://") || strings.HasPrefix(sourceURL, "http://") || strings.HasPrefix(sourceURL, "ssh://") {
|
||||
return "git", sourceURL, nil
|
||||
}
|
||||
|
||||
return "", "", fmt.Errorf("unsupported source URL scheme (expected oci://, https://, http://, or ssh://): %s", sourceURL)
|
||||
}
|
||||
|
||||
// parseRefSpec parses a reference specification string in the format "key1=value1,key2=value2".
|
||||
// Returns a map of key-value pairs.
|
||||
func parseRefSpec(refSpec string) (map[string]string, error) {
|
||||
result := make(map[string]string)
|
||||
|
||||
refSpec = strings.TrimSpace(refSpec)
|
||||
if refSpec == "" {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
pairs := strings.Split(refSpec, ",")
|
||||
for _, pair := range pairs {
|
||||
pair = strings.TrimSpace(pair)
|
||||
if pair == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Split on first '=' only to allow '=' in values (e.g., digest=sha256:...)
|
||||
idx := strings.Index(pair, "=")
|
||||
if idx == -1 {
|
||||
return nil, fmt.Errorf("invalid reference specification format: %q (expected key=value)", pair)
|
||||
}
|
||||
|
||||
key := strings.TrimSpace(pair[:idx])
|
||||
value := strings.TrimSpace(pair[idx+1:])
|
||||
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("empty key in reference specification: %q", pair)
|
||||
}
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("empty value for key %q in reference specification", key)
|
||||
}
|
||||
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Valid reference keys for OCI repositories
|
||||
var validOCIRefKeys = map[string]bool{
|
||||
"digest": true,
|
||||
"semver": true,
|
||||
"semverFilter": true,
|
||||
"tag": true,
|
||||
}
|
||||
|
||||
// Valid reference keys for Git repositories
|
||||
var validGitRefKeys = map[string]bool{
|
||||
"branch": true,
|
||||
"tag": true,
|
||||
"semver": true,
|
||||
"name": true,
|
||||
"commit": true,
|
||||
}
|
||||
|
||||
// validateOCIRef validates reference keys for OCI repositories
|
||||
func validateOCIRef(refMap map[string]string) error {
|
||||
for key := range refMap {
|
||||
if !validOCIRefKeys[key] {
|
||||
return fmt.Errorf("invalid OCI reference key %q (valid keys: digest, semver, semverFilter, tag)", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate digest format if provided
|
||||
if digest, ok := refMap["digest"]; ok {
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
return fmt.Errorf("digest must be in format 'sha256:<hash>', got: %s", digest)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateGitRef validates reference keys for Git repositories
|
||||
func validateGitRef(refMap map[string]string) error {
|
||||
for key := range refMap {
|
||||
if !validGitRefKeys[key] {
|
||||
return fmt.Errorf("invalid Git reference key %q (valid keys: branch, tag, semver, name, commit)", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate commit format if provided (should be a hex string)
|
||||
if commit, ok := refMap["commit"]; ok {
|
||||
if len(commit) < 7 {
|
||||
return fmt.Errorf("commit SHA should be at least 7 characters, got: %s", commit)
|
||||
}
|
||||
for _, c := range commit {
|
||||
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
|
||||
return fmt.Errorf("commit SHA should be a hexadecimal string, got: %s", commit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateOCIRepository creates an OCIRepository resource
|
||||
func generateOCIRepository(name, repoURL string, refMap map[string]string) (*sourcev1.OCIRepository, error) {
|
||||
if err := validateOCIRef(refMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := &sourcev1.OCIRepository{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: sourcev1.GroupVersion.String(),
|
||||
Kind: sourcev1.OCIRepositoryKind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: "cozy-system",
|
||||
},
|
||||
Spec: sourcev1.OCIRepositorySpec{
|
||||
URL: repoURL,
|
||||
Interval: metav1.Duration{Duration: 5 * time.Minute},
|
||||
},
|
||||
}
|
||||
|
||||
// Set reference if any ref options are provided
|
||||
if len(refMap) > 0 {
|
||||
obj.Spec.Reference = &sourcev1.OCIRepositoryRef{
|
||||
Digest: refMap["digest"],
|
||||
SemVer: refMap["semver"],
|
||||
SemverFilter: refMap["semverFilter"],
|
||||
Tag: refMap["tag"],
|
||||
}
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// generateGitRepository creates a GitRepository resource
|
||||
func generateGitRepository(name, repoURL string, refMap map[string]string) (*sourcev1.GitRepository, error) {
|
||||
if err := validateGitRef(refMap); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
obj := &sourcev1.GitRepository{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: sourcev1.GroupVersion.String(),
|
||||
Kind: sourcev1.GitRepositoryKind,
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: "cozy-system",
|
||||
},
|
||||
Spec: sourcev1.GitRepositorySpec{
|
||||
URL: repoURL,
|
||||
Interval: metav1.Duration{Duration: 5 * time.Minute},
|
||||
},
|
||||
}
|
||||
|
||||
// Set reference if any ref options are provided
|
||||
if len(refMap) > 0 {
|
||||
obj.Spec.Reference = &sourcev1.GitRepositoryRef{
|
||||
Branch: refMap["branch"],
|
||||
Tag: refMap["tag"],
|
||||
SemVer: refMap["semver"],
|
||||
Name: refMap["name"],
|
||||
Commit: refMap["commit"],
|
||||
}
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
179
cmd/lineage-controller-webhook/main.go
Normal file
179
cmd/lineage-controller-webhook/main.go
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"os"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
cozystackiov1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
lcw "github.com/cozystack/cozystack/internal/lineagecontrollerwebhook"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
|
||||
utilruntime.Must(cozystackiov1alpha1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var tlsOpts []func(*tls.Config)
|
||||
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
|
||||
"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
|
||||
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
|
||||
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
|
||||
"Enable leader election for controller manager. "+
|
||||
"Enabling this will ensure there is only one active controller manager.")
|
||||
flag.BoolVar(&secureMetrics, "metrics-secure", true,
|
||||
"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
|
||||
flag.BoolVar(&enableHTTP2, "enable-http2", false,
|
||||
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
|
||||
opts := zap.Options{
|
||||
Development: false,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
// due to its vulnerabilities. More specifically, disabling http/2 will
|
||||
// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
|
||||
// Rapid Reset CVEs. For more information see:
|
||||
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
|
||||
// - https://github.com/advisories/GHSA-4374-p667-p6c8
|
||||
disableHTTP2 := func(c *tls.Config) {
|
||||
setupLog.Info("disabling http/2")
|
||||
c.NextProtos = []string{"http/1.1"}
|
||||
}
|
||||
|
||||
if !enableHTTP2 {
|
||||
tlsOpts = append(tlsOpts, disableHTTP2)
|
||||
}
|
||||
|
||||
webhookServer := webhook.NewServer(webhook.Options{
|
||||
TLSOpts: tlsOpts,
|
||||
})
|
||||
|
||||
// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
|
||||
// More info:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
|
||||
// - https://book.kubebuilder.io/reference/metrics.html
|
||||
metricsServerOptions := metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
TLSOpts: tlsOpts,
|
||||
}
|
||||
|
||||
if secureMetrics {
|
||||
// FilterProvider is used to protect the metrics endpoint with authn/authz.
|
||||
// These configurations ensure that only authorized users and service accounts
|
||||
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
|
||||
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
|
||||
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
|
||||
|
||||
// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
|
||||
// generate self-signed certificates for the metrics server. While convenient for development and testing,
|
||||
// this setup is not recommended for production.
|
||||
}
|
||||
|
||||
// Configure rate limiting for the Kubernetes client
|
||||
config := ctrl.GetConfigOrDie()
|
||||
config.QPS = 50.0 // Increased from default 5.0
|
||||
config.Burst = 100 // Increased from default 10
|
||||
|
||||
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsServerOptions,
|
||||
WebhookServer: webhookServer,
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "8796f12d.cozystack.io",
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||
// speeds up voluntary leader transitions as the new leader don't have to wait
|
||||
// LeaseDuration time first.
|
||||
//
|
||||
// In the default scaffold provided, the program ends immediately after
|
||||
// the manager stops, so would be fine to enable this option. However,
|
||||
// if you are doing or is intended to do any operation such as perform cleanups
|
||||
// after the manager stops then its usage might be unsafe.
|
||||
// LeaderElectionReleaseOnCancel: true,
|
||||
})
|
||||
if err != nil {
|
||||
setupLog.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
lineageControllerWebhook := &lcw.LineageControllerWebhook{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}
|
||||
if err := lineageControllerWebhook.SetupWithManagerAsController(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to setup controller", "controller", "LineageController")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := lineageControllerWebhook.SetupWithManagerAsWebhook(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to setup webhook", "webhook", "LineageWebhook")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||
setupLog.Error(err, "unable to set up ready check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
setupLog.Error(err, "problem running manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
666
docs/agents/changelog.md
Normal file
666
docs/agents/changelog.md
Normal file
@@ -0,0 +1,666 @@
|
||||
# Changelog Generation Instructions
|
||||
|
||||
This file contains detailed instructions for AI-powered IDE on how to generate changelogs for Cozystack releases.
|
||||
|
||||
## When to use these instructions
|
||||
|
||||
Follow these instructions when the user explicitly asks to generate a changelog.
|
||||
|
||||
## Required Tools
|
||||
|
||||
Before generating changelogs, ensure you have access to `gh` (GitHub CLI) tool, which is used to fetch commit and PR author information. The GitHub CLI is used to correctly identify PR authors from commits and pull requests.
|
||||
|
||||
## Changelog Generation Process
|
||||
|
||||
When the user asks to generate a changelog, follow these steps in the specified order:
|
||||
|
||||
**CHECKLIST - All actions that must be completed:**
|
||||
- [ ] Step 1: Update information from remote (git fetch)
|
||||
- [ ] Step 2: Check current branch (must be main)
|
||||
- [ ] Step 3: Determine release type and previous version (minor vs patch release)
|
||||
- [ ] Step 4: Determine versions and analyze existing changelogs
|
||||
- [ ] 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**: 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)
|
||||
- [ ] **MANDATORY**: Extract PR numbers from commit messages, then use `gh pr view` for each PR to get the PR author. Do NOT use commit author. Only for commits without PR numbers (rare), fall back to `gh api repos/cozystack/cozystack/commits/<hash> --jq '.author.login'`
|
||||
- [ ] Step 8: Form new changelog (structure, format, generate contributors list)
|
||||
- [ ] Step 9: Verify completeness and save
|
||||
|
||||
### 1. Updating information from remote
|
||||
|
||||
```bash
|
||||
git fetch --tags --force --prune
|
||||
```
|
||||
|
||||
This is necessary to get up-to-date information about tags and commits from the remote repository.
|
||||
|
||||
### 2. Checking current branch
|
||||
|
||||
Make sure we are on the `main` branch:
|
||||
|
||||
```bash
|
||||
git branch --show-current
|
||||
```
|
||||
|
||||
### 3. Determining release type and previous version
|
||||
|
||||
**Important**: Determine if you're generating a changelog for a **minor release** (vX.Y.0) or a **patch release** (vX.Y.Z where Z > 0).
|
||||
|
||||
**For minor releases (vX.Y.0):**
|
||||
- Each minor version lives and evolves in its own branch (`release-X.Y`)
|
||||
- You MUST compare with the **previous minor version** (v(X-1).Y.0), not the last patch release
|
||||
- This ensures you capture all changes from the entire minor version cycle, including all patch releases
|
||||
- Example: For v0.38.0, compare with v0.37.0 (not v0.37.8)
|
||||
- Run a separate cycle to check the diff with the zero version of the previous minor release
|
||||
|
||||
**For patch releases (vX.Y.Z where Z > 0):**
|
||||
- Compare with the previous patch version (vX.Y.(Z-1))
|
||||
- Example: For v0.37.2, compare with v0.37.1
|
||||
|
||||
### 4. Determining versions and analyzing existing changelogs
|
||||
|
||||
**Determine the last published version:**
|
||||
1. Get the list of version tags:
|
||||
```bash
|
||||
git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V
|
||||
```
|
||||
|
||||
2. Get the last tag:
|
||||
```bash
|
||||
git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -1
|
||||
```
|
||||
|
||||
3. Compare tags with existing changelog files in `docs/changelogs/` to determine the last published version (the newest file `vX.Y.Z.md`)
|
||||
|
||||
**Study existing changelog format:**
|
||||
- Review recent changelog files to understand the format and structure
|
||||
- Pay attention to:
|
||||
- **Feature Highlights format** (for minor releases): Use `## Feature Highlights` with `### Feature Name` subsections containing detailed descriptions (2-4 paragraphs each). See v0.35.0 and v0.36.0 for examples.
|
||||
- Section structure (Major Features and Improvements, Security, Fixes, Dependencies, etc.)
|
||||
- PR link format (e.g., `[**@username**](https://github.com/username) in #1234`)
|
||||
- Change description style
|
||||
- Presence of Breaking changes sections, etc.
|
||||
|
||||
### 5. Getting the list of commits
|
||||
|
||||
**Important**: Determine if you're generating a changelog for a **minor release** (vX.Y.0) or a **patch release** (vX.Y.Z where Z > 0).
|
||||
|
||||
**For patch releases (vX.Y.Z where Z > 0):**
|
||||
Get the list of commits starting from the previous patch version to HEAD:
|
||||
|
||||
**⚠️ CRITICAL: Do NOT use --first-parent flag! It will skip merge commits including backports!**
|
||||
|
||||
```bash
|
||||
# Get all commits including merge commits (backports)
|
||||
git log <previous_version>..HEAD --pretty=format:"%h - %s (%an, %ar)"
|
||||
```
|
||||
|
||||
For example, if generating changelog for `v0.37.2`:
|
||||
```bash
|
||||
git log v0.37.1..HEAD --pretty=format:"%h - %s (%an, %ar)"
|
||||
```
|
||||
|
||||
**⚠️ IMPORTANT: Check for backports:**
|
||||
- Look for commits with "[Backport release-X.Y]" in the commit message
|
||||
- For backport PRs, find the original PR number mentioned in the backport commit message or PR description
|
||||
- Use the original PR author (not the backport PR author) when creating changelog entries
|
||||
- Include both the original PR number and backport PR number in the changelog entry (e.g., `#1606, #1609`)
|
||||
|
||||
**For minor releases (vX.Y.0):**
|
||||
Minor releases must include **all changes** from patch releases of the previous minor version. Get commits from the previous minor release:
|
||||
|
||||
**⚠️ CRITICAL: Do NOT use --first-parent flag! It will skip merge commits including backports!**
|
||||
|
||||
```bash
|
||||
# For v0.38.0, get all commits since v0.37.0 (including all patch releases v0.37.1, v0.37.2, etc.)
|
||||
git log v<previous_minor_version>..HEAD --pretty=format:"%h - %s (%an, %ar)"
|
||||
```
|
||||
|
||||
For example, if generating changelog for `v0.38.0`:
|
||||
```bash
|
||||
git log v0.37.0..HEAD --pretty=format:"%h - %s (%an, %ar)"
|
||||
```
|
||||
|
||||
This will include all commits from v0.37.1, v0.37.2, v0.37.3, etc., up to v0.38.0.
|
||||
|
||||
**⚠️ IMPORTANT: Always check merge commits:**
|
||||
- Merge commits may contain backports that need to be included
|
||||
- Check all commits in the range, including merge commits
|
||||
- For backports, always find and reference the original PR
|
||||
|
||||
### 6. Analyzing additional repositories
|
||||
|
||||
**⚠️ CRITICAL: This step is MANDATORY and must NOT be skipped!**
|
||||
|
||||
Cozystack release may include changes from related repositories. Check and include commits from these repositories if tags were released during the release period:
|
||||
|
||||
**Required repositories:**
|
||||
- **Documentation**: [https://github.com/cozystack/website](https://github.com/cozystack/website)
|
||||
- **MANDATORY**: Always check this repository for documentation changes during the release period
|
||||
- **MANDATORY**: Get GitHub username for EVERY commit. Extract PR number from commit message, then use `gh pr view <PR_NUMBER> --repo cozystack/website --json author --jq .author.login` to get PR author. Only if no PR number, fall back to `gh api repos/cozystack/website/commits/<hash> --jq '.author.login'`
|
||||
|
||||
**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/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.
|
||||
|
||||
**Process for each repository:**
|
||||
|
||||
1. **Get release period dates:**
|
||||
```bash
|
||||
# Get dates for the release period
|
||||
cd /path/to/cozystack
|
||||
RELEASE_START=$(git log -1 --format=%ai v<previous_version>)
|
||||
RELEASE_END=$(git log -1 --format=%ai HEAD)
|
||||
```
|
||||
|
||||
2. **Check for commits in website repository (always required):**
|
||||
```bash
|
||||
# Ensure website repository is cloned and up-to-date
|
||||
mkdir -p _repos
|
||||
if [ ! -d "_repos/website" ]; then
|
||||
cd _repos && git clone https://github.com/cozystack/website.git && cd ..
|
||||
fi
|
||||
cd _repos/website
|
||||
git fetch --all --tags --force
|
||||
git checkout main 2>/dev/null || git checkout master
|
||||
git pull
|
||||
|
||||
# Get commits between release dates (with some buffer)
|
||||
git log --since="$RELEASE_START" --until="$RELEASE_END" --format="%H|%s|%an" | while IFS='|' read -r commit_hash subject author_name; do
|
||||
# Extract PR number from commit message
|
||||
PR_NUMBER=$(git log -1 --format="%B" "$commit_hash" | grep -oE '#[0-9]+' | head -1 | tr -d '#')
|
||||
|
||||
# ALWAYS use PR author if PR number found, not commit author
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
GITHUB_USERNAME=$(gh pr view "$PR_NUMBER" --repo cozystack/website --json author --jq '.author.login // empty' 2>/dev/null)
|
||||
echo "$commit_hash|$subject|$author_name|$GITHUB_USERNAME|cozystack/website#$PR_NUMBER"
|
||||
else
|
||||
# Only fallback to commit author if no PR number found (rare)
|
||||
GITHUB_USERNAME=$(gh api repos/cozystack/website/commits/$commit_hash --jq '.author.login // empty')
|
||||
echo "$commit_hash|$subject|$author_name|$GITHUB_USERNAME|cozystack/website@${commit_hash:0:7}"
|
||||
fi
|
||||
done
|
||||
|
||||
# Look for documentation updates, new pages, or significant content changes
|
||||
# Include these in the "Documentation" section of the changelog WITH authors and PR links
|
||||
```
|
||||
|
||||
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!**
|
||||
|
||||
**Use the helper script:**
|
||||
```bash
|
||||
# Get release period dates
|
||||
RELEASE_START=$(git log -1 --format=%ai v<previous_version>)
|
||||
RELEASE_END=$(git log -1 --format=%ai HEAD)
|
||||
|
||||
# Run the script to check all optional repositories
|
||||
./docs/changelogs/hack/check-optional-repos.sh "$RELEASE_START" "$RELEASE_END"
|
||||
```
|
||||
|
||||
The script will:
|
||||
- Check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy)
|
||||
- Look for tags created during the release period
|
||||
- Get commits between tags (if tags exist) or by date range (if no tags)
|
||||
- Extract PR numbers from commit messages
|
||||
- For EVERY commit with PR number, get PR author via CLI: `gh pr view <PR_NUMBER> --repo cozystack/<repo> --json author --jq .author.login` (ALWAYS use PR author, not commit author)
|
||||
- For commits without PR numbers (rare), fallback to: `gh api repos/cozystack/<repo>/commits/<hash> --jq '.author.login'`
|
||||
- Output results in format: `commit_hash|subject|author_name|github_username|cozystack/repo#PR_NUMBER` or `cozystack/repo@commit_hash`
|
||||
|
||||
4. **Extract PR numbers and authors using GitHub CLI:**
|
||||
- **ALWAYS use PR author, not commit author** for commits from additional repositories
|
||||
- For each commit, extract PR number from commit message first: Extract `#123` pattern from commit message
|
||||
- If PR number found, use `gh pr view <PR_NUMBER> --repo cozystack/<repo> --json author --jq .author.login` to get PR author (the person who wrote the code)
|
||||
- Only if no PR number found (rare), fallback to commit author: `gh api repos/cozystack/<repo>/commits/<hash> --jq '.author.login'`
|
||||
- **Prefer PR numbers**: Use format `cozystack/website#123` if PR number found in commit message
|
||||
- **Fallback to commit hash**: Use format `cozystack/website@abc1234` if no PR number
|
||||
- **ALWAYS include author**: Every entry from additional repositories MUST include author in format `([**@username**](https://github.com/username) in cozystack/repo#123)`
|
||||
- Determine user impact and categorize appropriately
|
||||
- Format entries with repository prefix: `[website]`, `[talm]`, etc.
|
||||
|
||||
**Example entry format for additional repositories:**
|
||||
```markdown
|
||||
# If PR number found in commit message (REQUIRED format):
|
||||
* **[website] Update installation documentation**: Improved installation guide with new examples ([**@username**](https://github.com/username) in cozystack/website#123).
|
||||
|
||||
# If no PR number (fallback, use commit hash):
|
||||
* **[website] Update installation documentation**: Improved installation guide with new examples ([**@username**](https://github.com/username) in cozystack/website@abc1234).
|
||||
|
||||
# For optional repositories:
|
||||
* **[talm] Add new feature**: Description of the change ([**@username**](https://github.com/username) in cozystack/talm#456).
|
||||
```
|
||||
|
||||
**CRITICAL**:
|
||||
- **ALWAYS include author** for every entry from additional repositories
|
||||
- **ALWAYS include PR link or commit hash** for every entry
|
||||
- Never add entries without author and PR/commit reference
|
||||
- **ALWAYS use PR author, not commit author**: Extract PR number from commit message, then use `gh pr view <PR_NUMBER> --repo cozystack/<repo> --json author --jq .author.login` to get the PR author (the person who wrote the code)
|
||||
- Only if no PR number found (rare), fallback to commit author: `gh api repos/cozystack/<repo>/commits/<hash> --jq '.author.login'`
|
||||
- The commit author (especially for squash/merge commits) is usually the person who merged the PR, not the person who wrote the code
|
||||
|
||||
### 7. Analyzing commits and PRs
|
||||
|
||||
**⚠️ CRITICAL: You MUST get the author from PR, not from commit! Always use `gh pr view` to get the PR author. Do NOT use commit author!**
|
||||
|
||||
**Get all PR numbers from commits:**
|
||||
**⚠️ CRITICAL: Do NOT use --no-merges flag! It will skip merge commits including backports!**
|
||||
|
||||
```bash
|
||||
# Extract all PR numbers from commit messages in the release range (including merge commits)
|
||||
git log <previous_version>..<new_version> --format="%s%n%b" | grep -oE '#[0-9]+' | sort -u | tr -d '#'
|
||||
```
|
||||
|
||||
**⚠️ IMPORTANT: Handle backports correctly:**
|
||||
- Backport PRs have format: `[Backport release-X.Y] <original title> (#BACKPORT_PR_NUMBER)`
|
||||
- The backport commit message or PR description usually mentions the original PR number
|
||||
- For backport entries in changelog, use the original PR author (not the backport PR author)
|
||||
- Include both original and backport PR numbers in the changelog entry (e.g., `#1606, #1609`)
|
||||
- To find original PR from backport: Check the backport PR description or commit message for "Backport of #ORIGINAL_PR"
|
||||
|
||||
**For each PR number, get the author:**
|
||||
|
||||
**CRITICAL**: The commit author (especially for squash/merge commits) is usually the person who merged the PR (or GitHub bot), NOT the person who wrote the code. **ALWAYS use the PR author**, not the commit author.
|
||||
|
||||
**⚠️ MANDATORY: ALWAYS use `gh pr view` to get the PR author. Do NOT use commit author!**
|
||||
|
||||
**ALWAYS use GitHub CLI** to get the PR author:
|
||||
|
||||
```bash
|
||||
# Usage: Get PR author - MANDATORY for EVERY PR
|
||||
# Loop through ALL PR numbers and get PR author (including backports)
|
||||
git log <previous_version>..<new_version> --format="%s%n%b" | grep -oE '#[0-9]+' | sort -u | tr -d '#' | while read PR_NUMBER; do
|
||||
# Check if this is a backport PR
|
||||
BACKPORT_INFO=$(gh pr view "$PR_NUMBER" --json body --jq '.body' 2>/dev/null | grep -i "backport of #" || echo "")
|
||||
if [ -n "$BACKPORT_INFO" ]; then
|
||||
# Extract original PR number from backport description
|
||||
ORIGINAL_PR=$(echo "$BACKPORT_INFO" | grep -oE 'backport of #([0-9]+)' | grep -oE '[0-9]+' | head -1)
|
||||
if [ -n "$ORIGINAL_PR" ]; then
|
||||
# Use original PR author
|
||||
GITHUB_USERNAME=$(gh pr view "$ORIGINAL_PR" --json author --jq '.author.login // empty')
|
||||
PR_TITLE=$(gh pr view "$ORIGINAL_PR" --json title --jq '.title // empty')
|
||||
echo "$PR_NUMBER|$ORIGINAL_PR|$GITHUB_USERNAME|$PR_TITLE|BACKPORT"
|
||||
else
|
||||
# Fallback to backport PR author if original not found
|
||||
GITHUB_USERNAME=$(gh pr view "$PR_NUMBER" --json author --jq '.author.login // empty')
|
||||
PR_TITLE=$(gh pr view "$PR_NUMBER" --json title --jq '.title // empty')
|
||||
echo "$PR_NUMBER||$GITHUB_USERNAME|$PR_TITLE|BACKPORT"
|
||||
fi
|
||||
else
|
||||
# Regular PR
|
||||
GITHUB_USERNAME=$(gh pr view "$PR_NUMBER" --json author --jq '.author.login // empty')
|
||||
PR_TITLE=$(gh pr view "$PR_NUMBER" --json title --jq '.title // empty')
|
||||
echo "$PR_NUMBER||$GITHUB_USERNAME|$PR_TITLE|REGULAR"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
**⚠️ IMPORTANT**: You must run this for EVERY PR in the release period. Do NOT skip any PRs or assume the GitHub username based on the git author name.
|
||||
|
||||
**CRITICAL**: Always use `gh pr view <PR_NUMBER> --json author --jq .author.login` to get the PR author. This correctly identifies the person who wrote the code, not the person who merged it (which is especially important for squash merges).
|
||||
|
||||
**Why this matters**: Using the wrong author in changelogs gives incorrect credit and can confuse contributors. The merge/squash commit is created by the person who clicks "Merge" in GitHub, not the PR author.
|
||||
|
||||
**For commits without PR numbers (rare):**
|
||||
- Only if a commit has no PR number, fall back to commit author: `gh api repos/cozystack/cozystack/commits/<hash> --jq '.author.login'`
|
||||
- But this should be very rare - most commits should have PR numbers
|
||||
|
||||
**Extract PR number from commit messages:**
|
||||
- Check commit message subject (`%s`) and body (`%b`) for PR references: `#1234` or `(#1234)`
|
||||
- **Primary method**: Extract from commit message format `(#PR_NUMBER)` or `in #PR_NUMBER` or `Merge pull request #1234`
|
||||
- Use regex: `grep -oE '#[0-9]+'` to find all PR numbers
|
||||
|
||||
**⚠️ CRITICAL: Verify PR numbers match commit messages!**
|
||||
- Always verify that the PR number in the changelog matches the PR number in the commit message
|
||||
- Common mistake: Using wrong PR number (e.g., #1614 instead of #1617) when multiple similar commits exist
|
||||
- To verify: Check the actual commit message: `git log <commit_hash> -1 --format="%s%n%b" | grep -oE '#[0-9]+'`
|
||||
- If multiple PR numbers appear in a commit, use the one that matches the PR title/description
|
||||
- For merge commits, check the merged branch commits, not just the merge commit message
|
||||
|
||||
3. **Understand the change:**
|
||||
```bash
|
||||
# Get PR details (preferred method)
|
||||
gh pr view <PR_NUMBER> --json title,body,url
|
||||
|
||||
# Or get commit details if no PR number
|
||||
git show <commit_hash> --stat
|
||||
git show <commit_hash>
|
||||
```
|
||||
- Review PR description and changed files
|
||||
- Understand functionality added/changed/fixed
|
||||
- **Determine user impact**: What can users do now? What problems are fixed? What improvements do users experience?
|
||||
|
||||
4. **For release branches (backports):**
|
||||
- If commit is from `release-X.Y` branch, check if it's a backport
|
||||
- Find original commit in `main` to get correct PR number:
|
||||
```bash
|
||||
git log origin/main --grep="<part of commit message>" --oneline
|
||||
```
|
||||
|
||||
### 8. Forming a new changelog
|
||||
|
||||
Create a new changelog file in the format matching previous versions:
|
||||
|
||||
1. **Determine the release type:**
|
||||
- **Minor release (vX.Y.0)** - use full format with **Feature Highlights** section. **Must include all changes from patch releases of the previous minor version** (e.g., v0.38.0 should include changes from v0.37.1, v0.37.2, v0.37.3, etc.)
|
||||
- **Patch release (vX.Y.Z, where Z > 0)** - use more compact format, includes only changes since the previous patch release
|
||||
|
||||
**Feature Highlights format for minor releases:**
|
||||
- Use section header: `## Feature Highlights`
|
||||
- Include 3-6 major features as subsections with `### Feature Name` headers
|
||||
- Each feature subsection should contain:
|
||||
- **Detailed description** (2-4 paragraphs) explaining:
|
||||
- What the feature is and what problem it solves
|
||||
- How it works and what users can do with it
|
||||
- How to use it (if applicable)
|
||||
- Benefits and impact for users
|
||||
- **Links to documentation** when available (use markdown links)
|
||||
- **Code examples or configuration snippets** if helpful
|
||||
- Focus on user value and practical implications, not just technical details
|
||||
- Each feature should be substantial enough to warrant its own subsection
|
||||
- Order features by importance/impact (most important first)
|
||||
- Example format:
|
||||
```markdown
|
||||
## Feature Highlights
|
||||
|
||||
### Feature Name
|
||||
|
||||
Detailed description paragraph explaining what the feature is...
|
||||
|
||||
Another paragraph explaining how it works and what users can do...
|
||||
|
||||
Learn more in the [documentation](https://cozystack.io/docs/...).
|
||||
```
|
||||
|
||||
**Important for minor releases**: After collecting all commits, **systematically verify** that all PRs from patch releases are included:
|
||||
```bash
|
||||
# Extract all PR numbers from patch release changelogs
|
||||
grep -h "#[0-9]\+" docs/changelogs/v<previous_minor>.*.md | sort -u
|
||||
|
||||
# Extract all PR numbers from the new minor release changelog
|
||||
grep -h "#[0-9]\+" docs/changelogs/v<new_minor>.0.md | sort -u
|
||||
|
||||
# Compare and identify missing PRs
|
||||
# Ensure every PR from patch releases appears in the minor release changelog
|
||||
```
|
||||
|
||||
2. **Structure changes by categories:**
|
||||
|
||||
**For minor releases (vX.Y.0):**
|
||||
- **Feature Highlights** (required) - see format above
|
||||
- **Major Features and Improvements** - detailed list of all major features and improvements
|
||||
- **Improvements (minor)** - smaller improvements and enhancements
|
||||
- **Bug fixes** - all bug fixes
|
||||
- **Security** - security-related changes
|
||||
- **Dependencies & version updates** - dependency updates
|
||||
- **System Configuration** - system-level configuration changes
|
||||
- **Development, Testing, and CI/CD** - development and testing improvements
|
||||
- **Documentation** (include changes from website repository here - **MUST include authors and PR links for all entries**)
|
||||
- **Breaking changes & upgrade notes** (if any)
|
||||
- **Refactors & chores** (if any)
|
||||
|
||||
**For patch releases (vX.Y.Z where Z > 0):**
|
||||
- **Features and Improvements** - new features and improvements
|
||||
- **Fixes** - bug fixes
|
||||
- **Security** - security-related changes
|
||||
- **Dependencies** - dependency updates
|
||||
- **System Configuration** - system-level configuration changes
|
||||
- **Development, Testing, and CI/CD** - development and testing improvements
|
||||
- **Documentation** (include changes from website repository here - **MUST include authors and PR links for all entries**)
|
||||
- **Migration and Upgrades** (if applicable)
|
||||
|
||||
**Note**: When including changes from additional repositories, group them logically with main repository changes, or create separate subsections if there are many changes from a specific repository.
|
||||
|
||||
3. **Entry format:**
|
||||
- Use the format: `* **Brief description**: detailed description ([**@username**](https://github.com/username) in #PR_NUMBER)`
|
||||
- **CRITICAL - Get authorship correctly**:
|
||||
- **ALWAYS use PR author, not commit author**: Extract PR number from commit message, then use `gh pr view` to get the PR author. The commit author (especially for squash/merge commits) is usually the person who merged the PR (or GitHub bot), NOT the person who wrote the code.
|
||||
```bash
|
||||
# Get PR author from GitHub CLI (correct method)
|
||||
# Step 1: Extract PR number from commit message
|
||||
PR_NUMBER=$(git log <commit_hash> -1 --format="%s%n%b" | grep -oE '#[0-9]+' | head -1 | tr -d '#')
|
||||
|
||||
# Step 2: Get PR author (the person who wrote the code)
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
GITHUB_USERNAME=$(gh pr view "$PR_NUMBER" --json author --jq '.author.login')
|
||||
else
|
||||
# Only fallback to commit author if no PR number found (rare)
|
||||
GITHUB_USERNAME=$(gh api repos/cozystack/cozystack/commits/<commit_hash> --jq '.author.login')
|
||||
fi
|
||||
```
|
||||
**Example**: For PR #1507, the squash commit has author "kvaps" (who merged), but the PR author is "lllamnyp" (who wrote the code). Using `gh pr view 1507 --json author --jq .author.login` correctly returns "lllamnyp".
|
||||
- **For regular commits**: Use the commit author directly:
|
||||
```bash
|
||||
git log <commit_hash> -1 --format="%an|%ae"
|
||||
```
|
||||
- **Validation**: Before adding to changelog, verify the author by checking:
|
||||
- For merge commits: Compare merge commit author vs PR author (they should be different)
|
||||
- Check existing changelogs for author name to GitHub username mappings
|
||||
- Verify with: `git log <merge_commit>^1..<merge_commit>^2 --format="%an" --no-merges`
|
||||
- **Map author name to GitHub username**: Check existing changelogs for author name mappings, or extract from PR links in commit messages
|
||||
- **Always include user impact**: Each entry must explain how the change affects users
|
||||
- For new features: explain what users can now do
|
||||
- For bug fixes: explain what problem is solved for users
|
||||
- For improvements: explain what users will experience better
|
||||
- For breaking changes: clearly state what users need to do
|
||||
- Group related changes
|
||||
- Use bold font for important components/modules
|
||||
- Focus on user value, not just technical details
|
||||
|
||||
4. **Add a link to the full changelog:**
|
||||
|
||||
**For patch releases (vX.Y.Z where Z > 0):**
|
||||
```markdown
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v<previous_patch_version>...v<new_version>
|
||||
```
|
||||
Example: For v0.37.2, use `v0.37.1...v0.37.2`
|
||||
|
||||
**For minor releases (vX.Y.0):**
|
||||
```markdown
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v<previous_minor_version>...v<new_version>
|
||||
```
|
||||
Example: For v0.38.0, use `v0.37.0...v0.38.0` (NOT `v0.37.8...v0.38.0`)
|
||||
|
||||
**Important**: Minor releases must reference the previous minor release (vX.Y.0), not the last patch release, to include all changes from the entire minor version cycle.
|
||||
|
||||
5. **Generate contributors list:**
|
||||
|
||||
**⚠️ SIMPLIFIED APPROACH: Extract contributors from the generated changelog itself!**
|
||||
|
||||
Since you've already generated the changelog with all PR authors correctly identified, simply extract GitHub usernames from the changelog entries:
|
||||
|
||||
```bash
|
||||
# Extract all GitHub usernames from the current release changelog
|
||||
# This method is simpler and more reliable than extracting from git history
|
||||
|
||||
# For patch releases: extract from the current changelog file
|
||||
grep -oE '\[@[a-zA-Z0-9_-]+\]' docs/changelogs/v<version>.md | \
|
||||
sed 's/\[@/@/' | sed 's/\]//' | \
|
||||
sort -u
|
||||
|
||||
# For minor releases: extract from the current changelog file
|
||||
grep -oE '\[@[a-zA-Z0-9_-]+\]' docs/changelogs/v<version>.md | \
|
||||
sed 's/\[@/@/' | sed 's/\]//' | \
|
||||
sort -u
|
||||
```
|
||||
|
||||
**Get all previous contributors (to identify new ones):**
|
||||
```bash
|
||||
# Extract GitHub usernames from all previous changelogs
|
||||
grep -hE '\[@[a-zA-Z0-9_-]+\]' docs/changelogs/v*.md | \
|
||||
grep -oE '@[a-zA-Z0-9_-]+' | \
|
||||
sort -u > /tmp/previous_contributors.txt
|
||||
```
|
||||
|
||||
**Identify new contributors (first-time contributors):**
|
||||
```bash
|
||||
# Get current release contributors from the changelog
|
||||
grep -oE '@[a-zA-Z0-9_-]+' docs/changelogs/v<version>.md | \
|
||||
sort -u > /tmp/current_contributors.txt
|
||||
|
||||
# Get all previous contributors
|
||||
grep -hE '@[a-zA-Z0-9_-]+' docs/changelogs/v*.md | \
|
||||
grep -oE '@[a-zA-Z0-9_-]+' | \
|
||||
sort -u > /tmp/all_previous_contributors.txt
|
||||
|
||||
# Find new contributors (those in current but not in previous)
|
||||
comm -23 <(sort /tmp/current_contributors.txt) <(sort /tmp/all_previous_contributors.txt)
|
||||
```
|
||||
|
||||
**Why this approach is better:**
|
||||
- ✅ Uses the already-verified PR authors from the changelog (no need to query GitHub API again)
|
||||
- ✅ Automatically handles backports correctly (original PR authors are already in the changelog)
|
||||
- ✅ Simpler and faster (no git log parsing or API calls)
|
||||
- ✅ More reliable (matches exactly what's in the changelog)
|
||||
- ✅ Works for both patch and minor releases
|
||||
|
||||
**Add contributors section to changelog:**
|
||||
|
||||
Place the contributors section at the end of the changelog, before the "Full Changelog" link:
|
||||
```markdown
|
||||
## Contributors
|
||||
|
||||
We'd like to thank all contributors who made this release possible:
|
||||
|
||||
* [**@username1**](https://github.com/username1)
|
||||
* [**@username2**](https://github.com/username2)
|
||||
* [**@username3**](https://github.com/username3)
|
||||
* ...
|
||||
|
||||
### New Contributors
|
||||
|
||||
We're excited to welcome our first-time contributors:
|
||||
|
||||
* [**@newuser1**](https://github.com/newuser1) - First contribution!
|
||||
* [**@newuser2**](https://github.com/newuser2) - First contribution!
|
||||
```
|
||||
|
||||
**Formatting guidelines:**
|
||||
- List contributors in alphabetical order by GitHub username
|
||||
- Use the format: `* [**@username**](https://github.com/username)`
|
||||
- For new contributors, add " - First contribution!" note
|
||||
- If GitHub username cannot be determined, you can skip that contributor or use their git author name
|
||||
|
||||
**When to include:**
|
||||
- **For patch releases**: Contributors section is optional, but can be included for significant releases
|
||||
- **For minor releases (vX.Y.0)**: Contributors section is required - you must generate and include the contributors list
|
||||
- Always verify GitHub usernames by checking commit messages, PR links in changelog entries, or by examining PR details
|
||||
|
||||
6. **Add a comment with a link to the GitHub release:**
|
||||
```markdown
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v<new_version>
|
||||
-->
|
||||
```
|
||||
|
||||
### 9. Verification and saving
|
||||
|
||||
**Before saving, verify completeness:**
|
||||
|
||||
**For ALL releases:**
|
||||
- [ ] 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: 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
|
||||
- [ ] Step 8 completed: Contributors list generated
|
||||
- [ ] All commits from main repository included (including merge commits)
|
||||
- [ ] User impact described for each change
|
||||
- [ ] Format matches existing changelogs
|
||||
|
||||
**For patch releases:**
|
||||
- [ ] All commits from the release period are included (including merge commits with backports)
|
||||
- [ ] PR numbers match commit messages
|
||||
- [ ] Backports are properly identified and linked to original PRs
|
||||
|
||||
**For minor releases (vX.Y.0):**
|
||||
- [ ] All changes from patch releases (vX.Y.1, vX.Y.2, etc.) are included
|
||||
- [ ] Contributors section is present and complete
|
||||
- [ ] Full Changelog link references previous minor version (vX.Y.0), not last patch
|
||||
- [ ] Verify all PRs from patch releases are included:
|
||||
```bash
|
||||
# Extract and compare PR numbers
|
||||
PATCH_PRS=$(grep -hE "#[0-9]+" docs/changelogs/v<previous_minor>.*.md | grep -oE "#[0-9]+" | sort -u)
|
||||
MINOR_PRS=$(grep -hE "#[0-9]+" docs/changelogs/v<new_minor>.0.md | grep -oE "#[0-9]+" | sort -u)
|
||||
MISSING=$(comm -23 <(echo "$PATCH_PRS") <(echo "$MINOR_PRS"))
|
||||
|
||||
if [ -n "$MISSING" ]; then
|
||||
echo "Missing PRs from patch releases:"
|
||||
echo "$MISSING"
|
||||
# For each missing PR, check if it's a backport and verify change is included by description
|
||||
fi
|
||||
```
|
||||
|
||||
**Only proceed to save after all checkboxes are verified!**
|
||||
|
||||
**Save the changelog:**
|
||||
Save the changelog to file `docs/changelogs/v<version>.md` according to the version for which the changelog is being generated.
|
||||
|
||||
### Important notes
|
||||
|
||||
- **After fetch with --force** local tags are up-to-date, use them for work
|
||||
- **For release branches** always check original commits in `main` to get correct PR numbers
|
||||
- **Preserve the format** of existing changelog files
|
||||
- **Group related changes** logically
|
||||
- **Be accurate** in describing changes, based on actual commit diffs
|
||||
- **Check for PR numbers** and commit authors
|
||||
- **CRITICAL - Get authorship from PR, not from commit**:
|
||||
- **ALWAYS use PR author**: Extract PR number from commit message, then use `gh pr view <PR_NUMBER> --json author --jq .author.login` to get the PR author
|
||||
- Do NOT use commit author - the commit author (especially for squash/merge commits) is usually the person who merged the PR, not the person who wrote the code
|
||||
- For commits without PR numbers (rare), fall back to commit author: `gh api repos/cozystack/cozystack/commits/<commit_hash> --jq '.author.login'`
|
||||
- **Workflow**: Extract PR numbers from commits → Use `gh pr view` for each PR → Get PR author (the person who wrote the code)
|
||||
- Example: For PR #1507, the commit author is `@kvaps` (who merged), but `gh pr view 1507 --json author --jq .author.login` correctly returns `@lllamnyp` (who wrote the code)
|
||||
- Check existing changelogs for author name to GitHub username mappings
|
||||
- **Validation**: Before adding to changelog, always verify the author using `gh pr view` - never use commit author for PRs
|
||||
- **MANDATORY**: Always describe user impact: Every changelog entry must explain how the change affects end users, not just what was changed technically. Focus on user value and practical implications.
|
||||
|
||||
**Required steps:**
|
||||
|
||||
- **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**: 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)
|
||||
- **MANDATORY**: Only for commits without PR numbers (rare), fallback to: `gh api repos/cozystack/<repo>/commits/<hash> --jq '.author.login'`
|
||||
- **MANDATORY**: Do NOT skip getting GitHub username via CLI - do this for EVERY commit
|
||||
- **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.
|
||||
- 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
|
||||
- Group changes from additional repositories with main repository changes, or create separate subsections if there are many changes from a specific repository
|
||||
|
||||
- **PR author verification (Step 7) - MANDATORY**:
|
||||
- **⚠️ CRITICAL**: You MUST get the author from PR using `gh pr view`, NOT from commit
|
||||
- **⚠️ CRITICAL**: Extract PR numbers from commit messages, then use `gh pr view <PR_NUMBER> --json author --jq .author.login` for each PR
|
||||
- **⚠️ CRITICAL**: Do NOT use commit author - commit author is usually the person who merged, not the person who wrote the code
|
||||
- **⚠️ CRITICAL**: Do NOT skip this step for any PR, even if the author seems obvious
|
||||
- For commits without PR numbers (rare), fall back to: `gh api repos/cozystack/cozystack/commits/<hash> --jq '.author.login'`
|
||||
- This ensures correct attribution and prevents errors in changelog entries (especially important for squash/merge commits)
|
||||
|
||||
- **Contributors list (Step 8)**:
|
||||
- For minor releases (vX.Y.0): You must generate a list of all contributors and identify first-time contributors.
|
||||
- For patch releases: Contributors section is optional, but recommended for significant releases
|
||||
- Extract GitHub usernames from PR links in commit messages or changelog entries
|
||||
- This helps recognize community contributions and welcome new contributors
|
||||
- **Minor releases (vX.Y.0)**:
|
||||
- Must include **all changes** from patch releases of the previous minor version (e.g., v0.38.0 includes all changes from v0.37.1, v0.37.2, v0.37.3, etc.)
|
||||
- The "Full Changelog" link must reference the previous minor release (v0.37.0...v0.38.0), NOT the last patch release (v0.37.8...v0.38.0)
|
||||
- This ensures users can see the complete set of changes for the entire minor version cycle
|
||||
- **Verification step**: After creating the changelog, extract all PR numbers from patch release changelogs and verify they all appear in the minor release changelog to prevent missing entries
|
||||
- **Backport handling**: Patch releases may contain backports with different PR numbers (e.g., #1624 in patch release vs #1622 in main). For minor releases, use original PR numbers from main when available, but verify that all changes from patch releases are included regardless of PR number differences
|
||||
- **Content verification**: Don't rely solely on PR number matching - verify that change descriptions from patch releases appear in the minor release changelog, as backports may have different PR numbers
|
||||
|
||||
275
docs/agents/contributing.md
Normal file
275
docs/agents/contributing.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Instructions for AI Agents
|
||||
|
||||
Guidelines for AI agents contributing to Cozystack.
|
||||
|
||||
## Checklist for Creating a Pull Request
|
||||
|
||||
- [ ] Changes are made and tested
|
||||
- [ ] Commit message uses correct `[component]` prefix
|
||||
- [ ] Commit is signed off with `--signoff`
|
||||
- [ ] Branch is rebased on `upstream/main` (no extra commits)
|
||||
- [ ] PR body includes description and release note
|
||||
- [ ] PR is pushed and created with `gh pr create`
|
||||
|
||||
## How to Commit and Create Pull Requests
|
||||
|
||||
### 1. Make Your Changes
|
||||
|
||||
Edit the necessary files in the codebase.
|
||||
|
||||
### 2. Commit with Proper Format
|
||||
|
||||
Use the `[component]` prefix and `--signoff` flag:
|
||||
|
||||
```bash
|
||||
git commit --signoff -m "[component] Brief description of changes"
|
||||
```
|
||||
|
||||
**Component prefixes:**
|
||||
- System: `[dashboard]`, `[platform]`, `[cilium]`, `[kube-ovn]`, `[linstor]`, `[fluxcd]`, `[cluster-api]`
|
||||
- Apps: `[postgres]`, `[mysql]`, `[redis]`, `[kafka]`, `[clickhouse]`, `[virtual-machine]`, `[kubernetes]`
|
||||
- Other: `[tests]`, `[ci]`, `[docs]`, `[maintenance]`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
git commit --signoff -m "[dashboard] Add config hash annotations to restart pods on config changes"
|
||||
git commit --signoff -m "[postgres] Update operator to version 1.2.3"
|
||||
git commit --signoff -m "[docs] Add installation guide"
|
||||
```
|
||||
|
||||
### 3. Rebase on upstream/main (if needed)
|
||||
|
||||
If your branch has extra commits, clean it up:
|
||||
|
||||
```bash
|
||||
# Fetch latest
|
||||
git fetch upstream
|
||||
|
||||
# Create clean branch from upstream/main
|
||||
git checkout -b my-feature upstream/main
|
||||
|
||||
# Cherry-pick only your commit
|
||||
git cherry-pick <your-commit-hash>
|
||||
|
||||
# Force push to your branch
|
||||
git push -f origin my-feature:my-branch-name
|
||||
```
|
||||
|
||||
### 4. Push Your Branch
|
||||
|
||||
```bash
|
||||
git push origin <branch-name>
|
||||
```
|
||||
|
||||
### 5. Create Pull Request
|
||||
|
||||
Write the PR body to a temporary file:
|
||||
|
||||
```bash
|
||||
cat > /tmp/pr_body.md << 'EOF'
|
||||
## What this PR does
|
||||
|
||||
Brief description of the changes.
|
||||
|
||||
Changes:
|
||||
- Change 1
|
||||
- Change 2
|
||||
|
||||
### Release note
|
||||
|
||||
```release-note
|
||||
[component] Description for changelog
|
||||
```
|
||||
EOF
|
||||
```
|
||||
|
||||
Create the PR:
|
||||
|
||||
```bash
|
||||
gh pr create --title "[component] Brief description" --body-file /tmp/pr_body.md
|
||||
```
|
||||
|
||||
Clean up:
|
||||
|
||||
```bash
|
||||
rm /tmp/pr_body.md
|
||||
```
|
||||
|
||||
## Addressing AI Bot Reviewer Comments
|
||||
|
||||
When the user asks to fix comments from AI bot reviewers (like Qodo, Copilot, etc.):
|
||||
|
||||
### 1. Get PR Comments
|
||||
|
||||
View all comments on the pull request:
|
||||
|
||||
```bash
|
||||
gh pr view <PR-number> --comments
|
||||
```
|
||||
|
||||
Or for the current branch:
|
||||
|
||||
```bash
|
||||
gh pr view --comments
|
||||
```
|
||||
|
||||
### 2. Review Each Comment Carefully
|
||||
|
||||
**Important**: Do NOT blindly apply all suggestions. Each comment should be evaluated:
|
||||
|
||||
- **Consider context** - Does the suggestion make sense for this specific case?
|
||||
- **Check project conventions** - Does it align with Cozystack patterns?
|
||||
- **Evaluate impact** - Will this improve code quality or introduce issues?
|
||||
- **Question validity** - AI bots can be wrong or miss context
|
||||
|
||||
**When to apply:**
|
||||
- ✅ Legitimate bugs or security issues
|
||||
- ✅ Clear improvements to code quality
|
||||
- ✅ Better error handling or edge cases
|
||||
- ✅ Conformance to project conventions
|
||||
|
||||
**When to skip:**
|
||||
- ❌ Stylistic preferences that don't match project style
|
||||
- ❌ Over-engineering simple code
|
||||
- ❌ Changes that break existing patterns
|
||||
- ❌ Suggestions that show misunderstanding of the code
|
||||
|
||||
### 3. Apply Valid Fixes
|
||||
|
||||
Make changes addressing the valid comments. Use your judgment.
|
||||
|
||||
### 4. Leave Changes Uncommitted
|
||||
|
||||
**Critical**: Do NOT commit or push the changes automatically.
|
||||
|
||||
Leave the changes in the working directory so the user can:
|
||||
- Review the fixes
|
||||
- Decide whether to commit them
|
||||
- Make additional adjustments if needed
|
||||
|
||||
```bash
|
||||
# After making changes, show status but DON'T commit
|
||||
git status
|
||||
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
|
||||
# Get PR comments
|
||||
gh pr view 1234 --comments
|
||||
|
||||
# Review comments and identify valid ones
|
||||
# Make necessary changes to address valid comments
|
||||
# ... edit files ...
|
||||
|
||||
# Show what was changed (but don't commit)
|
||||
git status
|
||||
git diff
|
||||
|
||||
# Tell the user what was fixed and what was skipped
|
||||
```
|
||||
|
||||
## Git Permissions
|
||||
|
||||
Request these permissions when needed:
|
||||
- `git_write` - For commit, rebase, cherry-pick, branch operations
|
||||
- `network` - For push, fetch, pull operations
|
||||
|
||||
## Common Issues
|
||||
|
||||
**PR has extra commits?**
|
||||
→ Rebase on `upstream/main` and cherry-pick only your commits
|
||||
|
||||
**Wrong commit message?**
|
||||
→ `git commit --amend --signoff -m "[correct] message"` then `git push -f`
|
||||
|
||||
**Need to update PR?**
|
||||
→ `gh pr edit <number> --body "new description"`
|
||||
115
docs/agents/overview.md
Normal file
115
docs/agents/overview.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Cozystack Project Overview
|
||||
|
||||
This document provides detailed information about Cozystack project structure and conventions for AI agents.
|
||||
|
||||
## About Cozystack
|
||||
|
||||
Cozystack is an open-source Kubernetes-based platform and framework for building cloud infrastructure. It provides:
|
||||
|
||||
- **Managed Services**: Databases, VMs, Kubernetes clusters, object storage, and more
|
||||
- **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
|
||||
|
||||
The platform exposes infrastructure services via the Kubernetes API with ready-made configs, built-in monitoring, and alerts.
|
||||
|
||||
## Code Layout
|
||||
|
||||
```
|
||||
.
|
||||
├── packages/ # Main directory for cozystack packages
|
||||
│ ├── core/ # Core platform logic charts (installer, platform)
|
||||
│ ├── system/ # System charts (CSI, CNI, operators, etc.)
|
||||
│ ├── apps/ # User-facing charts shown in dashboard catalog
|
||||
│ └── extra/ # Tenant-specific modules, singleton charts which are used as dependencies
|
||||
├── dashboards/ # Grafana dashboards for monitoring
|
||||
├── hack/ # Helper scripts for local development
|
||||
│ └── e2e-apps/ # End-to-end application tests
|
||||
├── scripts/ # Scripts used by cozystack container
|
||||
│ └── migrations/ # Version migration scripts
|
||||
├── docs/ # Documentation
|
||||
│ ├── agents/ # AI agent instructions
|
||||
│ └── changelogs/ # Release changelogs
|
||||
├── cmd/ # Go command entry points
|
||||
│ ├── cozystack-api/
|
||||
│ ├── cozystack-controller/
|
||||
│ └── cozystack-assets-server/
|
||||
├── internal/ # Internal Go packages
|
||||
│ ├── controller/ # Controller implementations
|
||||
│ └── lineagecontrollerwebhook/
|
||||
├── pkg/ # Public Go packages
|
||||
│ ├── apis/
|
||||
│ ├── apiserver/
|
||||
│ └── registry/
|
||||
└── api/ # Kubernetes API definitions (CRDs)
|
||||
└── v1alpha1/
|
||||
```
|
||||
|
||||
## Package Structure
|
||||
|
||||
Every package is a Helm chart following the umbrella chart pattern:
|
||||
|
||||
```
|
||||
packages/<category>/<package-name>/
|
||||
├── Chart.yaml # Chart definition and parameter docs
|
||||
├── Makefile # Development workflow targets
|
||||
├── charts/ # Vendored upstream charts
|
||||
├── images/ # Dockerfiles and image build context
|
||||
├── patches/ # Optional upstream chart patches
|
||||
├── templates/ # Additional manifests
|
||||
├── templates/dashboard-resourcemap.yaml # Dashboard resource mapping
|
||||
├── values.yaml # Override values for upstream
|
||||
└── values.schema.json # JSON schema for validation and UI
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
### Helm Charts
|
||||
- Follow **umbrella chart** pattern for system components
|
||||
- Include upstream charts in `charts/` directory (vendored, not referenced)
|
||||
- Override configuration in root `values.yaml`
|
||||
- Use `values.schema.json` for input validation and dashboard UI rendering
|
||||
|
||||
### Go Code
|
||||
- Follow standard **Go conventions** and idioms
|
||||
- Use **controller-runtime** patterns for Kubernetes controllers
|
||||
- Prefer **kubebuilder** for API definitions and controllers
|
||||
- Add proper error handling and structured logging
|
||||
|
||||
### Git Commits
|
||||
- Use format: `[component] Description`
|
||||
- Always use `--signoff` flag
|
||||
- Reference PR numbers when available
|
||||
- Keep commits atomic and focused
|
||||
- Follow conventional commit format for changelogs
|
||||
|
||||
### Documentation
|
||||
|
||||
Documentation is organized as follows:
|
||||
- `docs/` - General documentation
|
||||
- `docs/agents/` - Instructions for AI agents
|
||||
- `docs/changelogs/` - Release changelogs
|
||||
- Main website: https://github.com/cozystack/website
|
||||
|
||||
## Things Agents Should Not Do
|
||||
|
||||
### Never Edit These
|
||||
- Do not modify files in `/vendor/` (Go dependencies)
|
||||
- Do not edit generated files: `zz_generated.*.go`
|
||||
- Do not change `go.mod`/`go.sum` manually (use `go get`)
|
||||
- Do not edit upstream charts in `packages/*/charts/` directly (use patches)
|
||||
- Do not modify image digests in `values.yaml` (generated by build)
|
||||
|
||||
### Version Control
|
||||
- Do not commit built artifacts from `_out`
|
||||
- Do not commit test artifacts or temporary files
|
||||
|
||||
### Git Operations
|
||||
- Do not force push to main/master
|
||||
- Do not update git config
|
||||
- Do not perform destructive operations without explicit request
|
||||
|
||||
### Core Components
|
||||
- Do not modify `packages/core/platform/` without understanding migration impact
|
||||
|
||||
29
docs/agents/releasing.md
Normal file
29
docs/agents/releasing.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Release Process
|
||||
|
||||
This document provides instructions for AI agents on how to handle release-related tasks.
|
||||
|
||||
## When to Use
|
||||
|
||||
Follow these instructions when the user asks to:
|
||||
- Create a new release
|
||||
- Prepare a release
|
||||
- Tag a release
|
||||
- Perform release-related tasks
|
||||
|
||||
## Instructions
|
||||
|
||||
For detailed release process instructions, follow the steps documented in:
|
||||
|
||||
**[docs/release.md](../release.md)**
|
||||
|
||||
## Quick Reference
|
||||
|
||||
The release process typically involves:
|
||||
1. Preparing the release branch
|
||||
2. Generating changelog
|
||||
3. Updating version numbers
|
||||
4. Creating git tags
|
||||
5. Building and publishing artifacts
|
||||
|
||||
All detailed steps are documented in `docs/release.md`.
|
||||
|
||||
18
docs/changelogs/patch-template.md
Normal file
18
docs/changelogs/patch-template.md
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0..
|
||||
-->
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
## Security
|
||||
|
||||
## Fixes
|
||||
|
||||
## Dependencies
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.36.0...main
|
||||
@@ -17,4 +17,4 @@ https://github.com/cozystack/cozystack/releases/tag/v0..
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: **Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.0...v0.35.0
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.34.0...v0.35.0
|
||||
|
||||
10
docs/changelogs/v0.35.3.md
Normal file
10
docs/changelogs/v0.35.3.md
Normal file
@@ -0,0 +1,10 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.35.3
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* [seaweedfs] Add a liveness check for the SeaweedFS S3 endpoint to improve health monitoring and enable automatic recovery. (@IvanHunters in https://github.com/cozystack/cozystack/pull/1368)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.35.2...v0.35.3
|
||||
14
docs/changelogs/v0.35.4.md
Normal file
14
docs/changelogs/v0.35.4.md
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.35.4
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* [virtual-machine] Fix the regression in VM update hook introduced in https://github.com/cozystack/cozystack/pull/1169 by targeting the correct API resource and avoiding conflicts with KubeVirt resources. (@kvaps in https://github.com/cozystack/cozystack/pull/1376, backported in https://github.com/cozystack/cozystack/pull/1377)
|
||||
* [cozy-lib] Add the missing template `cozy-lib.resources.flatten`. (@kvaps in https://github.com/cozystack/cozystack/pull/1372, backported in https://github.com/cozystack/cozystack/pull/1375)
|
||||
* [platform] Fix a boolean override bug in Helm merge. ConfigMap values now correctly take precedence over bundle defaults. (@dyudin0821 in https://github.com/cozystack/cozystack/pull/1385, backported in https://github.com/cozystack/cozystack/pull/1388)
|
||||
* [seaweedfs] Resolve connectivity issues in SeaweedFS. Increase Nginx ingress timeouts for SeaweedFS S3 endpoint. (@kvaps in https://github.com/cozystack/cozystack/pull/1386, backported in https://github.com/cozystack/cozystack/pull/1390)
|
||||
* [dx] Remove the BUILDER and PLATFORM autodetect logic in Makefiles. (@kvaps in https://github.com/cozystack/cozystack/pull/1391, backported in https://github.com/cozystack/cozystack/pull/1392)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.35.3...v0.35.4
|
||||
11
docs/changelogs/v0.35.5.md
Normal file
11
docs/changelogs/v0.35.5.md
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.35.5
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* [etcd] Ensure that TopologySpreadConstraints consistently target etcd pods. (@kvaps in https://github.com/cozystack/cozystack/pull/1405, backported in https://github.com/cozystack/cozystack/pull/1406)
|
||||
* [tests] Add resource quota for testing namespaces. (@IvanHunters in https://github.com/cozystack/cozystack/commit/4982cdf5024c8bb9aa794b91d55545ea6b105d17)
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.35.4...v0.35.5
|
||||
117
docs/changelogs/v0.36.0.md
Normal file
117
docs/changelogs/v0.36.0.md
Normal file
@@ -0,0 +1,117 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.36.0
|
||||
-->
|
||||
|
||||
|
||||
## Feature Highlights
|
||||
|
||||
Release v0.36.0 focuses on the stability, observability, and flexible configuration of managed applications.
|
||||
|
||||
### Per-Namespace Resource Limits for Tenants
|
||||
|
||||
Resource management for Cozystack tenants has received a final patch and is now graduated to a stable feature.
|
||||
Platform administrators can define explicit CPU, memory, and storage limits for each tenant's namespace
|
||||
via the tenant specification.
|
||||
This prevents any single tenant from consuming more than their share of cluster resources,
|
||||
ensuring cluster stability and a guaranteed service level for each tenant.
|
||||
|
||||
### Kube-OVN Cluster Health Monitor
|
||||
|
||||
A new component called the Kube-OVN Plunger continuously monitors the health of the Kube-OVN network's central control cluster.
|
||||
This external agent gathers OVN cluster status and consensus information, exposing Prometheus metrics and live events stream via SSE.
|
||||
As a result, it provides much better visibility of the virtual network layer and helps maintain a reliable and observable network in Cozystack.
|
||||
This change opens the road to automated Kube-OVN database operations and recovery in specific corner cases.
|
||||
|
||||
### Configurable CoreDNS Addon for Kubernetes
|
||||
|
||||
Cozystack introduces a dedicated CoreDNS addon for managing cluster DNS with greater flexibility.
|
||||
CoreDNS is now deployed via a Helm chart and can be tuned through custom values in the cluster specification,
|
||||
including autoscaling, replica count, and adjusting service IP.
|
||||
CoreDNS can now be configured in the dashboard and using Cozystack API.
|
||||
|
||||
### Granular SeaweedFS Service Configuration
|
||||
|
||||
The SeaweedFS S3 storage service in Cozystack is now far more configurable at a component level.
|
||||
The Helm chart for SeaweedFS now includes independent configuration for each component and its resources.
|
||||
It includes the master nodes, volume servers with support for multiple zones, filers, the backing database, and the S3 gateway.
|
||||
Administrators can set per-component parameters such as the number of replicas, available CPU, memory, and storage size.
|
||||
|
||||
### Server-side Encryption for S3
|
||||
|
||||
Cozystack v0.36.0 includes SeaweedFS 3.97, bringing support for server-side encryption of S3 buckets (SSE-C, SSE-KMS, and SSE-S3).
|
||||
|
||||
**Breaking change:** upon updating Cozystack, SeaweedFS will be updated to a newer version, and the services specification
|
||||
will be converted to the new format.
|
||||
|
||||
### Custom Resource Profiles for Ingress Controller
|
||||
|
||||
NGINX controller is now configurable on a per-replica basis.
|
||||
Configurations include the ingress controller pods' CPU and memory requests/limits, either with direct values or using one of the available presets.
|
||||
|
||||
### Cozystack REST API Documentation
|
||||
|
||||
[Cozystack REST API reference](https://cozystack.io/docs/cozystack-api/rest/) is now published on the website.
|
||||
It includes endpoints and methods for listing, creating, updating, and removing each managed application, defined as Cozystack CRD.
|
||||
|
||||
|
||||
### Built-in LLDP-Based Neighbor Discovery in Talos
|
||||
|
||||
Cozystack now includes the LLDPD extension in its Talos OS image, enabling Link Layer Discovery Protocol (LLDP) out of the box.
|
||||
This means each node can automatically discover and advertise its network neighbors and topology without any manual setup.
|
||||
|
||||
### Use external IP for Egress Traffic in VMs
|
||||
|
||||
When a virtual machine has an external IP assigned to it, it will now always use it for egress traffic, independently of the external method used.
|
||||
|
||||
## Major Features and Improvements
|
||||
|
||||
* [talos] Add LLDPD (`ghcr.io/siderolabs/lldpd`) as a built-in system extension, enabling LLDP-based neighbor discovery out of the box. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1351 and https://github.com/cozystack/cozystack/pull/1360)
|
||||
* [kubernetes] Add a configurable CoreDNS addon with valuesOverride, packaged chart, and managed deployment (metrics, autoscaling, HPA, customizable Service). (@klinch0 in https://github.com/cozystack/cozystack/pull/1362)
|
||||
* [kube-ovn] Implement the Kube-OVN plunger, an external monitoring agent for the ovn-central cluster. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1380, patched in https://github.com/cozystack/cozystack/pull/1414 and https://github.com/cozystack/cozystack/pull/1418)
|
||||
* [tenant] Enable per-namespace resource quota settings in tenants, with explicit cpu, memory, and storage values. (@IvanHunters in https://github.com/cozystack/cozystack/pull/1389)
|
||||
* [seaweedfs] Add detailed resource configuration for each component of the SeaweedFS service. (@klinch0 and @kvaps in https://github.com/cozystack/cozystack/pull/1415)
|
||||
* [ingress] Enable per-replica resource configuration to the ingress controller. (@kvaps in https://github.com/cozystack/cozystack/pull/1416)
|
||||
* [virtual-machine] Use external IP for egress traffic with `PortList` method. (@kvaps in https://github.com/cozystack/cozystack/pull/1349)
|
||||
|
||||
|
||||
## Fixes
|
||||
|
||||
* [cozy-lib] Fix malformed retrieval of `cozyConfig` in the cozy-lib template. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1348)
|
||||
* [cozy-lib] Add the missing template `cozy-lib.resources.flatten`. (@kvaps in https://github.com/cozystack/cozystack/pull/1372)
|
||||
* [cozystack-api] Sanitize the OpenAPI v2 schema. (@kvaps in https://github.com/cozystack/cozystack/pull/1353)
|
||||
* [kube-ovn] Improve northd leader detection. Patch the northd leader check to test against all endpoints instead of just the first one marked as ready. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1363)
|
||||
* [seaweedfs] Add a liveness check for the SeaweedFS S3 endpoint to improve health monitoring and enable automatic recovery. (@IvanHunters in https://github.com/cozystack/cozystack/pull/1368)
|
||||
* [seaweedfs] Resolve race conditions in SeaweedFS. Increase deployment timeouts and set install/upgrade remediation to unlimited retries to improve deployment resilience. (@IvanHunters in https://github.com/cozystack/cozystack/pull/1371)
|
||||
* [seaweedfs] Resolve connectivity issues in SeaweedFS. Increase Nginx ingress timeouts for SeaweedFS S3 endpoint. (@kvaps in https://github.com/cozystack/cozystack/pull/1386)
|
||||
* [virtual-machine] Fix the reg ression in VM update hook introduced in https://github.com/cozystack/cozystack/pull/1169. Target the correct API resource and avoid conflicts with KubeVirt resources. (@kvaps in https://github.com/cozystack/cozystack/pull/1376)
|
||||
* [virtual-machine] Correct app version references in `virtual-machine` and `vm-instance`, ensuring accurate versioning during migrations. (@kvaps in https://github.com/cozystack/cozystack/pull/1378).
|
||||
* [cozyreport] Fix an error where cozyreport tried to parse non-existent objects and generated garbage output in CI debug logs. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1383)
|
||||
* [platform] Fix a boolean override bug in Helm merge. ConfigMap values now correctly take precedence over bundle defaults. (@dyudin0821 in https://github.com/cozystack/cozystack/pull/1385)
|
||||
* [kubernetes] CoreDNS release now installs and stores state in the `kube-system` namespace. (@kvaps in https://github.com/cozystack/cozystack/pull/1395)
|
||||
* [kubernetes] Expose configuration for CoreDNS, enabling setting the image repository and replica count via `values.yaml`. (@kvaps in https://github.com/cozystack/cozystack/pull/1410)
|
||||
* [etcd] Ensure that TopologySpreadConstraints consistently target etcd pods. (@kvaps in https://github.com/cozystack/cozystack/pull/1405)
|
||||
* [tenant] Use force-upgrade for ingress controller charts. (@klinch0 in https://github.com/cozystack/cozystack/pull/1404)
|
||||
* [cozystack-controller] Fix an RBAC error that prevented the workload labelling feature from working. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1419)
|
||||
* [seaweedfs] Remove VerticalPodAutoscaler for SeaweedFS. (@kvaps in https://github.com/cozystack/cozystack/pull/1421)
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
* Update LINSTOR to v1.31.3. (@kvaps in https://github.com/cozystack/cozystack/pull/1358)
|
||||
* Update SeaweedFS to v3.97. (@kvaps in https://github.com/cozystack/cozystack/pull/1361 and https://github.com/cozystack/cozystack/pull/1373)
|
||||
* Update Kube-OVN to 1.14.5. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1363)
|
||||
* Replace Bitnami images with alternatives in all charts. (@kvaps in https://github.com/cozystack/cozystack/pull/1374)
|
||||
|
||||
## Documentation
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* [dx] Remove the BUILDER and PLATFORM autodetect logic in Makefiles. (@kvaps in https://github.com/cozystack/cozystack/pull/1391)
|
||||
* [ci] Use the host buildx config in CI. (@kvaps in https://github.com/cozystack/cozystack/pull/1015)
|
||||
* [ci] Add `jq` and `git` to the installer image. (@kvaps in https://github.com/cozystack/cozystack/pull/1417)
|
||||
* [ci] Source the `REGISTRY` environment variable from actions' variables, not secrets, so external pull requests can work. (@lllamnyp in https://github.com/cozystack/cozystack/pull/1423)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.35.0...v0.36.0
|
||||
22
docs/changelogs/v0.36.1.md
Normal file
22
docs/changelogs/v0.36.1.md
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.36.1
|
||||
-->
|
||||
|
||||
## Major Features and Improvements
|
||||
|
||||
* [cozystack-api] Implement recursive, Kubernetes-like defaulting for applications: missing fields in nested objects and arrays are auto-populated safely without mutating shared defaults. (@kvaps in https://github.com/cozystack/cozystack/pull/1432)
|
||||
|
||||
## Fixes
|
||||
|
||||
* [cozystack-api] Update defaulting API schemas. (@kvaps in https://github.com/cozystack/cozystack/pull/1433)
|
||||
* [dashboard] Fix Bitnami dependencies. (@kvaps in https://github.com/cozystack/cozystack/pull/1431)
|
||||
* [seaweedfs] Fix SeaweedFS migration. (@kvaps in https://github.com/cozystack/cozystack/pull/1430)
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* [adopters] Add [Hidora](https://hikube.cloud) to the Cozystack adopters list. (@matthieu-robin in https://github.com/cozystack/cozystack/pull/1429)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v0.36.0...v0.36.1
|
||||
21
docs/changelogs/v0.36.2.md
Normal file
21
docs/changelogs/v0.36.2.md
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
<!--
|
||||
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)
|
||||
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.36.1...v0.36.2](https://github.com/cozystack/cozystack/compare/v0.36.1...v0.36.2)
|
||||
117
docs/changelogs/v0.37.0.md
Normal file
117
docs/changelogs/v0.37.0.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Cozystack v0.37 — “OpenAPI Dashboard & Lineage Everywhere”
|
||||
|
||||
We’ve shipped a big usability push this cycle: a brand-new **OpenAPI-driven dashboard**, lineage labeling across core resource types, and several reliability improvements to smooth upgrades from 0.36→ 0.37. Below are the highlights and the full categorized lists.
|
||||
|
||||
## Highlights
|
||||
|
||||
* **New OpenAPI-based Dashboard** replaces the old UI, adds module-aware navigation, dynamic branding, and richer Kubernetes resource views ([**@kvaps**](https://github.com/kvaps) in #1269, #1463, #1460).
|
||||
* **Lineage Webhook** tags Pods, PVCs, Services, Ingresses, and Secrets, adding labels referencing the managing Cozystack application ([**@lllamnyp**](https://github.com/lllamnyp) in #1448, #1452, #1477, #1486, #1497; [**@kvaps**](https://github.com/kvaps) in #1454).
|
||||
* **Smoother upgrades** with installer and migration hardening, decoupled CRDs vs. API server ([**@lllamnyp**](https://github.com/lllamnyp) in #1494, #1498; [**@kvaps**](https://github.com/kvaps) in #1506).
|
||||
* **Operations quality**: Kubernetes tests with smarter waits/readiness checks ([**@IvanHunters**](https://github.com/IvanHunters) in #1485).
|
||||
|
||||
---
|
||||
|
||||
## New features
|
||||
|
||||
### Dashboard
|
||||
|
||||
* Introduce the OpenAPI-based dashboard and controller; implement TenantNamespace, TenantModules, TenantSecret/SecretsTable resources ([**@kvaps**](https://github.com/kvaps) in #1269).
|
||||
* Module-aware navigation, richer detail views (Services/Secrets/Ingresses), improved sidebars; “Tenant Modules” grouping ([**@kvaps**](https://github.com/kvaps) in #1463).
|
||||
* Dynamic branding via cluster config (tenant name, footer/title, logo/icon SVGs) ([**@kvaps**](https://github.com/kvaps) in #1460).
|
||||
* Dashboard: fix namespace listing for unprivileged users and stabilize streamed requests; build-time patching ([**@kvaps**](https://github.com/kvaps) in #1456).
|
||||
* Dashboard UX set: marketplace hides module resources; consistent navigation/links; prefill “name” in forms; ingress factory; formatted TenantNamespaces tables ([**@kvaps**](https://github.com/kvaps) in #1463).
|
||||
* **Dashboard**: list modules reliably; remove Tenant from Marketplace; fix field override while typing ([**@kvaps**](https://github.com/kvaps) in #1501, #1503).
|
||||
* **Dashboard**: correct API group for applications; sidebars; disable auto-expand; fix `/docs` redirect ([**@kvaps**](https://github.com/kvaps) in #1463, #1465, #1462).
|
||||
* **Dashboard**: show Secrets with empty values correctly ([**@kvaps**](https://github.com/kvaps) in #1480).
|
||||
* Dashboard configuration refactor: generate static resources at startup; auto-cleanup stale objects; higher controller client throughput ([**@kvaps**](https://github.com/kvaps) in #1457).
|
||||
|
||||
### Migration to v0.37
|
||||
* **Installer/Migrations**: prevent unintended deletion of platform resource definitions; resilient timestamping; tolerant annotations; stronger migrate-then-reconcile flow ([**@kvaps**](https://github.com/kvaps) in #1475; Andrei Kvapil & [**@lllamnyp**](https://github.com/lllamnyp) in #1498).
|
||||
* Installer hardening for **migration #20**: packaged apply, ordered waits/readiness checks, RFC3339(nano) stamping; Helm in installer image (Andrei Kvapil & [**@lllamnyp**](https://github.com/lllamnyp) in #1498).
|
||||
* **Decoupled API & CozyRDs**: You can now upgrade the Cozystack API server independently of CRDs/CozyRD instances, easing 0.36 → 0.37 migrations ([**@lllamnyp**](https://github.com/lllamnyp) in #1494).
|
||||
* **Migration #20**: The installer runs migration from packaged Helm charts with ordered waits/readiness checks; annotations are tolerant; timestamps are environment-robust (Andrei Kvapil & [**@lllamnyp**](https://github.com/lllamnyp) in #1498; [**@kvaps**](https://github.com/kvaps) in #1475).
|
||||
|
||||
### Webhook / Lineage
|
||||
|
||||
* Add a lineage mutating webhook to auto-label Pods/Secrets/PVCs/Ingresses/WorkloadMonitors with owning app ([**@lllamnyp**](https://github.com/lllamnyp) in #1448, #1497, [**@kvaps**](https://github.com/kvaps) in #1454).
|
||||
* **Name-based** selectors for Secret visibility (templates supported) ([**@lllamnyp**](https://github.com/lllamnyp) in #1477).
|
||||
* Select **Services** and **Ingresses** in CRDs/API; treat them as user-facing when configured ([**@lllamnyp**](https://github.com/lllamnyp) in #1486).
|
||||
* **VictoriaMetrics integration**: Lineage labels are explicitly set on VM resources; `managedMetadata` is configured to avoid controller “fights” over labels ([**@lllamnyp**](https://github.com/lllamnyp) in #1452).
|
||||
* Webhook **excludes** `default` and `kube-system` to avoid unintended mutations (part of the installer/migration hardening by Andrei Kvapil & [**@lllamnyp**](https://github.com/lllamnyp) in #1498).
|
||||
|
||||
### API / Platform
|
||||
|
||||
* Decouple the Cozystack API from Cozystack Resource Definitions to allow independent upgrades ([**@lllamnyp**](https://github.com/lllamnyp) in #1494).
|
||||
* Add **label selectors** to app definitions for Secret include/exclude ([**@lllamnyp**](https://github.com/lllamnyp) in #1447).
|
||||
|
||||
### Monitoring & Ops
|
||||
|
||||
* Reduce node labelsets in target relabeling configs on cadvisor/kubelet metrics to reduce cardinality while keeping useful CPU metrics ([**@IvanHunters**](https://github.com/IvanHunters) in #1455).
|
||||
|
||||
### Storage & Backups
|
||||
|
||||
* PVC expansion in tenant clusters via KubeVirt CSI resizer; RBAC updates (Klinch0 in #1438).
|
||||
* Velero upgraded to **v1.17.0**; node agent enabled by default and a raft of usability features ([**@kvaps**](https://github.com/kvaps) in #1484).
|
||||
|
||||
### Kubernetes/tests & Tooling
|
||||
|
||||
* Smarter Kubernetes test flows: node readiness checks, kubelet version validation, longer rollout waits, per-component readiness ([**@IvanHunters**](https://github.com/IvanHunters) in #1485).
|
||||
|
||||
### UI/Icons
|
||||
|
||||
* New **VM-Disk** SVG icon ([**@kvapsova**](https://github.com/kvapsova) in #1435).
|
||||
|
||||
---
|
||||
|
||||
## Improvements (minor)
|
||||
|
||||
* Make the **Info** app deploy irrespective of OIDC settings ([**klinch0**](https://github.com/klinch0) in #1474).
|
||||
* Move SA token Secret creation to **Info** app ([**@lllamnyp**](https://github.com/lllamnyp) in #1446).
|
||||
* Explicitly set lineage labels for VictoriaMetrics resources ([**@lllamnyp**](https://github.com/lllamnyp) in #1452).
|
||||
|
||||
---
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* **Kubernetes**: fix MachineDeployment `spec.selector` mismatch to ensure proper targeting ([**@kvaps**](https://github.com/kvaps) in #1502).
|
||||
* **Old dashboard**: FerretDB spec typo prevented deploy/display ([**@lllamnyp**](https://github.com/lllamnyp) in #1440).
|
||||
* **SeaweedFS**: fix per-zone size fallback for multi-DC volumes; make migrations more robust ([**@kvaps**](https://github.com/kvaps) in #1476, #1430).
|
||||
* **CoreDNS**: pin tag to v1.12.4 ([**@kvaps**](https://github.com/kvaps) in #1469).
|
||||
* **OIDC**: avoid creating KeycloakRealmGroup before operator API is available ([**@lllamnyp**](https://github.com/lllamnyp) in #1495).
|
||||
* **Kafka**: disable noisy alerts when Kafka isn’t deployed ([**@lllamnyp**](https://github.com/lllamnyp) in #1488).
|
||||
|
||||
---
|
||||
|
||||
## Dependency & version updates
|
||||
|
||||
* **Velero → v1.17.0**; Helm chart v11; node agent default-on ([**@kvaps**](https://github.com/kvaps) in #1484).
|
||||
* **Cilium → v1.17.8** ([**@kvaps**](https://github.com/kvaps) in #1473).
|
||||
* **Flux Operator → v0.29.0** (Kingdon Barrett in #1466).
|
||||
|
||||
---
|
||||
|
||||
## Refactors & chores
|
||||
|
||||
* Remove legacy `versions_map`; unify packaging targets; tighten HelmRelease defaults; replace many chart versions with build-time placeholders ([**@kvaps**](https://github.com/kvaps) in #1453).
|
||||
* Pin CoreDNS image and refresh numerous images ([**@kvaps**](https://github.com/kvaps) in #1469; related image refreshes across #1448 work).
|
||||
|
||||
---
|
||||
|
||||
## Documentation & governance
|
||||
|
||||
* **Contributor Ladder** created and later updated (Timur Tukaev in #1224; Andrei Kvapil & Timur Tukaev in #1492).
|
||||
* **Code of Conduct** updated with a Vendor Neutrality Manifesto (Timur Tukaev in #1493).
|
||||
* **Adopters**: add Hidora (Matthieu Robin in #1429).
|
||||
* **MAINTAINERS**: add/remove entries (Nikita Bykov in #1487; Timur Tukaev in #1491).
|
||||
* **Issue templates**: new bug-report template and tweaks (Moriarti).
|
||||
* **README**: updated dark-theme screenshot ([**@kvaps**](https://github.com/kvaps) in #1459).
|
||||
|
||||
---
|
||||
|
||||
## Breaking changes & upgrade notes
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Security & stability
|
||||
|
||||
31
docs/changelogs/v0.37.1.md
Normal file
31
docs/changelogs/v0.37.1.md
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.37.1
|
||||
-->
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
* **[api] Efficient listing of TenantNamespaces**: Optimized TenantNamespace listing by replacing per-namespace SubjectAccessReview calls with group-based rolebinding checks, significantly reducing API latency and improving performance ([**@lllamnyp**](https://github.com/lllamnyp) in #1507).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[api] Fix RBAC for listing of TenantNamespaces and handle system:masters**: Fixed regression in TenantNamespace listing RBAC and added proper handling for system:masters group to ensure correct authorization ([**@kvaps**](https://github.com/kvaps) in #1511).
|
||||
* **[dashboard] Fix logout**: Fixed dashboard logout functionality to properly clear session and redirect users ([**@kvaps**](https://github.com/kvaps) in #1510).
|
||||
* **[installer] Add additional check to wait for lineage-webhook**: Added additional readiness check to ensure lineage-webhook is fully ready before proceeding with installation, improving upgrade reliability ([**@kvaps**](https://github.com/kvaps) in #1506).
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* **[tests] Make Kubernetes tests POSIX-compatible**: Replaced bash-specific constructs with POSIX-compliant code, ensuring tests work reliably with /bin/sh and improving compatibility across different shell environments ([**@IvanHunters**](https://github.com/IvanHunters) in #1509).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] Update troubleshooting documentation**: Updated Kubernetes installation troubleshooting guide with additional information and fixes ([**@lb0o**](https://github.com/lb0o) in cozystack/website@82beddd).
|
||||
* **[website] Add LLDPD disabling documentation**: Added minimal patch documentation for disabling lldpd based on official LLDPD usage guide ([**@lb0o**](https://github.com/lb0o) in cozystack/website@7ec5d7b).
|
||||
* **[website] Fix typo in utility command**: Fixed typo in utility command documentation ([**@lb0o**](https://github.com/lb0o) in cozystack/website@6c76cb5).
|
||||
* **[website] Update backup and recovery docs**: Updated backup and recovery documentation with latest information ([**@kvaps**](https://github.com/kvaps) in cozystack/website@2781aa5).
|
||||
* **[website] Add Troubleshooting checklist**: Added troubleshooting checklist to help users diagnose and resolve common issues ([**@kvaps**](https://github.com/kvaps) in cozystack/website@59fc304).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.37.0...v0.37.1](https://github.com/cozystack/cozystack/compare/v0.37.0...v0.37.1)
|
||||
|
||||
21
docs/changelogs/v0.37.2.md
Normal file
21
docs/changelogs/v0.37.2.md
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.37.2
|
||||
-->
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
* **[lineage] Separate webhook from cozy controller**: Separated the lineage-controller-webhook from cozystack-controller into a separate daemonset component deployed on all control-plane nodes, reducing API server latency and improving performance by decreasing outgoing API calls. Introduced internal label to track resources already handled by the webhook ([**@lllamnyp**](https://github.com/lllamnyp) in #1515).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[api] Fix listing tenantnamespaces for non-oidc users**: Fixed TenantNamespace listing functionality for users not using OIDC authentication, ensuring proper namespace visibility for all authentication methods ([**@kvaps**](https://github.com/kvaps) in #1517, #1519).
|
||||
|
||||
## Migration and Upgrades
|
||||
|
||||
* **[platform] Better migration for 0.36.2->0.37.2+**: Improved migration script for users upgrading directly from 0.36.2 to 0.37.2+, ensuring the new lineage webhook daemonset is properly deployed and fixing a bug where webhook readiness was not appropriately verified during migration ([**@lllamnyp**](https://github.com/lllamnyp) in #1521, #1522).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.37.1...v0.37.2](https://github.com/cozystack/cozystack/compare/v0.37.1...v0.37.2)
|
||||
|
||||
45
docs/changelogs/v0.37.3.md
Normal file
45
docs/changelogs/v0.37.3.md
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.37.3
|
||||
-->
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
* **[apps] Make VM service user facing**: Virtual machine services are now marked as user-facing, improving service discovery and visibility in the dashboard ([**@lllamnyp**](https://github.com/lllamnyp) in #1523).
|
||||
* **[seaweedfs] Allow users to discover their buckets**: Users can now discover and list their S3 buckets in SeaweedFS, improving usability and bucket management ([**@kvaps**](https://github.com/kvaps) in #1528).
|
||||
* **[seaweedfs] Update SeaweedFS v3.99 and deploy S3 as stacked service**: Updated SeaweedFS to version 3.99 and deployed S3 gateway as a stacked service for better integration and performance ([**@kvaps**](https://github.com/kvaps) in #1562).
|
||||
* **[dashboard] Show service LB IP**: Fixed JSON path issue to correctly display Service LoadBalancer IPs in the dashboard table view, improving visibility of service endpoints ([**@lllamnyp**](https://github.com/lllamnyp) in #1524).
|
||||
* **[dashboard] Update openapi-ui v1.0.3 + fixes**: Updated OpenAPI UI to version 1.0.3 with various fixes and improvements ([**@kvaps**](https://github.com/kvaps) in #1564).
|
||||
* **[kubernetes] Use controlPlane.replicas field**: Fixed managed Kubernetes app to properly use the `controlPlane.replicas` field instead of hardcoding the value, allowing users to configure control plane replica count ([**@lllamnyp**](https://github.com/lllamnyp) in #1556).
|
||||
* **[monitoring] add settings alert for slack**: Added Slack integration configuration for Alerta alerts, enabling notifications to Slack channels ([**@scooby87**](https://github.com/scooby87) in #1545).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[lineage] Check for nil chart in HelmRelease**: Added nil check to prevent crashes when lineage webhook encounters HelmReleases using `chartRef` instead of `chart`, improving stability ([**@lllamnyp**](https://github.com/lllamnyp) in #1525).
|
||||
* **[kamaji] Respect 3rd party labels**: Applied patch to Kamaji controller to respect third-party labels, preventing reconciliation loops between lineage webhook and Kamaji controller ([**@lllamnyp**](https://github.com/lllamnyp) in #1531, #1534).
|
||||
* **[redis-operator] Build patched operator in-tree**: Moved Redis operator build into Cozystack organization and patched it to prevent overwriting third-party labels on owned resources ([**@lllamnyp**](https://github.com/lllamnyp) in #1547).
|
||||
* **[mariadb-operator] Add post-delete job to remove PVCs**: Added post-delete job to automatically remove PersistentVolumeClaims when MariaDB instances are deleted, preventing orphaned storage resources ([**@IvanHunters**](https://github.com/IvanHunters) in #1553).
|
||||
* **[velero] Set defaultItemOperationTimeout=24h**: Set default item operation timeout to 24 hours for Velero backups, preventing timeouts on large backup operations ([**@kvaps**](https://github.com/kvaps) in #1542).
|
||||
|
||||
## Dependencies
|
||||
|
||||
* **Update LINSTOR v1.32.3**: Updated LINSTOR to version 1.32.3 with latest features and bug fixes ([**@kvaps**](https://github.com/kvaps) in #1565).
|
||||
|
||||
## System Configuration
|
||||
|
||||
* **[system] kube-ovn: turn off enableLb**: Disabled load balancer functionality in Kube-OVN configuration ([**@nbykov0**](https://github.com/nbykov0) in #1548).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] Update LINSTOR documentation**: Updated LINSTOR guide and set failmode=continue for ZFS configurations ([**@kvaps**](https://github.com/kvaps) in cozystack/website@033804e).
|
||||
* **[website] Update managed apps reference**: Updated managed applications reference documentation ([**@kvaps**](https://github.com/kvaps) in cozystack/website@b886a74).
|
||||
* **[website] Update external apps documentation**: Updated documentation for external applications ([**@kvaps**](https://github.com/kvaps) in cozystack/website@565dad9).
|
||||
* **[website] Add naming conventions**: Added naming conventions documentation ([**@kvaps**](https://github.com/kvaps) in cozystack/website@b227abb).
|
||||
* **[website] Update golden image documentation**: Updated documentation for creating golden images for virtual machines ([**@kvaps**](https://github.com/kvaps) in cozystack/website@34c2f3a, cozystack/website@ef65593).
|
||||
* **[website] Fix documentation formatting**: Fixed alerts, infoboxes, tabs styles and main page formatting ([**@kvaps**](https://github.com/kvaps) in cozystack/website@e992e97, cozystack/website@b2c4dee).
|
||||
* **[website] Fix typo in blog article**: Fixed typo in blog article ([**@kvaps**](https://github.com/kvaps) in cozystack/website@0a4bbf3).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.37.2...v0.37.3](https://github.com/cozystack/cozystack/compare/v0.37.2...v0.37.3)
|
||||
|
||||
29
docs/changelogs/v0.37.4.md
Normal file
29
docs/changelogs/v0.37.4.md
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.37.4
|
||||
-->
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
* **[tenant] Allow listing workloads**: Enabled listing of workloads for tenants, improving visibility and management of tenant resources ([**@kvaps**](https://github.com/kvaps) in #1576, #1577).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[seaweedfs] Fix migration to v3.99**: Fixed migration issues when upgrading SeaweedFS to version 3.99, ensuring smooth upgrades ([**@kvaps**](https://github.com/kvaps) in #1572, #1575).
|
||||
* **[nats] Merge container spec, not podTemplate**: Fixed NATS configuration to properly merge container specifications instead of podTemplate, ensuring correct container configuration ([**@lllamnyp**](https://github.com/lllamnyp) in #1571, #1574).
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* **[e2e] Increase Kubernetes connection timeouts**: Increased connection and request timeouts in E2E tests when communicating with Kubernetes API, improving test stability under high load and slow cluster response conditions ([**@IvanHunters**](https://github.com/IvanHunters) in #1570, #1573).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] Optimize website for mobile devices**: Improved website layout and responsiveness for mobile devices ([**@kvaps**](https://github.com/kvaps) in cozystack/website@3ab2338).
|
||||
* **[website] Add OpenAPI UI**: Added OpenAPI UI documentation and integration ([**@kvaps**](https://github.com/kvaps) in cozystack/website@b1c1668).
|
||||
* **[website] Update Cozystack video in hero banner**: Updated hero banner with new Cozystack video ([**@kvaps**](https://github.com/kvaps) in cozystack/website@e351137).
|
||||
* **[website] Add screenshots carousel**: Added screenshots carousel to showcase Cozystack features ([**@kvaps**](https://github.com/kvaps) in cozystack/website@8422bd0).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.37.3...v0.37.4](https://github.com/cozystack/cozystack/compare/v0.37.3...v0.37.4)
|
||||
|
||||
28
docs/changelogs/v0.37.5.md
Normal file
28
docs/changelogs/v0.37.5.md
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.37.5
|
||||
-->
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
* **[dashboard-controller] Move badges generation logic to internal dashboard component**: Moved badges generation logic to internal dashboard component for better code organization and maintainability ([**@kvaps**](https://github.com/kvaps) in #1567).
|
||||
|
||||
## Security
|
||||
|
||||
* **[redis] Bump Redis image version for security fixes**: Updated Redis image version to include latest security fixes, improving cluster security ([**@IvanHunters**](https://github.com/IvanHunters) in #1580).
|
||||
* **[flux] Close Flux Operator ports to external access**: Removed hostPort and hostNetwork from Flux Operator Deployment, ensuring ports 8080 and 8081 are only accessible within the cluster, preventing external exposure and improving security ([**@IvanHunters**](https://github.com/IvanHunters) in #1581).
|
||||
* **[ingress] Enforce HTTPS-only for API**: Added force-ssl-redirect annotation to default API Ingress, ensuring all HTTP traffic is redirected to HTTPS, preventing unencrypted external access and improving security ([**@IvanHunters**](https://github.com/IvanHunters) in #1582, #1585).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[nats] Fixes for NATS App Helm chart, fix template issues with config.merge**: Fixed template issues in NATS Helm chart related to config.merge value, ensuring correct configuration ([**@insignia96**](https://github.com/insignia96) in #1583, #1591).
|
||||
* **[kubevirt] Fix: kubevirt metrics rule**: Fixed KubeVirt metrics rule configuration ([**@kvaps**](https://github.com/kvaps) in #1584, #1588).
|
||||
|
||||
## System Configuration
|
||||
|
||||
* **[core] rm talos lldp extension**: Removed Talos LLDP extension from core configuration ([**@nbykov0**](https://github.com/nbykov0) in #1586).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.37.4...v0.37.5](https://github.com/cozystack/cozystack/compare/v0.37.4...v0.37.5)
|
||||
|
||||
30
docs/changelogs/v0.37.6.md
Normal file
30
docs/changelogs/v0.37.6.md
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.37.6
|
||||
-->
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
* **[api] Use shared informer cache**: Optimized API server by using shared informer cache, reducing API server load and improving performance ([**@lllamnyp**](https://github.com/lllamnyp) in #1539).
|
||||
* **[dashboard] sync with upstream & enhancements**: Synchronized dashboard with upstream and added various enhancements ([**@kvaps**](https://github.com/kvaps) in #1603).
|
||||
* **[cozystack-api][dashboard] Fix filtering for application services/ingresses/secrets**: Fixed filtering functionality for application services, ingresses, and secrets in both API and dashboard ([**@kvaps**](https://github.com/kvaps) in #1612).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[controller] Remove crdmem, handle DaemonSet**: Removed crdmem and improved DaemonSet handling in controller ([**@lllamnyp**](https://github.com/lllamnyp) in #1555).
|
||||
* **[dashboard] Revert reconciler removal**: Reverted reconciler removal to restore proper dashboard functionality ([**@lllamnyp**](https://github.com/lllamnyp) in #1559).
|
||||
* **[dashboard-controller] Fix static resources reconciliation and showing secrets**: Fixed static resources reconciliation and improved secret display in dashboard controller ([**@kvaps**](https://github.com/kvaps) in #1605).
|
||||
* **[api,lineage] Ensure node-local traffic**: Ensured node-local traffic handling for API and lineage components ([**@lllamnyp**](https://github.com/lllamnyp) in #1606).
|
||||
* **[virtual-machine] Revert per-vm network policies**: Reverted per-VM network policies to previous behavior ([**@lllamnyp**](https://github.com/lllamnyp) in #1611).
|
||||
* **[cozy-lib] Fix: handling resources=nil**: Fixed handling of nil resources in cozy-lib templates ([**@kvaps**](https://github.com/kvaps) in #1607).
|
||||
* **[nats] Use dig function to check for existing secret and prevent nil indexing**: Fixed NATS app chart to use dig function for checking existing secrets and prevent nil indexing errors ([**@kvaps**](https://github.com/kvaps) in #1609, #1610).
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* **[cozystack-controller] improve API tests**: Improved API tests for cozystack-controller ([**@lllamnyp**](https://github.com/lllamnyp) in #1599).
|
||||
* **[kubernetes] Helm hooks for cleanup**: Added Helm hooks for cleanup operations in Kubernetes app ([**@lllamnyp**](https://github.com/lllamnyp) in #1616).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.37.5...v0.37.6](https://github.com/cozystack/cozystack/compare/v0.37.5...v0.37.6)
|
||||
|
||||
18
docs/changelogs/v0.37.7.md
Normal file
18
docs/changelogs/v0.37.7.md
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.37.7
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[kubernetes] Cleanup loadbalancer services**: Added cleanup functionality for load balancer services in Kubernetes app ([**@lllamnyp**](https://github.com/lllamnyp) in #1622).
|
||||
* **[rbac] Fix permissions for high-privilege users**: Fixed RBAC permissions for high-privilege users, ensuring proper access control ([**@lllamnyp**](https://github.com/lllamnyp) in #1624).
|
||||
|
||||
## System Configuration
|
||||
|
||||
* **[system] kubeovn: increase limits**: Increased resource limits for Kube-OVN components to improve stability and performance ([**@nbykov0**](https://github.com/nbykov0) in #1629).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.37.6...v0.37.7](https://github.com/cozystack/cozystack/compare/v0.37.6...v0.37.7)
|
||||
|
||||
19
docs/changelogs/v0.37.8.md
Normal file
19
docs/changelogs/v0.37.8.md
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.37.8
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[cozy-lib] Fix malformed ResourceQuota rendering for LoadBalancer services**: Fixed malformed ResourceQuota rendering for LoadBalancer services in cozy-lib templates ([**@IvanHunters**](https://github.com/IvanHunters) in #1642).
|
||||
* **[extra] ingress: rm spaces from external ip list**: Removed spaces from external IP list in ingress configuration, fixing formatting issues ([**@nbykov0**](https://github.com/nbykov0) in #1652).
|
||||
* **scripts: fix 20 migration**: Fixed migration script #20 to ensure proper execution during upgrades ([**@nbykov0**](https://github.com/nbykov0) in #1653).
|
||||
|
||||
## System Configuration
|
||||
|
||||
* **Increase strimzi memory limit**: Increased memory limit for Strimzi Kafka operator to improve stability and performance ([**@nbykov0**](https://github.com/nbykov0) in #1651).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.37.7...v0.37.8](https://github.com/cozystack/cozystack/compare/v0.37.7...v0.37.8)
|
||||
|
||||
19
docs/changelogs/v0.37.9.md
Normal file
19
docs/changelogs/v0.37.9.md
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.37.9
|
||||
-->
|
||||
|
||||
## Improvements
|
||||
|
||||
* **[seaweedfs] Extended CA certificate duration to reduce disruptive CA rotations**: Extended CA certificate duration to reduce disruptive CA rotations. ([**@IvanHunters**](https://github.com/IvanHunters) in #1657, #1666).
|
||||
* **[dashboard] Add config hash annotations to restart pods on config changes**: Added config hash annotations to restart pods when configuration changes, ensuring pods are automatically restarted when their configuration is updated ([**@kvaps**](https://github.com/kvaps) in #1662, #1665).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[tenant][kubernetes] Introduce better cleanup logic**: Improved cleanup logic for tenant Kubernetes resources, ensuring proper resource cleanup when tenants are deleted or updated ([**@kvaps**](https://github.com/kvaps) in #1661).
|
||||
* **[dashboard] Fix loading arrays in forms when editing existing objects**: Fixed issue where arrays in forms were not loading correctly when editing existing objects in the dashboard ([**@kvaps**](https://github.com/kvaps)).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.37.8...v0.37.9](https://github.com/cozystack/cozystack/compare/v0.37.8...v0.37.9)
|
||||
|
||||
235
docs/changelogs/v0.38.0.md
Normal file
235
docs/changelogs/v0.38.0.md
Normal file
@@ -0,0 +1,235 @@
|
||||
# Cozystack v0.38 — "VPC & Enhanced Networking"
|
||||
|
||||
This release introduces **Virtual Private Cloud (VPC)** support, enabling advanced networking capabilities for tenant applications. We've also added VNC console support in the dashboard, made Kubernetes worker versions configurable, and delivered numerous improvements and fixes across the platform.
|
||||
|
||||
### Virtual Private Cloud (VPC) Networking
|
||||
|
||||
Cozystack v0.38.0 introduces Virtual Private Cloud (VPC) support, enabling platform administrators to create isolated network segments for tenant applications. VPCs provide network isolation and allow fine-grained control over network topology, subnets, and routing. Each VPC can contain multiple subnets, and administrators can configure subnet details including IP ranges, gateway settings, and DNS configuration.
|
||||
|
||||
The VPC feature integrates seamlessly with the Cozystack dashboard, allowing users to view and manage VPCs and their subnets through an intuitive interface. Subnet details are exposed in the dashboard as tables, making it easy to understand network configuration at a glance. VPC configuration is stored in ConfigMaps with predictable naming, ensuring reliable access to subnet information.
|
||||
|
||||
This feature is particularly valuable for multi-tenant environments where network isolation is critical, and for applications that require specific network configurations or routing rules.
|
||||
|
||||
### VNC Console for Virtual Machines
|
||||
|
||||
The Cozystack dashboard now includes a built-in VNC console for virtual machines, enabling users to access VM console directly from the web interface without requiring external tools. This feature provides immediate access to virtual machine consoles for troubleshooting, configuration, and maintenance tasks. The VNC console integration streamlines VM management workflows and improves the user experience by keeping all VM operations within the Cozystack dashboard.
|
||||
|
||||
## Highlights
|
||||
|
||||
* **Virtual Private Cloud (VPC)**: New VPC system module enables advanced networking with Multus CNI, subnet management, and network isolation for tenant applications ([**@nbykov0**](https://github.com/nbykov0) in #1543; [**@lllamnyp**](https://github.com/lllamnyp) in #1587, #1590, #1600, #1621, #1638).
|
||||
* **VNC Console in Dashboard**: Users can now access virtual machine consoles directly from the dashboard, improving VM management experience ([**@kvaps**](https://github.com/kvaps) in #1627).
|
||||
* **Configurable Kubernetes Worker Versions**: Platform administrators can now configure Kubernetes worker node versions independently, providing more flexibility in cluster management ([**@lllamnyp**](https://github.com/lllamnyp) in #1619).
|
||||
* **Security Enhancements**: Multiple security improvements including HTTPS-only enforcement for API, closed Flux Operator ports, and Redis security updates ([**@IvanHunters**](https://github.com/IvanHunters) in #1580, #1581, #1582).
|
||||
* **Cozy-lib Improvements**: Enhanced flatten function with better ResourceQuota handling and nil resource support ([**@lllamnyp**](https://github.com/lllamnyp) in #1647; [**@IvanHunters**](https://github.com/IvanHunters) in #1642; [**@kvaps**](https://github.com/kvaps) in #1607).
|
||||
|
||||
---
|
||||
|
||||
## New features
|
||||
|
||||
### VPC (Virtual Private Cloud)
|
||||
|
||||
* **[system] Add VPC**: Introduced Virtual Private Cloud system module with Multus CNI integration, enabling advanced networking capabilities for tenant applications ([**@nbykov0**](https://github.com/nbykov0) in #1543).
|
||||
* **[vpc] Install Multus by default**: Multus CNI is now installed by default when VPC is enabled, providing multi-network interface support ([**@lllamnyp**](https://github.com/lllamnyp) in #1587).
|
||||
* **[vpc] Give predictable name to subnet configmap**: Subnet configuration maps now use predictable naming for better management and debugging ([**@lllamnyp**](https://github.com/lllamnyp) in #1590).
|
||||
* **[vpc] Entry per subnet in the subnets configmap**: Each subnet now has its own entry in the subnets configmap, improving subnet organization and management ([**@lllamnyp**](https://github.com/lllamnyp) in #1600).
|
||||
* **[vpc,dashboard] Print subnet details as table**: Subnet details are now displayed as a table in the dashboard, improving visibility and management ([**@lllamnyp**](https://github.com/lllamnyp) in #1621).
|
||||
* **[apps] Add VPC app**: Added VPC application for tenant use, enabling users to create and manage VPCs ([**@nbykov0**](https://github.com/nbykov0) in #1543).
|
||||
|
||||
### Dashboard
|
||||
|
||||
* **[dashboard] Introduce VNC console**: Added VNC console support in the dashboard, allowing users to access virtual machine consoles directly from the web interface ([**@kvaps**](https://github.com/kvaps) in #1627).
|
||||
* **[dashboard] sync with upstream & enhancements**: Synchronized dashboard with upstream project and added various enhancements ([**@kvaps**](https://github.com/kvaps) in #1603).
|
||||
* **[dashboard] Migrate patches to upstream project**: Migrated dashboard patches to upstream project for better maintainability ([**@kvaps**](https://github.com/kvaps) in #1569).
|
||||
|
||||
### Kubernetes
|
||||
|
||||
* **[kubernetes] Make worker version configurable**: Platform administrators can now configure Kubernetes worker node versions independently from control plane versions, providing more flexibility ([**@lllamnyp**](https://github.com/lllamnyp) in #1619).
|
||||
* **[kubernetes] Use controlPlane.replicas field**: Fixed managed Kubernetes app to properly use the `controlPlane.replicas` field instead of hardcoding the value ([**@lllamnyp**](https://github.com/lllamnyp) in #1556).
|
||||
* **[kubernetes] Helm hooks for cleanup**: Added Helm hooks for cleanup operations in Kubernetes app ([**@lllamnyp**](https://github.com/lllamnyp) in #1606).
|
||||
|
||||
### API & Platform
|
||||
|
||||
* **[api] Efficient listing of TenantNamespaces**: Optimized TenantNamespace listing by replacing per-namespace SubjectAccessReview calls with group-based rolebinding checks, significantly reducing API latency ([**@lllamnyp**](https://github.com/lllamnyp) in #1507).
|
||||
* **[api] Use shared informer cache**: Optimized API server by using shared informer cache, reducing API server load and improving performance ([**@lllamnyp**](https://github.com/lllamnyp) in #1539).
|
||||
* **[api] Fix representation of dynamic list kinds**: Fixed API representation of dynamic list kinds for better compatibility ([**@lllamnyp**](https://github.com/lllamnyp) in #1630).
|
||||
* **[api] Delete previous instance when changing type**: API now properly deletes previous instance when changing application type ([**@lllamnyp**](https://github.com/lllamnyp) in #1579).
|
||||
|
||||
### Applications
|
||||
|
||||
* **[tenant] Allow listing workloads**: Enabled listing of workloads for tenants, improving visibility and management of tenant resources ([**@kvaps**](https://github.com/kvaps) in #1576).
|
||||
* **[apps] Make VM service user facing**: Virtual machine services are now marked as user-facing, improving service discovery and visibility in the dashboard ([**@lllamnyp**](https://github.com/lllamnyp) in #1523).
|
||||
* **[foundationdb] Upgrade FDB app for latest Cozy**: Upgraded FoundationDB application for compatibility with latest Cozystack version ([**@lllamnyp**](https://github.com/lllamnyp) in #1505).
|
||||
|
||||
### Storage & Backups
|
||||
|
||||
* **[seaweedfs] Update SeaweedFS v3.99 and deploy S3 as stacked service**: Updated SeaweedFS to version 3.99 and deployed S3 gateway as a stacked service for better integration and performance ([**@kvaps**](https://github.com/kvaps) in #1562).
|
||||
* **[seaweedfs] Allow users to discover their buckets**: Users can now discover and list their S3 buckets in SeaweedFS, improving usability and bucket management ([**@kvaps**](https://github.com/kvaps) in #1528).
|
||||
* **[velero] Set defaultItemOperationTimeout=24h**: Set default item operation timeout to 24 hours for Velero backups, preventing timeouts on large backup operations ([**@kvaps**](https://github.com/kvaps) in #1542).
|
||||
|
||||
### Monitoring & Operations
|
||||
|
||||
* **[monitoring] add settings alert for slack**: Added Slack integration configuration for Alerta alerts, enabling notifications to Slack channels ([**@scooby87**](https://github.com/scooby87) in #1545).
|
||||
|
||||
---
|
||||
|
||||
## Improvements (minor)
|
||||
|
||||
* **[lineage] Separate webhook from cozy controller**: Separated the lineage-controller-webhook from cozystack-controller into a separate daemonset component deployed on all control-plane nodes, reducing API server latency ([**@lllamnyp**](https://github.com/lllamnyp) in #1515).
|
||||
* **[dashboard] Show service LB IP**: Fixed JSON path issue to correctly display Service LoadBalancer IPs in the dashboard table view ([**@lllamnyp**](https://github.com/lllamnyp) in #1524).
|
||||
* **[dashboard] Update openapi-ui v1.0.3 + fixes**: Updated OpenAPI UI to version 1.0.3 with various fixes and improvements ([**@kvaps**](https://github.com/kvaps) in #1564).
|
||||
* **[dashboard-controller] Move badges generation logic to internal dashboard component**: Moved badges generation logic to internal dashboard component for better code organization ([**@kvaps**](https://github.com/kvaps) in #1567).
|
||||
* **[bucket] Expose bucket name in secrets**: Bucket names are now exposed in secrets for better integration with applications ([**@lllamnyp**](https://github.com/lllamnyp) in #1518).
|
||||
* **[platform] Better migration for 0.36.2->0.37.2+**: Improved migration script for users upgrading directly from 0.36.2 to 0.37.2+ ([**@lllamnyp**](https://github.com/lllamnyp) in #1521).
|
||||
* **[cozy-lib] Improve flatten function**: Improved flatten function in cozy-lib with better handling of complex resource structures ([**@lllamnyp**](https://github.com/lllamnyp) in #1647).
|
||||
* **[dx] JSDoc compatible syntax for values.yaml**: Added JSDoc compatible syntax for values.yaml documentation ([**@kvaps**](https://github.com/kvaps) in #1536).
|
||||
* **[system] Tune kubevirt rollout and eviction settings**: Tuned KubeVirt rollout and eviction settings for better stability ([**@nbykov0**](https://github.com/nbykov0) in #1544).
|
||||
* **[system] multus: update to the latest version**: Updated Multus CNI to the latest version ([**@nbykov0**](https://github.com/nbykov0) in #1628).
|
||||
* **[system] kubeovn: increase limits**: Increased resource limits for Kube-OVN components to improve stability and performance ([**@nbykov0**](https://github.com/nbykov0) in #1629).
|
||||
* **[linstor] Update Piraeus Operator to v2.10.1 to enable RWX support**: Updated Piraeus Operator to v2.10.1, enabling ReadWriteMany (RWX) volume support ([**@kvaps**](https://github.com/kvaps) in #1650).
|
||||
* **[ci,dx] Bump MariaDB operator version**: Bumped MariaDB operator version for latest features and bug fixes ([**@IvanHunters**](https://github.com/IvanHunters) in #1646).
|
||||
|
||||
---
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* **[api] Fix RBAC for listing of TenantNamespaces and handle system:masters**: Fixed regression in TenantNamespace listing RBAC and added proper handling for system:masters group ([**@kvaps**](https://github.com/kvaps) in #1511).
|
||||
* **[api] Fix listing tenantnamespaces for non-oidc users**: Fixed TenantNamespace listing functionality for users not using OIDC authentication ([**@kvaps**](https://github.com/kvaps) in #1517).
|
||||
* **[dashboard] Fix logout**: Fixed dashboard logout functionality to properly clear session and redirect users ([**@kvaps**](https://github.com/kvaps) in #1510).
|
||||
* **[installer] Add additional check to wait for lineage-webhook**: Added additional readiness check to ensure lineage-webhook is fully ready before proceeding with installation ([**@kvaps**](https://github.com/kvaps) in #1506).
|
||||
* **[lineage] Check for nil chart in HelmRelease**: Added nil check to prevent crashes when lineage webhook encounters HelmReleases using `chartRef` instead of `chart` ([**@lllamnyp**](https://github.com/lllamnyp) in #1525).
|
||||
* **[kamaji] Respect 3rd party labels**: Applied patch to Kamaji controller to respect third-party labels, preventing reconciliation loops ([**@lllamnyp**](https://github.com/lllamnyp) in #1531).
|
||||
* **[redis-operator] Build patched operator in-tree**: Moved Redis operator build into Cozystack organization and patched it to prevent overwriting third-party labels ([**@lllamnyp**](https://github.com/lllamnyp) in #1547).
|
||||
* **[mariadb-operator] Add post-delete job to remove PVCs**: Added post-delete job to automatically remove PersistentVolumeClaims when MariaDB instances are deleted ([**@IvanHunters**](https://github.com/IvanHunters) in #1553).
|
||||
* **[seaweedfs] Fix migration to v3.99**: Fixed migration issues when upgrading SeaweedFS to version 3.99 ([**@kvaps**](https://github.com/kvaps) in #1572).
|
||||
* **[nats] Merge container spec, not podTemplate**: Fixed NATS configuration to properly merge container specifications instead of podTemplate ([**@lllamnyp**](https://github.com/lllamnyp) in #1571).
|
||||
* **[nats] Fixes for NATS App Helm chart, fix template issues with config.merge**: Fixed template issues in NATS Helm chart related to config.merge value ([**@insignia96**](https://github.com/insignia96) in #1583).
|
||||
* **[nats] Fix NATS app chart to use existing secret credentials when present**: Fixed NATS app chart to use existing secret credentials when present, preventing credential regeneration ([**@insignia96**](https://github.com/insignia96) in #1599).
|
||||
* **[kubevirt] Fix: kubevirt metrics rule**: Fixed KubeVirt metrics rule configuration ([**@kvaps**](https://github.com/kvaps) in #1584).
|
||||
* **[controller] Remove crdmem, handle DaemonSet**: Removed crdmem and improved DaemonSet handling in controller ([**@lllamnyp**](https://github.com/lllamnyp) in #1555).
|
||||
* **[dashboard] Revert reconciler removal**: Reverted reconciler removal to restore proper dashboard functionality ([**@lllamnyp**](https://github.com/lllamnyp) in #1559).
|
||||
* **[dashboard-controller] Fix static resources reconciliation and showing secrets**: Fixed static resources reconciliation and improved secret display in dashboard controller ([**@kvaps**](https://github.com/kvaps) in #1615).
|
||||
* **[cozystack-api][dashboard] Fix filtering for application services/ingresses/secrets**: Fixed filtering functionality for application services, ingresses, and secrets in both API and dashboard ([**@kvaps**](https://github.com/kvaps) in #1612).
|
||||
* **[virtual-machine] Revert per-vm network policies**: Reverted per-VM network policies to previous behavior ([**@kvaps**](https://github.com/kvaps) in #1611).
|
||||
* **[cozy-lib] Fix: handling resources=nil**: Fixed handling of nil resources in cozy-lib templates ([**@kvaps**](https://github.com/kvaps) in #1607).
|
||||
* **[cozy-lib] Fix malformed ResourceQuota rendering for LoadBalancer services**: Fixed malformed ResourceQuota rendering for LoadBalancer services in cozy-lib templates ([**@IvanHunters**](https://github.com/IvanHunters) in #1642).
|
||||
* **[kubernetes] Cleanup loadbalancer services**: Added cleanup functionality for load balancer services in Kubernetes app ([**@lllamnyp**](https://github.com/lllamnyp) in #1631).
|
||||
* **[rbac] Fix permissions for high-privilege users**: Fixed RBAC permissions for high-privilege users, ensuring proper access control ([**@lllamnyp**](https://github.com/lllamnyp) in #1622).
|
||||
* **[vpc] Fix access to subnet details configmap**: Fixed access to subnet details configmap in VPC functionality ([**@lllamnyp**](https://github.com/lllamnyp) in #1638).
|
||||
* **[api,lineage] Ensure node-local traffic**: Ensured node-local traffic handling for API and lineage components ([**@lllamnyp**](https://github.com/lllamnyp) in #1554).
|
||||
* **[extra] ingress: rm spaces from external ip list**: Removed spaces from external IP list in ingress configuration, fixing formatting issues ([**@nbykov0**](https://github.com/nbykov0) in #1652).
|
||||
* **scripts: fix 20 migration**: Fixed migration script #20 to ensure proper execution during upgrades ([**@nbykov0**](https://github.com/nbykov0) in #1653).
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
* **[redis] Bump Redis image version for security fixes**: Updated Redis image version to include latest security fixes, improving cluster security ([**@IvanHunters**](https://github.com/IvanHunters) in #1580).
|
||||
* **[flux] Close Flux Operator ports to external access**: Removed hostPort and hostNetwork from Flux Operator Deployment, ensuring ports 8080 and 8081 are only accessible within the cluster ([**@IvanHunters**](https://github.com/IvanHunters) in #1581).
|
||||
* **[ingress] Enforce HTTPS-only for API**: Added force-ssl-redirect annotation to default API Ingress, ensuring all HTTP traffic is redirected to HTTPS ([**@IvanHunters**](https://github.com/IvanHunters) in #1582).
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & version updates
|
||||
|
||||
* **Update LINSTOR v1.32.3**: Updated LINSTOR to version 1.32.3 with latest features and bug fixes ([**@kvaps**](https://github.com/kvaps) in #1565).
|
||||
* **Update Talos Linux v1.11.3**: Updated Talos Linux to version 1.11.3 ([**@kvaps**](https://github.com/kvaps) in #1527).
|
||||
* **Update Kube-OVN v1.14.11**: Updated Kube-OVN to version 1.14.11 ([**@kvaps**](https://github.com/kvaps) in #1514).
|
||||
* **[linstor] Update Piraeus Operator to v2.10.1**: Updated Piraeus Operator to v2.10.1 to enable RWX support ([**@kvaps**](https://github.com/kvaps) in #1650).
|
||||
* **[system] multus: update to the latest version**: Updated Multus CNI to the latest version ([**@nbykov0**](https://github.com/nbykov0) in #1628).
|
||||
* **[ci,dx] Bump MariaDB operator version**: Bumped MariaDB operator version ([**@IvanHunters**](https://github.com/IvanHunters) in #1646).
|
||||
* **Increase strimzi memory limit**: Increased memory limit for Strimzi Kafka operator to improve stability and performance ([**@nbykov0**](https://github.com/nbykov0) in #1651).
|
||||
|
||||
---
|
||||
|
||||
## System Configuration
|
||||
|
||||
* **[system] kube-ovn: turn off enableLb**: Disabled load balancer functionality in Kube-OVN configuration ([**@nbykov0**](https://github.com/nbykov0) in #1548).
|
||||
* **[core] rm talos lldp extension**: Removed Talos LLDP extension from core configuration ([**@nbykov0**](https://github.com/nbykov0) in #1586).
|
||||
|
||||
---
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* **[tests] Make Kubernetes tests POSIX-compatible**: Replaced bash-specific constructs with POSIX-compliant code, ensuring tests work reliably with /bin/sh ([**@IvanHunters**](https://github.com/IvanHunters) in #1509).
|
||||
* **[ferretdb] fix tests**: Fixed FerretDB tests to ensure proper execution ([**@IvanHunters**](https://github.com/IvanHunters) in #1540).
|
||||
* **[e2e] Increase Kubernetes connection timeouts**: Increased connection and request timeouts in E2E tests when communicating with Kubernetes API ([**@IvanHunters**](https://github.com/IvanHunters) in #1570).
|
||||
* **[cozystack-controller] improve API tests**: Improved API tests for cozystack-controller ([**@kvaps**](https://github.com/kvaps) in #1617).
|
||||
* **[ci] Fix build from external forks**: Fixed build process to work correctly from external forks ([**@kvaps**](https://github.com/kvaps) in #1530).
|
||||
* **[ci,dx] Add unit tests for cozy-lib**: Added unit tests for cozy-lib to improve code quality and reliability ([**@lllamnyp**](https://github.com/lllamnyp) in #1643).
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] Add VPC page**: Added VPC documentation page explaining VPC features and usage ([**@nbykov0**](https://github.com/nbykov0) in cozystack/website@9ccac78).
|
||||
* **[website] Add VPC to auto-update list**: Added VPC to auto-update list in documentation ([**@nbykov0**](https://github.com/nbykov0) in cozystack/website@ca2bce6).
|
||||
* **[website] Update dashboard part in OIDC configuration doc**: Updated OIDC configuration documentation with dashboard information ([**@nbykov0**](https://github.com/nbykov0) in cozystack/website@6c44b93).
|
||||
* **[website] Update storage requirements**: Updated storage requirements documentation ([**@nbykov0**](https://github.com/nbykov0) in cozystack/website@cac3af6).
|
||||
* **[website] Add System Resource Planning Recommendations**: Added system resource planning recommendations documentation ([**@kvaps**](https://github.com/kvaps) in cozystack/website@c877c2a).
|
||||
* **[website] Optimize website for mobile devices**: Improved website layout and responsiveness for mobile devices ([**@kvaps**](https://github.com/kvaps) in cozystack/website@3ab2338).
|
||||
* **[website] Add OpenAPI UI**: Added OpenAPI UI documentation and integration ([**@kvaps**](https://github.com/kvaps) in cozystack/website@b1c1668).
|
||||
* **[website] Update Cozystack video in hero banner**: Updated hero banner with new Cozystack video ([**@kvaps**](https://github.com/kvaps) in cozystack/website@e351137).
|
||||
* **[website] Add screenshots carousel**: Added screenshots carousel to showcase Cozystack features ([**@kvaps**](https://github.com/kvaps) in cozystack/website@8422bd0).
|
||||
* **[website] Update LINSTOR documentation**: Updated LINSTOR guide and set failmode=continue for ZFS configurations ([**@kvaps**](https://github.com/kvaps) in cozystack/website@033804e).
|
||||
* **[website] Update managed apps reference**: Updated managed applications reference documentation ([**@kvaps**](https://github.com/kvaps) in cozystack/website@b886a74, cozystack/website@41c1849, cozystack/website@0ab71fd).
|
||||
* **[website] Update external apps documentation**: Updated documentation for external applications ([**@kvaps**](https://github.com/kvaps) in cozystack/website@565dad9).
|
||||
* **[website] Add naming conventions**: Added naming conventions documentation ([**@kvaps**](https://github.com/kvaps) in cozystack/website@b227abb).
|
||||
* **[website] Update golden image documentation**: Updated documentation for creating golden images for virtual machines ([**@kvaps**](https://github.com/kvaps) in cozystack/website@34c2f3a, cozystack/website@ef65593).
|
||||
* **[website] Fix documentation formatting**: Fixed alerts, infoboxes, tabs styles and main page formatting ([**@kvaps**](https://github.com/kvaps) in cozystack/website@e992e97, cozystack/website@b2c4dee).
|
||||
* **[website] Fix typo in blog article**: Fixed typo in blog article ([**@kvaps**](https://github.com/kvaps) in cozystack/website@0a4bbf3).
|
||||
* **[apps] vpc: more docs**: Added more VPC documentation ([**@nbykov0**](https://github.com/nbykov0) in #1594).
|
||||
* **[apps] vpc: fix typo in README**: Fixed typo in VPC README ([**@nbykov0**](https://github.com/nbykov0) in #1637).
|
||||
|
||||
---
|
||||
|
||||
## Additional Repositories
|
||||
|
||||
### boot-to-talos
|
||||
|
||||
* **[boot-to-talos] Introduce boot/install mode**: Introduced boot/install mode in boot-to-talos tool ([**@kvaps**](https://github.com/kvaps) in cozystack/boot-to-talos#5).
|
||||
|
||||
### cozypkg
|
||||
|
||||
* **[cozypkg] Handle valuesFiles from cozypkg.cozystack.io/values-files annotation**: Added support for handling valuesFiles from annotation in cozypkg ([**@kvaps**](https://github.com/kvaps) in cozystack/cozypkg#8).
|
||||
|
||||
---
|
||||
|
||||
## Refactors & chores
|
||||
|
||||
* **[dashboard] Migrate patches to upstream project**: Migrated dashboard patches to upstream project for better maintainability ([**@kvaps**](https://github.com/kvaps) in #1569).
|
||||
* **Update CODEOWNERS**: Updated CODEOWNERS file ([**@nbykov0**](https://github.com/nbykov0) in #1537).
|
||||
* **Add QOSI to ADOPTERS.md**: Added QOSI to adopters list ([**@tabu-a**](https://github.com/tabu-a) in #1589).
|
||||
|
||||
---
|
||||
|
||||
## Breaking changes & upgrade notes
|
||||
|
||||
No breaking changes in this release.
|
||||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
We'd like to thank all contributors who made this release possible:
|
||||
|
||||
* [**@IvanHunters**](https://github.com/IvanHunters)
|
||||
* [**@insignia96**](https://github.com/insignia96)
|
||||
* [**@kvaps**](https://github.com/kvaps)
|
||||
* [**@lllamnyp**](https://github.com/lllamnyp)
|
||||
* [**@nbykov0**](https://github.com/nbykov0)
|
||||
* [**@scooby87**](https://github.com/scooby87)
|
||||
* [**@tabu-a**](https://github.com/tabu-a)
|
||||
|
||||
### New Contributors
|
||||
|
||||
We're excited to welcome our first-time contributors:
|
||||
|
||||
* [**@tabu-a**](https://github.com/tabu-a) - First contribution!
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.37.0...v0.38.0](https://github.com/cozystack/cozystack/compare/v0.37.0...v0.38.0)
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.38.0
|
||||
-->
|
||||
19
docs/changelogs/v0.38.1.md
Normal file
19
docs/changelogs/v0.38.1.md
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.38.1
|
||||
-->
|
||||
|
||||
## Improvements
|
||||
|
||||
* **[seaweedfs] Extended CA certificate duration to reduce disruptive CA rotations**: Extended CA certificate duration to reduce disruptive CA rotations. ([**@IvanHunters**](https://github.com/IvanHunters) in #1657, #1666).
|
||||
* **[dashboard] Add config hash annotations to restart pods on config changes**: Added config hash annotations to restart pods when configuration changes, ensuring pods are automatically restarted when their configuration is updated ([**@kvaps**](https://github.com/kvaps) in #1662, #1665).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[tenant][kubernetes] Introduce better cleanup logic**: Improved cleanup logic for tenant Kubernetes resources, ensuring proper resource cleanup when tenants are deleted or updated ([**@kvaps**](https://github.com/kvaps) in #1661).
|
||||
* **[dashboard] Fix loading arrays in forms when editing existing objects**: Fixed issue where arrays in forms were not loading correctly when editing existing objects in the dashboard ([**@kvaps**](https://github.com/kvaps)).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.38.0...v0.38.1](https://github.com/cozystack/cozystack/compare/v0.38.0...v0.38.1)
|
||||
|
||||
13
docs/changelogs/v0.38.2.md
Normal file
13
docs/changelogs/v0.38.2.md
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.38.2
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[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, particularly when running migration 20 ([**@kvaps**](https://github.com/kvaps) in #1677).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.38.1...v0.38.2](https://github.com/cozystack/cozystack/compare/v0.38.1...v0.38.2)
|
||||
|
||||
14
docs/changelogs/v0.38.3.md
Normal file
14
docs/changelogs/v0.38.3.md
Normal file
@@ -0,0 +1,14 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.38.3
|
||||
-->
|
||||
|
||||
## Improvements
|
||||
|
||||
* **[core:installer] Address buildx warnings for installer image builds**: Aligns Dockerfile syntax casing to remove buildx warnings, keeping installer builds clean ([**@nbykov0**](https://github.com/nbykov0) in #1682).
|
||||
* **[system:coredns] Align CoreDNS app labels with Talos defaults**: Matches CoreDNS labels to Talos conventions so services select pods consistently across platform and tenant clusters ([**@nbykov0**](https://github.com/nbykov0) in #1675).
|
||||
* **[system:monitoring-agents] Rename CoreDNS metrics service to avoid conflicts**: Renames the metrics service so it no longer clashes with the CoreDNS service used for name resolution in tenant clusters ([**@nbykov0**](https://github.com/nbykov0) in #1676).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.38.2...v0.38.3](https://github.com/cozystack/cozystack/compare/v0.38.2...v0.38.3)
|
||||
|
||||
18
docs/changelogs/v0.38.4.md
Normal file
18
docs/changelogs/v0.38.4.md
Normal file
@@ -0,0 +1,18 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.38.4
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[linstor] Update piraeus-operator v2.10.2 to handle fsck checks reliably**: Upgrades LINSTOR CSI to avoid failed mounts when fsck sees mounted volumes, improving volume publish reliability ([**@kvaps**](https://github.com/kvaps) in #1689, #1697).
|
||||
* **[dashboard] Nest CustomFormsOverride properties under spec.properties**: Fixes schema generation so custom form properties are placed under `spec.properties`, preventing mis-rendered or missing form fields ([**@kvaps**](https://github.com/kvaps) in #1692, #1700).
|
||||
* **[virtual-machine] Guard PVC resize to only expand storage**: Ensures resize jobs run only when storage size increases, avoiding unintended shrink attempts during VM updates ([**@kvaps**](https://github.com/kvaps) in #1688, #1701).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] Clarify GPU check command**: Makes the kubectl command for validating GPU binding more explicit, including namespace context ([**@nbykov0**](https://github.com/nbykov0) in cozystack/website#379).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.38.3...v0.38.4](https://github.com/cozystack/cozystack/compare/v0.38.3...v0.38.4)
|
||||
|
||||
95
docs/changelogs/v0.39.0.md
Normal file
95
docs/changelogs/v0.39.0.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Cozystack v0.39 — "Enhanced Networking & Monitoring"
|
||||
|
||||
This release introduces topology-aware routing for Cilium services, automatic pod rollouts on configuration changes, improved monitoring capabilities, and numerous bug fixes and improvements across the platform.
|
||||
|
||||
## Highlights
|
||||
|
||||
* **Topology-Aware Routing**: Enabled topology-aware routing for Cilium services, improving traffic distribution and reducing latency by routing traffic to endpoints in the same zone when possible ([**@nbykov0**](https://github.com/nbykov0) in #1734).
|
||||
* **Automatic Pod Rollouts**: Cilium and Cilium operator pods now automatically restart when configuration changes, ensuring configuration updates are applied immediately ([**@kvaps**](https://github.com/kvaps) in #1728).
|
||||
* **Windows VM Scheduling**: Added nodeAffinity configuration for Windows VMs based on scheduling config, enabling dedicated nodes for Windows workloads ([**@kvaps**](https://github.com/kvaps) in #1693).
|
||||
* **SeaweedFS Updates**: Updated to SeaweedFS v4.02 with improved S3 daemon performance and fixes ([**@kvaps**](https://github.com/kvaps) in #1725).
|
||||
|
||||
---
|
||||
|
||||
## Major Features and Improvements
|
||||
|
||||
### Networking
|
||||
|
||||
* **[system/cilium] Enable topology-aware routing for services**: Enabled topology-aware routing for services, improving traffic distribution and reducing latency by routing traffic to endpoints in the same zone when possible. This feature helps optimize network performance in multi-zone deployments ([**@nbykov0**](https://github.com/nbykov0) in #1734).
|
||||
* **[cilium] Enable automatic pod rollout on configmap updates**: Cilium and Cilium operator pods now automatically restart when the cilium-config ConfigMap is updated, ensuring configuration changes are applied immediately without manual intervention ([**@kvaps**](https://github.com/kvaps) in #1728).
|
||||
|
||||
### Virtual Machines
|
||||
|
||||
* **[virtual-machine,vm-instance] Add nodeAffinity for Windows VMs based on scheduling config**: Added nodeAffinity configuration to virtual-machine and vm-instance charts to support dedicated nodes for Windows VMs. When `dedicatedNodesForWindowsVMs` is enabled in the `cozystack-scheduling` ConfigMap, Windows VMs are scheduled on nodes with label `scheduling.cozystack.io/vm-windows=true`, while non-Windows VMs prefer nodes without this label ([**@kvaps**](https://github.com/kvaps) in #1693).
|
||||
|
||||
### Storage
|
||||
|
||||
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 with improved performance for S3 daemon and fixes for known issues. This update includes better S3 compatibility and performance improvements ([**@kvaps**](https://github.com/kvaps) in #1725).
|
||||
|
||||
### Tools
|
||||
|
||||
* **[talm] feat(init)!: require --name flag for cluster name**: Breaking change: The `talm init` command now requires the `--name` flag to specify the cluster name. This ensures consistent cluster naming and prevents accidental initialization without a name ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#86).
|
||||
* **[talm] feat(template): preserve extra YAML documents in output**: Templates now preserve extra YAML documents in the output, allowing for more flexible template processing ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#87).
|
||||
* **[talm] feat: add directory expansion for -f flag**: Added directory expansion support for the `-f` flag, allowing users to specify directories instead of individual files ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@ca5713e).
|
||||
* **[talm] Introduce automatic root detection**: Added automatic root detection logic to simplify talm usage and reduce manual configuration ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@d165162).
|
||||
* **[talm] Introduce talm kubeconfig --login command**: Added new `talm kubeconfig --login` command for easier kubeconfig management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@5f7e05b).
|
||||
* **[talm] Introduce encryption**: Added encryption support to talm for secure configuration management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#81).
|
||||
* **[talm] Replace code-generation with wrapper on talosctl**: Refactored talm to use a wrapper on talosctl instead of code generation, simplifying the codebase and improving maintainability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#80).
|
||||
* **[talm] Use go embed instead of code generation**: Migrated from code generation to go embed for better build performance and simpler dependency management ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#79).
|
||||
* **[boot-to-talos] Cozystack: Update Talos Linux v1.11.3**: Updated boot-to-talos to use Talos Linux v1.11.3 ([**@kvaps**](https://github.com/kvaps) in cozystack/boot-to-talos#7).
|
||||
|
||||
## Improvements
|
||||
|
||||
* **[seaweedfs] Extended CA certificate duration to reduce disruptive CA rotations**: Extended CA certificate duration to reduce disruptive CA rotations, improving long-term certificate management and reducing operational overhead ([**@IvanHunters**](https://github.com/IvanHunters) in #1657).
|
||||
* **[dashboard] Add config hash annotations to restart pods on config changes**: Added config hash annotations to dashboard deployment templates to ensure pods are automatically restarted when their configuration changes, ensuring configuration updates are applied immediately ([**@kvaps**](https://github.com/kvaps) in #1662).
|
||||
* **[tenant][kubernetes] Introduce better cleanup logic**: Improved cleanup logic for tenant Kubernetes resources, ensuring proper resource cleanup when tenants are deleted or updated. Added automated pre-delete cleanup job for tenant namespaces to remove tenant-related releases during uninstall ([**@kvaps**](https://github.com/kvaps) in #1661).
|
||||
* **[system:coredns] update coredns app labels to match Talos coredns labels**: Updated coredns app labels to match Talos coredns labels, ensuring consistency across the platform ([**@nbykov0**](https://github.com/nbykov0) in #1675).
|
||||
* **[system:monitoring-agents] rename coredns metrics service**: Renamed coredns metrics service to avoid interference with coredns service used for name resolution in tenant k8s clusters ([**@nbykov0**](https://github.com/nbykov0) in #1676).
|
||||
* **[core:installer] Address buildx warnings**: Fixed Dockerfile syntax warnings from buildx, ensuring clean builds without warnings ([**@nbykov0**](https://github.com/nbykov0) in #1682).
|
||||
* **[talm] Refactor root detection logic into single file**: Improved code organization by consolidating root detection logic into a single file ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@487b479).
|
||||
* **[talm] Refactor init logic, better upgrade**: Improved initialization logic and upgrade process for better reliability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@c512777).
|
||||
* **[talm] Sugar for kubeconfig command**: Added convenience features to the kubeconfig command for improved usability ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@a4010b3).
|
||||
* **[talm] wrap upgrade command**: Wrapped upgrade command for better integration and error handling ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@2e1afbf).
|
||||
* **[talm] docs(readme): add Homebrew installation option**: Added Homebrew installation option to the README for easier installation on macOS ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm@12bd4f2).
|
||||
* **[talm] cozystack: disable nodeCIDRs allocation**: Disabled nodeCIDRs allocation in talm for better network configuration control ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#82).
|
||||
* **[talm] Update license to Apache2.0**: Updated license to Apache 2.0 for better compatibility and clarity ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@eda1032).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK**: Refactored the apiserver REST handlers to use typed objects (`appsv1alpha1.Application`) instead of `unstructured.Unstructured`, eliminating the need for runtime conversions and simplifying the codebase. Additionally, fixed an issue where `UnstructuredList` objects were using the first registered kind from `typeToGVK` instead of the kind from the object's field when multiple kinds are registered with the same Go type. This fix includes the upstream fix from kubernetes/kubernetes#135537 ([**@kvaps**](https://github.com/kvaps) in #1679).
|
||||
* **[dashboard] Fix CustomFormsOverride schema to nest properties under spec.properties**: Fixed the logic for generating CustomFormsOverride schema to properly nest properties under `spec.properties` instead of directly under `properties`, ensuring correct form schema generation ([**@kvaps**](https://github.com/kvaps) in #1692).
|
||||
* **[virtual-machine] Improve check for resizing job**: Improved storage resize logic to only expand persistent volume claims when storage is being increased, preventing unintended storage reduction operations. Added validation to accurately compare current and desired storage sizes before triggering resize operations ([**@kvaps**](https://github.com/kvaps) in #1688).
|
||||
* **[linstor] Update piraeus-operator v2.10.2**: Updated LINSTOR CSI to fix issues with the new fsck behaviour, resolving mount failures when fsck attempts to run on mounted devices ([**@kvaps**](https://github.com/kvaps) in #1689).
|
||||
* **[api] Revert dynamic list kinds representation fix (fixes namespace deletion regression)**: Reverted changes from #1630 that caused a regression affecting namespace deletion and upgrades from previous versions. The regression caused namespace deletion failures with errors like "content is not a list: []unstructured.Unstructured" during namespace finalization. This revert restores compatibility with namespace deletion controller and fixes upgrade issues from previous versions ([**@kvaps**](https://github.com/kvaps) in #1677).
|
||||
* **[talm] fix: normalize template paths for Windows compatibility**: Fixed template path handling to ensure Windows compatibility by normalizing paths ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#88).
|
||||
|
||||
## Dependencies
|
||||
|
||||
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 ([**@kvaps**](https://github.com/kvaps) in #1725).
|
||||
* **[linstor] Update piraeus-operator v2.10.2**: Updated piraeus-operator to version 2.10.2 ([**@kvaps**](https://github.com/kvaps) in #1689).
|
||||
* **[talm] Cozystack: Update Talos Linux v1.11.3**: Updated talm to use Talos Linux v1.11.3 ([**@kvaps**](https://github.com/kvaps) in cozystack/talm#83).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] Add article: Talm v0.17: Built-in Age Encryption for Secrets Management**: Added comprehensive blog post announcing Talm v0.17 and its built-in age-based encryption for secrets. Covers initial setup and key generation, encryption/decryption workflows, idempotent encryption behavior, automatic .gitignore handling, file permission safeguards, security best practices, and guidance for GitOps and CI/CD integration ([**@kvaps**](https://github.com/kvaps) in [cozystack/website#384](https://github.com/cozystack/website/pull/384)).
|
||||
* **[website] docs(talm): update talm init syntax for mandatory --preset and --name flags**: Updated documentation to reflect breaking changes in talm, adding mandatory `--preset` and `--name` flags to the talm init command ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#386).
|
||||
|
||||
---
|
||||
|
||||
## Contributors
|
||||
|
||||
We'd like to thank all contributors who made this release possible:
|
||||
|
||||
* [**@IvanHunters**](https://github.com/IvanHunters)
|
||||
* [**@kvaps**](https://github.com/kvaps)
|
||||
* [**@lexfrei**](https://github.com/lexfrei)
|
||||
* [**@nbykov0**](https://github.com/nbykov0)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.38.0...v0.39.0](https://github.com/cozystack/cozystack/compare/v0.38.0...v0.39.0)
|
||||
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.39.0
|
||||
-->
|
||||
|
||||
12
docs/changelogs/v0.39.1.md
Normal file
12
docs/changelogs/v0.39.1.md
Normal file
@@ -0,0 +1,12 @@
|
||||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v0.39.1
|
||||
-->
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
* **[monitoring] Add SLACK_SEVERITY_FILTER field and VMAgent for tenant monitoring**: Introduced the SLACK_SEVERITY_FILTER environment variable in the Alerta deployment to enable filtering of alert severities for Slack notifications based on the disabledSeverity configuration. Additionally, added a VMAgent resource template for scraping metrics within tenant namespaces, improving monitoring granularity and control. This enhancement allows administrators to configure which alert severities are sent to Slack and enables tenant-specific metrics collection for better observability ([**@IvanHunters**](https://github.com/IvanHunters) in #1712).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: [v0.39.0...v0.39.1](https://github.com/cozystack/cozystack/compare/v0.39.0...v0.39.1)
|
||||
|
||||
20
examples/desired-backup.yaml
Normal file
20
examples/desired-backup.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
apiVersion: backups.cozystack.io/v1alpha1
|
||||
kind: BackupJob
|
||||
metadata:
|
||||
name: desired-backup
|
||||
namespace: tenant-root
|
||||
labels:
|
||||
backups.cozystack.io/triggered-by: manual
|
||||
spec:
|
||||
applicationRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: VirtualMachine
|
||||
name: vm1
|
||||
storageRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: Bucket
|
||||
name: test-bucket
|
||||
strategyRef:
|
||||
apiGroup: strategy.backups.cozystack.io
|
||||
kind: Velero
|
||||
name: velero-strategy-default
|
||||
155
go.mod
155
go.mod
@@ -2,33 +2,41 @@
|
||||
|
||||
module github.com/cozystack/cozystack
|
||||
|
||||
go 1.23.0
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/fluxcd/helm-controller/api v1.1.0
|
||||
github.com/emicklei/dot v1.10.0
|
||||
github.com/fluxcd/helm-controller/api v1.4.3
|
||||
github.com/fluxcd/source-controller/api v1.7.4
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/google/gofuzz v1.2.0
|
||||
github.com/onsi/ginkgo/v2 v2.19.0
|
||||
github.com/onsi/gomega v1.33.1
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.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/vmware-tanzu/velero v1.17.1
|
||||
go.uber.org/zap v1.27.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
k8s.io/api v0.31.2
|
||||
k8s.io/apiextensions-apiserver v0.31.2
|
||||
k8s.io/apimachinery v0.31.2
|
||||
k8s.io/apiserver v0.31.2
|
||||
k8s.io/client-go v0.31.2
|
||||
k8s.io/component-base v0.31.2
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.34.2
|
||||
k8s.io/apiserver v0.34.1
|
||||
k8s.io/client-go v0.34.1
|
||||
k8s.io/component-base v0.34.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
||||
sigs.k8s.io/controller-runtime v0.19.0
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
github.com/NYTimes/gziphandler v1.1.1 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
@@ -36,86 +44,91 @@ require (
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.6.1 // indirect
|
||||
github.com/fluxcd/pkg/apis/meta v1.6.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // 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.23.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-logr/zapr v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/cel-go v0.21.0 // indirect
|
||||
github.com/google/gnostic-models v0.6.8 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/google/cel-go v0.26.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/moby/spdystream v0.4.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.16 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.16 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.16 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.4 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.6.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.23.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/term v0.27.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
golang.org/x/net v0.45.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/term v0.35.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
|
||||
google.golang.org/grpc v1.73.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/kms v0.31.2 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
k8s.io/kms v0.34.1 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
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
|
||||
|
||||
355
go.sum
355
go.sum
@@ -1,11 +1,11 @@
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
@@ -18,45 +18,52 @@ 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/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/emicklei/dot v1.10.0 h1:z17n0ce/FBMz3QbShSzVGhiW447Qhu7fljzvp3Gs6ig=
|
||||
github.com/emicklei/dot v1.10.0/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
|
||||
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fluxcd/helm-controller/api v1.1.0 h1:NS5Wm3U6Kv4w7Cw2sDOV++vf2ecGfFV00x1+2Y3QcOY=
|
||||
github.com/fluxcd/helm-controller/api v1.1.0/go.mod h1:BgHMgMY6CWynzl4KIbHpd6Wpn3FN9BqgkwmvoKCp6iE=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.6.1 h1:22FJc69Mq4i8aCxnKPlddHhSMyI4UPkQkqiAdWFcqe0=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.6.1/go.mod h1:5dvQ4IZwz0hMGmuj8tTWGtarsuxW0rWsxJOwC6i+0V8=
|
||||
github.com/fluxcd/pkg/apis/meta v1.6.1 h1:maLhcRJ3P/70ArLCY/LF/YovkxXbX+6sTWZwZQBeNq0=
|
||||
github.com/fluxcd/pkg/apis/meta v1.6.1/go.mod h1:YndB/gxgGZmKfqpAfFxyCDNFJFP0ikpeJzs66jwq280=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
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.23.0 h1:fLis5YcHnOsyKYptzBtituBm5EWNx13I0bXQsy0FG4s=
|
||||
github.com/fluxcd/pkg/apis/meta v1.23.0/go.mod h1:UWsIbBPCxYvoVklr2mV2uLFBf/n17dNAmKFjRfApdDo=
|
||||
github.com/fluxcd/source-controller/api v1.7.4 h1:+EOVnRA9LmLxOx7J273l7IOEU39m+Slt/nQGBy69ygs=
|
||||
github.com/fluxcd/source-controller/api v1.7.4/go.mod h1:ruf49LEgZRBfcP+eshl2n9SX1MfHayCcViAIGnZcaDY=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3 h1:SsVGAaMBxzvcgrOz/Kl6c2ybMHVqoiEFwtI+bDuSeSs=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.3/go.mod h1:Nx3QZweVyuhaOtSNrw+oxifG+qrakPvjgNAN9qlUTb0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
@@ -64,162 +71,173 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
|
||||
github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
|
||||
github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k=
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8=
|
||||
github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
|
||||
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
|
||||
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0=
|
||||
github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM=
|
||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||
github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
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=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
|
||||
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
|
||||
github.com/vmware-tanzu/velero v1.17.1 h1:ldKeiTuUwkThOw7zrUucNA1NwnLG66zl13YetWAoE0I=
|
||||
github.com/vmware-tanzu/velero v1.17.1/go.mod h1:3KTxuUN6Un38JzmYAX+8U6j2k6EexGoNNxa8jrJML8U=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk=
|
||||
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
|
||||
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
|
||||
go.etcd.io/etcd/api/v3 v3.5.16 h1:WvmyJVbjWqK4R1E+B12RRHz3bRGy9XVfh++MgbN+6n0=
|
||||
go.etcd.io/etcd/api/v3 v3.5.16/go.mod h1:1P4SlIP/VwkDmGo3OlOD7faPeP8KDIFhqvciH5EfN28=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.16 h1:ZgY48uH6UvB+/7R9Yf4x574uCO3jIx0TRDyetSfId3Q=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.16/go.mod h1:V8acl8pcEK0Y2g19YlOV9m9ssUe6MgiDSobSoaBAM0E=
|
||||
go.etcd.io/etcd/client/v2 v2.305.13 h1:RWfV1SX5jTU0lbCvpVQe3iPQeAHETWdOTb6pxhd77C8=
|
||||
go.etcd.io/etcd/client/v2 v2.305.13/go.mod h1:iQnL7fepbiomdXMb3om1rHq96htNNGv2sJkEcZGDRRg=
|
||||
go.etcd.io/etcd/client/v3 v3.5.16 h1:sSmVYOAHeC9doqi0gv7v86oY/BTld0SEFGaxsU9eRhE=
|
||||
go.etcd.io/etcd/client/v3 v3.5.16/go.mod h1:X+rExSGkyqxvu276cr2OwPLBaeqFu1cIl4vmRjAD/50=
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.13 h1:st9bDWNsKkBNpP4PR1MvM/9NqUPfvYZx/YXegsYEH8M=
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.13/go.mod h1:N+4PLrp7agI/Viy+dUYpX7iRtSPvKq+w8Y14d1vX+m0=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.13 h1:7r/NKAOups1YnKcfro2RvGGo2PTuizF/xh26Z2CTAzA=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.13/go.mod h1:uUFibGLn2Ksm2URMxN1fICGhk8Wu96EfDQyuLhAcAmw=
|
||||
go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok=
|
||||
go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
|
||||
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
|
||||
go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo=
|
||||
go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
|
||||
go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A=
|
||||
go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
|
||||
go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA=
|
||||
go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE=
|
||||
go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU=
|
||||
go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg=
|
||||
go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
|
||||
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
@@ -228,50 +246,48 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
|
||||
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -281,39 +297,42 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0=
|
||||
k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk=
|
||||
k8s.io/apiextensions-apiserver v0.31.2 h1:W8EwUb8+WXBLu56ser5IudT2cOho0gAKeTOnywBLxd0=
|
||||
k8s.io/apiextensions-apiserver v0.31.2/go.mod h1:i+Geh+nGCJEGiCGR3MlBDkS7koHIIKWVfWeRFiOsUcM=
|
||||
k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw=
|
||||
k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4=
|
||||
k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE=
|
||||
k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=
|
||||
k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=
|
||||
k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA=
|
||||
k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ=
|
||||
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/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=
|
||||
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
|
||||
k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A=
|
||||
k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kms v0.31.2 h1:pyx7l2qVOkClzFMIWMVF/FxsSkgd+OIGH7DecpbscJI=
|
||||
k8s.io/kms v0.31.2/go.mod h1:OZKwl1fan3n3N5FFxnW5C4V3ygrah/3YXeJWS3O6+94=
|
||||
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 h1:GKE9U8BH16uynoxQii0auTjmmmuZ3O0LFMN6S0lPPhI=
|
||||
k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 h1:CPT0ExVicCzcpeN4baWEV2ko2Z/AsiZgEdwgcfwLgMo=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
|
||||
sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
k8s.io/kms v0.34.1 h1:iCFOvewDPzWM9fMTfyIPO+4MeuZ0tcZbugxLNSHFG4w=
|
||||
k8s.io/kms v0.34.1/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPGPs+Ki1gHw4w1R0=
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
145
hack/check-optional-repos.sh
Executable file
145
hack/check-optional-repos.sh
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
###############################################################################
|
||||
# check-optional-repos.sh - Check optional repositories for tags and commits #
|
||||
# during a release period #
|
||||
###############################################################################
|
||||
set -eu
|
||||
|
||||
# Function to ensure repository is cloned and up-to-date
|
||||
update_repo() {
|
||||
local repo_name=$1
|
||||
local repo_url="https://github.com/cozystack/${repo_name}.git"
|
||||
|
||||
mkdir -p _repos
|
||||
cd _repos
|
||||
|
||||
if [ -d "$repo_name" ]; then
|
||||
cd "$repo_name"
|
||||
git fetch --all --tags --force
|
||||
git checkout main 2>/dev/null || git checkout master
|
||||
git pull
|
||||
else
|
||||
git clone "$repo_url"
|
||||
cd "$repo_name"
|
||||
fi
|
||||
|
||||
cd ../..
|
||||
}
|
||||
|
||||
# Check if required parameters are provided
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Usage: $0 <RELEASE_START> <RELEASE_END>"
|
||||
echo "Example: $0 '2025-10-10 12:27:31 +0400' '2025-10-13 16:04:33 +0200'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RELEASE_START="$1"
|
||||
RELEASE_END="$2"
|
||||
|
||||
# Get the script directory to return to it later
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
COZYSTACK_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
|
||||
cd "$COZYSTACK_ROOT"
|
||||
|
||||
echo "Checking optional repositories for tags and commits between:"
|
||||
echo " Start: $RELEASE_START"
|
||||
echo " End: $RELEASE_END"
|
||||
echo ""
|
||||
|
||||
# Loop through ALL optional repositories
|
||||
for repo_name in talm boot-to-talos cozyhr cozy-proxy; do
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Checking repository: $repo_name"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
# Update/clone repository
|
||||
update_repo "$repo_name"
|
||||
|
||||
cd "_repos/$repo_name"
|
||||
REPO_NAME=$(basename "$(pwd)")
|
||||
git fetch --all --tags --force
|
||||
|
||||
# Check for tags matching release version pattern or created during release period
|
||||
TAGS=$(git for-each-ref --format='%(refname:short) %(creatordate)' refs/tags 2>/dev/null | \
|
||||
awk -v start="$RELEASE_START" -v end="$RELEASE_END" '$2 >= start && $2 <= end {print $1}' || true)
|
||||
|
||||
if [ -n "$TAGS" ]; then
|
||||
echo "Found tags in $repo_name: $TAGS"
|
||||
PREV_TAG=$(echo "$TAGS" | head -1)
|
||||
NEW_TAG=$(echo "$TAGS" | tail -1)
|
||||
|
||||
echo ""
|
||||
echo "Commits between $PREV_TAG and $NEW_TAG:"
|
||||
# Include merge commits to capture backports
|
||||
git log "$PREV_TAG..$NEW_TAG" --format="%H|%s|%an" 2>/dev/null | while IFS='|' read -r commit_hash subject author_name; do
|
||||
if [ -z "$commit_hash" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get PR number from commit message
|
||||
COMMIT_MSG=$(git log -1 --format=%B "$commit_hash" 2>/dev/null || echo "")
|
||||
PR_NUMBER=$(echo "$COMMIT_MSG" | grep -oE '#[0-9]+' | head -1 | tr -d '#' || echo "")
|
||||
|
||||
# Get author: prioritize PR author, fallback to commit author
|
||||
GITHUB_USERNAME=""
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
GITHUB_USERNAME=$(gh pr view "$PR_NUMBER" --repo "cozystack/$REPO_NAME" --json author --jq '.author.login // empty' 2>/dev/null || echo "")
|
||||
fi
|
||||
if [ -z "$GITHUB_USERNAME" ]; then
|
||||
GITHUB_USERNAME=$(gh api "repos/cozystack/$REPO_NAME/commits/$commit_hash" --jq '.author.login // empty' 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
echo " $commit_hash|$subject|$author_name|$GITHUB_USERNAME|cozystack/$REPO_NAME#$PR_NUMBER"
|
||||
else
|
||||
echo " $commit_hash|$subject|$author_name|$GITHUB_USERNAME|cozystack/$REPO_NAME@${commit_hash:0:7}"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "No tags found in $repo_name during release period"
|
||||
|
||||
# Check for commits by dates if no exact version tags
|
||||
# Include merge commits to capture backports
|
||||
COMMITS=$(git log --since="$RELEASE_START" --until="$RELEASE_END" --format="%H|%s|%an" 2>/dev/null || true)
|
||||
|
||||
if [ -n "$COMMITS" ]; then
|
||||
echo ""
|
||||
echo "Commits found by date range:"
|
||||
echo "$COMMITS" | while IFS='|' read -r commit_hash subject author_name; do
|
||||
if [ -z "$commit_hash" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get PR number from commit message
|
||||
COMMIT_MSG=$(git log -1 --format=%B "$commit_hash" 2>/dev/null || echo "")
|
||||
PR_NUMBER=$(echo "$COMMIT_MSG" | grep -oE '#[0-9]+' | head -1 | tr -d '#' || echo "")
|
||||
|
||||
# Get author: prioritize PR author, fallback to commit author
|
||||
GITHUB_USERNAME=""
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
GITHUB_USERNAME=$(gh pr view "$PR_NUMBER" --repo "cozystack/$REPO_NAME" --json author --jq '.author.login // empty' 2>/dev/null || echo "")
|
||||
fi
|
||||
if [ -z "$GITHUB_USERNAME" ]; then
|
||||
GITHUB_USERNAME=$(gh api "repos/cozystack/$REPO_NAME/commits/$commit_hash" --jq '.author.login // empty' 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
echo " $commit_hash|$subject|$author_name|$GITHUB_USERNAME|cozystack/$REPO_NAME#$PR_NUMBER"
|
||||
else
|
||||
echo " $commit_hash|$subject|$author_name|$GITHUB_USERNAME|cozystack/$REPO_NAME@${commit_hash:0:7}"
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "No commits found in $repo_name during release period"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
cd "$COZYSTACK_ROOT"
|
||||
done
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "Finished checking all optional repositories"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
|
||||
226
hack/e2e-apps/backup.bats.disabled
Executable file
226
hack/e2e-apps/backup.bats.disabled
Executable file
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
# Test variables - stored for teardown
|
||||
TEST_NAMESPACE='tenant-test'
|
||||
TEST_BUCKET_NAME='test-backup-bucket'
|
||||
TEST_VM_NAME='test-backup-vm'
|
||||
TEST_BACKUPJOB_NAME='test-backup-job'
|
||||
|
||||
teardown() {
|
||||
# Clean up resources (runs even if test fails)
|
||||
namespace="${TEST_NAMESPACE}"
|
||||
bucket_name="${TEST_BUCKET_NAME}"
|
||||
vm_name="${TEST_VM_NAME}"
|
||||
backupjob_name="${TEST_BACKUPJOB_NAME}"
|
||||
|
||||
# Clean up port-forward if still running
|
||||
pkill -f "kubectl.*port-forward.*seaweedfs-s3" 2>/dev/null || true
|
||||
|
||||
# Clean up Velero resources in cozy-velero namespace
|
||||
# Find Velero backup by pattern matching namespace-backupjob
|
||||
for backup in $(kubectl -n cozy-velero get backups.velero.io -o jsonpath='{.items[*].metadata.name}' 2>/dev/null || true); do
|
||||
if echo "$backup" | grep -q "^${namespace}-${backupjob_name}-"; then
|
||||
kubectl -n cozy-velero delete backups.velero.io ${backup} --wait=false 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
|
||||
# Clean up BackupStorageLocation and VolumeSnapshotLocation (named: namespace-backupjob)
|
||||
BSL_NAME="${namespace}-${backupjob_name}"
|
||||
kubectl -n cozy-velero delete backupstoragelocations.velero.io ${BSL_NAME} --wait=false 2>/dev/null || true
|
||||
kubectl -n cozy-velero delete volumesnapshotlocations.velero.io ${BSL_NAME} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up Velero credentials secret
|
||||
SECRET_NAME="backup-${namespace}-${backupjob_name}-s3-credentials"
|
||||
kubectl -n cozy-velero delete secret ${SECRET_NAME} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up BackupJob
|
||||
kubectl -n ${namespace} delete backupjob ${backupjob_name} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up Virtual Machine
|
||||
kubectl -n ${namespace} delete virtualmachines.apps.cozystack.io ${vm_name} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up Bucket
|
||||
kubectl -n ${namespace} delete bucket.apps.cozystack.io ${bucket_name} --wait=false 2>/dev/null || true
|
||||
|
||||
# Clean up temporary files
|
||||
rm -f /tmp/bucket-backup-credentials.json
|
||||
}
|
||||
|
||||
print_log() {
|
||||
echo "# $1" >&3
|
||||
}
|
||||
|
||||
@test "Create Backup for Virtual Machine" {
|
||||
# Test variables
|
||||
bucket_name="${TEST_BUCKET_NAME}"
|
||||
vm_name="${TEST_VM_NAME}"
|
||||
backupjob_name="${TEST_BACKUPJOB_NAME}"
|
||||
namespace="${TEST_NAMESPACE}"
|
||||
|
||||
print_log "Step 0:Ensure BackupJob and Velero strategy CRDs are installed"
|
||||
kubectl apply -f packages/system/backup-controller/definitions/backups.cozystack.io_backupjobs.yaml
|
||||
kubectl apply -f packages/system/backupstrategy-controller/definitions/strategy.backups.cozystack.io_veleroes.yaml
|
||||
# Wait for CRDs to be ready
|
||||
kubectl wait --for condition=established --timeout=30s crd backupjobs.backups.cozystack.io
|
||||
kubectl wait --for condition=established --timeout=30s crd veleroes.strategy.backups.cozystack.io
|
||||
|
||||
# Ensure velero-strategy-default resource exists
|
||||
kubectl apply -f packages/system/backup-controller/templates/strategy.yaml
|
||||
|
||||
print_log "Step 1: Create the bucket resource"
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: apps.cozystack.io/v1alpha1
|
||||
kind: Bucket
|
||||
metadata:
|
||||
name: ${bucket_name}
|
||||
namespace: ${namespace}
|
||||
spec: {}
|
||||
EOF
|
||||
|
||||
print_log "Wait for the bucket to be ready"
|
||||
kubectl -n ${namespace} wait hr bucket-${bucket_name} --timeout=100s --for=condition=ready
|
||||
kubectl -n ${namespace} wait bucketclaims.objectstorage.k8s.io bucket-${bucket_name} --timeout=300s --for=jsonpath='{.status.bucketReady}'=true
|
||||
kubectl -n ${namespace} wait bucketaccesses.objectstorage.k8s.io bucket-${bucket_name} --timeout=300s --for=jsonpath='{.status.accessGranted}'=true
|
||||
|
||||
# Get bucket credentials for later S3 verification
|
||||
kubectl -n ${namespace} get secret bucket-${bucket_name} -ojsonpath='{.data.BucketInfo}' | base64 -d > /tmp/bucket-backup-credentials.json
|
||||
ACCESS_KEY=$(jq -r '.spec.secretS3.accessKeyID' /tmp/bucket-backup-credentials.json)
|
||||
SECRET_KEY=$(jq -r '.spec.secretS3.accessSecretKey' /tmp/bucket-backup-credentials.json)
|
||||
BUCKET_NAME=$(jq -r '.spec.bucketName' /tmp/bucket-backup-credentials.json)
|
||||
|
||||
print_log "Step 2: Create the Virtual Machine"
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: apps.cozystack.io/v1alpha1
|
||||
kind: VirtualMachine
|
||||
metadata:
|
||||
name: ${vm_name}
|
||||
namespace: ${namespace}
|
||||
spec:
|
||||
external: false
|
||||
externalMethod: PortList
|
||||
externalPorts:
|
||||
- 22
|
||||
instanceType: "u1.medium"
|
||||
instanceProfile: ubuntu
|
||||
systemDisk:
|
||||
image: ubuntu
|
||||
storage: 5Gi
|
||||
storageClass: replicated
|
||||
gpus: []
|
||||
resources: {}
|
||||
sshKeys:
|
||||
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF
|
||||
test@test
|
||||
cloudInit: |
|
||||
#cloud-config
|
||||
users:
|
||||
- name: test
|
||||
shell: /bin/bash
|
||||
sudo: ['ALL=(ALL) NOPASSWD: ALL']
|
||||
groups: sudo
|
||||
ssh_authorized_keys:
|
||||
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPht0dPk5qQ+54g1hSX7A6AUxXJW5T6n/3d7Ga2F8gTF test@test
|
||||
cloudInitSeed: ""
|
||||
EOF
|
||||
|
||||
print_log "Wait for VM to be ready"
|
||||
sleep 5
|
||||
kubectl -n ${namespace} wait hr virtual-machine-${vm_name} --timeout=10s --for=condition=ready
|
||||
kubectl -n ${namespace} wait dv virtual-machine-${vm_name} --timeout=150s --for=condition=ready
|
||||
kubectl -n ${namespace} wait pvc virtual-machine-${vm_name} --timeout=100s --for=jsonpath='{.status.phase}'=Bound
|
||||
kubectl -n ${namespace} wait vm virtual-machine-${vm_name} --timeout=100s --for=condition=ready
|
||||
|
||||
print_log "Step 3: Create BackupJob"
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: backups.cozystack.io/v1alpha1
|
||||
kind: BackupJob
|
||||
metadata:
|
||||
name: ${backupjob_name}
|
||||
namespace: ${namespace}
|
||||
labels:
|
||||
backups.cozystack.io/triggered-by: e2e-test
|
||||
spec:
|
||||
applicationRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: VirtualMachine
|
||||
name: ${vm_name}
|
||||
storageRef:
|
||||
apiGroup: apps.cozystack.io
|
||||
kind: Bucket
|
||||
name: ${bucket_name}
|
||||
strategyRef:
|
||||
apiGroup: strategy.backups.cozystack.io
|
||||
kind: Velero
|
||||
name: velero-strategy-default
|
||||
EOF
|
||||
|
||||
print_log "Wait for BackupJob to start"
|
||||
kubectl -n ${namespace} wait backupjob ${backupjob_name} --timeout=60s --for=jsonpath='{.status.phase}'=Running
|
||||
|
||||
print_log "Wait for BackupJob to complete"
|
||||
kubectl -n ${namespace} wait backupjob ${backupjob_name} --timeout=300s --for=jsonpath='{.status.phase}'=Succeeded
|
||||
|
||||
print_log "Verify BackupJob status"
|
||||
PHASE=$(kubectl -n ${namespace} get backupjob ${backupjob_name} -o jsonpath='{.status.phase}')
|
||||
[ "$PHASE" = "Succeeded" ]
|
||||
|
||||
# Verify BackupJob has a backupRef
|
||||
BACKUP_REF=$(kubectl -n ${namespace} get backupjob ${backupjob_name} -o jsonpath='{.status.backupRef.name}')
|
||||
[ -n "$BACKUP_REF" ]
|
||||
|
||||
# Find the Velero backup by searching for backups matching the namespace-backupjob pattern
|
||||
# Format: namespace-backupjob-timestamp
|
||||
VELERO_BACKUP_NAME=""
|
||||
VELERO_BACKUP_PHASE=""
|
||||
|
||||
print_log "Wait a bit for the backup to be created and appear in the API"
|
||||
sleep 30
|
||||
|
||||
# Find backup by pattern matching namespace-backupjob
|
||||
for backup in $(kubectl -n cozy-velero get backups.velero.io -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
|
||||
if echo "$backup" | grep -q "^${namespace}-${backupjob_name}-"; then
|
||||
VELERO_BACKUP_NAME=$backup
|
||||
VELERO_BACKUP_PHASE=$(kubectl -n cozy-velero get backups.velero.io $backup -o jsonpath='{.status.phase}' 2>/dev/null || echo "")
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
print_log "Verify Velero Backup was found"
|
||||
[ -n "$VELERO_BACKUP_NAME" ]
|
||||
|
||||
echo '# Wait for Velero Backup to complete' >&3
|
||||
until kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' | grep -q 'Completed\|Failed'; do
|
||||
sleep 5
|
||||
done
|
||||
|
||||
print_log "Verify Velero Backup is Completed"
|
||||
timeout 90 sh -ec "until [ \"\$(kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' 2>/dev/null)\" = \"Completed\" ]; do sleep 30; done"
|
||||
|
||||
# Final verification
|
||||
VELERO_BACKUP_PHASE=$(kubectl -n cozy-velero get backups.velero.io ${VELERO_BACKUP_NAME} -o jsonpath='{.status.phase}' 2>/dev/null || echo "")
|
||||
[ "$VELERO_BACKUP_PHASE" = "Completed" ]
|
||||
|
||||
print_log "Step 4: Verify S3 has backup data"
|
||||
# Start port-forwarding to S3 service (with timeout to keep it alive)
|
||||
bash -c 'timeout 100s kubectl port-forward service/seaweedfs-s3 -n tenant-root 8333:8333 > /dev/null 2>&1 &'
|
||||
|
||||
# Wait for port-forward to be ready
|
||||
timeout 30 sh -ec "until nc -z localhost 8333; do sleep 1; done"
|
||||
|
||||
# Wait a bit for backup data to be written to S3
|
||||
sleep 30
|
||||
|
||||
# Set up MinIO client with insecure flag (use environment variable for all commands)
|
||||
export MC_INSECURE=1
|
||||
mc alias set local https://localhost:8333 $ACCESS_KEY $SECRET_KEY
|
||||
|
||||
# Verify backup directory exists in S3
|
||||
BACKUP_PATH="${BUCKET_NAME}/backups/${VELERO_BACKUP_NAME}"
|
||||
mc ls local/${BACKUP_PATH}/ 2>/dev/null
|
||||
[ $? -eq 0 ]
|
||||
|
||||
# Verify backup files exist (at least metadata files)
|
||||
BACKUP_FILES=$(mc ls local/${BACKUP_PATH}/ 2>/dev/null | wc -l || echo "0")
|
||||
[ "$BACKUP_FILES" -gt "0" ]
|
||||
}
|
||||
|
||||
44
hack/e2e-apps/ferretdb.bats
Normal file
44
hack/e2e-apps/ferretdb.bats
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
@test "Create DB FerretDB" {
|
||||
name='test'
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: apps.cozystack.io/v1alpha1
|
||||
kind: FerretDB
|
||||
metadata:
|
||||
name: $name
|
||||
namespace: tenant-test
|
||||
spec:
|
||||
backup:
|
||||
destinationPath: "s3://bucket/path/to/folder/"
|
||||
enabled: false
|
||||
endpointURL: "http://minio-gateway-service:9000"
|
||||
retentionPolicy: "30d"
|
||||
s3AccessKey: "<your-access-key>"
|
||||
s3SecretKey: "<your-secret-key>"
|
||||
schedule: "0 2 * * * *"
|
||||
bootstrap:
|
||||
enabled: false
|
||||
external: false
|
||||
quorum:
|
||||
maxSyncReplicas: 0
|
||||
minSyncReplicas: 0
|
||||
replicas: 2
|
||||
resources: {}
|
||||
resourcesPreset: "micro"
|
||||
size: "10Gi"
|
||||
users:
|
||||
testuser:
|
||||
password: xai7Wepo
|
||||
EOF
|
||||
sleep 5
|
||||
kubectl -n tenant-test wait hr ferretdb-$name --timeout=100s --for=condition=ready
|
||||
timeout 40 sh -ec "until kubectl -n tenant-test get svc ferretdb-$name-postgres-r -o jsonpath='{.spec.ports[0].port}' | grep -q '5432'; do sleep 10; done"
|
||||
timeout 40 sh -ec "until kubectl -n tenant-test get svc ferretdb-$name-postgres-ro -o jsonpath='{.spec.ports[0].port}' | grep -q '5432'; do sleep 10; done"
|
||||
timeout 40 sh -ec "until kubectl -n tenant-test get svc ferretdb-$name-postgres-rw -o jsonpath='{.spec.ports[0].port}' | grep -q '5432'; do sleep 10; done"
|
||||
timeout 120 sh -ec "until kubectl -n tenant-test get endpoints ferretdb-$name-postgres-r -o jsonpath='{.subsets[*].addresses[*].ip}' | grep -q '[0-9]'; do sleep 10; done"
|
||||
# for some reason it takes longer for the read-only endpoint to be ready
|
||||
#timeout 120 sh -ec "until kubectl -n tenant-test get endpoints ferretdb-$name-postgres-ro -o jsonpath='{.subsets[*].addresses[*].ip}' | grep -q '[0-9]'; do sleep 10; done"
|
||||
timeout 120 sh -ec "until kubectl -n tenant-test get endpoints ferretdb-$name-postgres-rw -o jsonpath='{.subsets[*].addresses[*].ip}' | grep -q '[0-9]'; do sleep 10; done"
|
||||
kubectl -n tenant-test delete ferretdb.apps.cozystack.io $name
|
||||
}
|
||||
121
hack/e2e-apps/foundationdb.bats
Normal file
121
hack/e2e-apps/foundationdb.bats
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
@test "Create DB FoundationDB" {
|
||||
name='test'
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: apps.cozystack.io/v1alpha1
|
||||
kind: FoundationDB
|
||||
metadata:
|
||||
name: $name
|
||||
namespace: tenant-test
|
||||
spec:
|
||||
cluster:
|
||||
version: "7.3.63"
|
||||
processCounts:
|
||||
storage: 3
|
||||
stateless: -1
|
||||
cluster_controller: 1
|
||||
redundancyMode: "double"
|
||||
storageEngine: "ssd-2"
|
||||
faultDomain:
|
||||
key: "foundationdb.org/none"
|
||||
valueFrom: "\$FDB_ZONE_ID"
|
||||
storage:
|
||||
size: "1Gi"
|
||||
storageClass: ""
|
||||
resourcesPreset: "small"
|
||||
backup:
|
||||
enabled: false
|
||||
s3:
|
||||
bucket: ""
|
||||
endpoint: ""
|
||||
region: ""
|
||||
credentials:
|
||||
accessKeyId: ""
|
||||
secretAccessKey: ""
|
||||
retentionPolicy: "7d"
|
||||
monitoring:
|
||||
enabled: true
|
||||
customParameters:
|
||||
- "knob_disable_posix_kernel_aio=1"
|
||||
imageType: "unified"
|
||||
automaticReplacements: true
|
||||
EOF
|
||||
sleep 15
|
||||
|
||||
# Wait for HelmRelease to be ready
|
||||
kubectl -n tenant-test wait hr foundationdb-$name --timeout=300s --for=condition=ready
|
||||
|
||||
# Wait for FoundationDBCluster to be created (name has foundationdb- prefix)
|
||||
timeout 300 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name; do sleep 15; done"
|
||||
|
||||
# Wait for cluster to become available (initial reconciliation takes time - allow 5 minutes)
|
||||
timeout 300 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.databaseConfiguration.usable_regions}' | grep -q '1'; do sleep 30; done"
|
||||
|
||||
# Check that storage processes are running
|
||||
timeout 300 sh -ec "until [ \$(kubectl -n tenant-test get pods -l foundationdb.org/fdb-cluster-name=foundationdb-$name,foundationdb.org/fdb-process-class=storage --field-selector=status.phase=Running --no-headers | wc -l) -eq 3 ]; do sleep 15; done"
|
||||
|
||||
# Check that log processes are running (these are the stateless processes)
|
||||
timeout 300 sh -ec "until [ \$(kubectl -n tenant-test get pods -l foundationdb.org/fdb-cluster-name=foundationdb-$name,foundationdb.org/fdb-process-class=log --field-selector=status.phase=Running --no-headers | wc -l) -ge 1 ]; do sleep 15; done"
|
||||
|
||||
# Check that cluster controller is running
|
||||
timeout 300 sh -ec "until [ \$(kubectl -n tenant-test get pods -l foundationdb.org/fdb-cluster-name=foundationdb-$name,foundationdb.org/fdb-process-class=cluster_controller --field-selector=status.phase=Running --no-headers | wc -l) -eq 1 ]; do sleep 15; done"
|
||||
|
||||
# Check WorkloadMonitor is created and configured
|
||||
timeout 120 sh -ec "until kubectl -n tenant-test get workloadmonitor foundationdb-$name; do sleep 10; done"
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get workloadmonitor foundationdb-$name -o jsonpath='{.spec.replicas}' | grep -q '3'; do sleep 5; done"
|
||||
|
||||
# Check dashboard resource map is created
|
||||
kubectl -n tenant-test get configmap foundationdb-$name-resourcemap
|
||||
|
||||
# Verify cluster is healthy (check cluster status) - allow extra time for initial setup
|
||||
timeout 300 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.health.available}' | grep -q 'true'; do sleep 20; done"
|
||||
|
||||
# Validate status.configured field
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.configured}' | grep -q 'true'; do sleep 10; done"
|
||||
|
||||
# Validate status.connectionString field exists and contains expected format
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.connectionString}' | grep -q '@.*\.svc\.cozy\.local'; do sleep 10; done"
|
||||
|
||||
# Validate comprehensive status.databaseConfiguration fields
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.databaseConfiguration.logs}' | grep -q '3'; do sleep 10; done"
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.databaseConfiguration.proxies}' | grep -q '3'; do sleep 10; done"
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.databaseConfiguration.redundancy_mode}' | grep -q 'double'; do sleep 10; done"
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.databaseConfiguration.resolvers}' | grep -q '1'; do sleep 10; done"
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.databaseConfiguration.storage_engine}' | grep -q 'ssd-2'; do sleep 10; done"
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.databaseConfiguration.usable_regions}' | grep -q '1'; do sleep 10; done"
|
||||
|
||||
# Validate status.desiredProcessGroups field
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.desiredProcessGroups}' | grep -q '^[0-9][0-9]*$'; do sleep 10; done"
|
||||
|
||||
# Validate status.generations.reconciled field
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.generations.reconciled}' | grep -q '^[0-9][0-9]*$'; do sleep 10; done"
|
||||
|
||||
# Validate status.hasListenIPsForAllPods field
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.hasListenIPsForAllPods}' | grep -q 'true'; do sleep 10; done"
|
||||
|
||||
# Validate comprehensive status.health fields
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.health.fullReplication}' | grep -q 'true'; do sleep 10; done"
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.status.health.healthy}' | grep -q 'true'; do sleep 10; done"
|
||||
|
||||
# Verify security context is applied correctly (non-root user)
|
||||
storage_pod=$(kubectl -n tenant-test get pods -l foundationdb.org/fdb-cluster-name=foundationdb-$name,foundationdb.org/fdb-process-class=storage --no-headers | head -n1 | awk '{print $1}')
|
||||
kubectl -n tenant-test get pod "$storage_pod" -o jsonpath='{.spec.containers[0].securityContext.runAsUser}' | grep -q '4059'
|
||||
kubectl -n tenant-test get pod "$storage_pod" -o jsonpath='{.spec.containers[0].securityContext.runAsGroup}' | grep -q '4059'
|
||||
|
||||
# Verify volumeClaimTemplate is properly configured in FoundationDBCluster CRD
|
||||
timeout 60 sh -ec "until kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name -o jsonpath='{.spec.processes.general.volumeClaimTemplate.spec.resources.requests.storage}' | grep -q '1Gi'; do sleep 10; done"
|
||||
|
||||
# Verify PVCs are created with correct storage size (1Gi as specified in test)
|
||||
timeout 120 sh -ec "until [ \$(kubectl -n tenant-test get pvc -l foundationdb.org/fdb-cluster-name=foundationdb-$name --no-headers | wc -l) -ge 3 ]; do sleep 10; done"
|
||||
kubectl -n tenant-test get pvc -l foundationdb.org/fdb-cluster-name=foundationdb-$name -o jsonpath='{.items[*].spec.resources.requests.storage}' | grep -q '1Gi'
|
||||
|
||||
# Verify actual PVC storage capacity matches requested size
|
||||
kubectl -n tenant-test get pvc -l foundationdb.org/fdb-cluster-name=foundationdb-$name -o jsonpath='{.items[*].status.capacity.storage}' | grep -q '1Gi'
|
||||
|
||||
# Clean up
|
||||
kubectl -n tenant-test delete foundationdb $name
|
||||
|
||||
# Wait for cleanup to complete
|
||||
timeout 120 sh -ec "while kubectl -n tenant-test get foundationdbclusters.apps.foundationdb.org foundationdb-$name 2>/dev/null; do sleep 10; done"
|
||||
}
|
||||
@@ -64,81 +64,160 @@ spec:
|
||||
EOF
|
||||
# Wait for the tenant-test namespace to be active
|
||||
kubectl wait namespace tenant-test --timeout=20s --for=jsonpath='{.status.phase}'=Active
|
||||
|
||||
|
||||
# Wait for the Kamaji control plane to be created (retry for up to 10 seconds)
|
||||
timeout 10 sh -ec 'until kubectl get kamajicontrolplane -n tenant-test kubernetes-'"${test_name}"'; do sleep 1; done'
|
||||
|
||||
# Wait for the tenant control plane to be fully created (timeout after 4 minutes)
|
||||
kubectl wait --for=condition=TenantControlPlaneCreated kamajicontrolplane -n tenant-test kubernetes-${test_name} --timeout=4m
|
||||
|
||||
|
||||
# Wait for Kubernetes resources to be ready (timeout after 2 minutes)
|
||||
kubectl wait tcp -n tenant-test kubernetes-${test_name} --timeout=2m --for=jsonpath='{.status.kubernetesResources.version.status}'=Ready
|
||||
|
||||
kubectl wait tcp -n tenant-test kubernetes-${test_name} --timeout=5m --for=jsonpath='{.status.kubernetesResources.version.status}'=Ready
|
||||
|
||||
# Wait for all required deployments to be available (timeout after 4 minutes)
|
||||
kubectl wait deploy --timeout=4m --for=condition=available -n tenant-test kubernetes-${test_name} kubernetes-${test_name}-cluster-autoscaler kubernetes-${test_name}-kccm kubernetes-${test_name}-kcsi-controller
|
||||
|
||||
|
||||
# Wait for the machine deployment to scale to 2 replicas (timeout after 1 minute)
|
||||
kubectl wait machinedeployment kubernetes-${test_name}-md0 -n tenant-test --timeout=1m --for=jsonpath='{.status.replicas}'=2
|
||||
# Get the admin kubeconfig and save it to a file
|
||||
kubectl get secret kubernetes-${test_name}-admin-kubeconfig -ojsonpath='{.data.super-admin\.conf}' -n tenant-test | base64 -d > tenantkubeconfig
|
||||
kubectl get secret kubernetes-${test_name}-admin-kubeconfig -ojsonpath='{.data.super-admin\.conf}' -n tenant-test | base64 -d > tenantkubeconfig-${test_name}
|
||||
|
||||
# Update the kubeconfig to use localhost for the API server
|
||||
yq -i ".clusters[0].cluster.server = \"https://localhost:${port}\"" tenantkubeconfig
|
||||
yq -i ".clusters[0].cluster.server = \"https://localhost:${port}\"" tenantkubeconfig-${test_name}
|
||||
|
||||
|
||||
# Set up port forwarding to the Kubernetes API server for a 200 second timeout
|
||||
bash -c 'timeout 200s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &'
|
||||
bash -c 'timeout 500s kubectl port-forward service/kubernetes-'"${test_name}"' -n tenant-test '"${port}"':6443 > /dev/null 2>&1 &'
|
||||
# Verify the Kubernetes version matches what we expect (retry for up to 20 seconds)
|
||||
timeout 20 sh -ec 'until kubectl --kubeconfig tenantkubeconfig version 2>/dev/null | grep -Fq "Server Version: ${k8s_version}"; do sleep 5; done'
|
||||
timeout 20 sh -ec 'until kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' version 2>/dev/null | grep -Fq "Server Version: ${k8s_version}"; do sleep 5; done'
|
||||
|
||||
# Wait for the nodes to be ready (timeout after 2 minutes)
|
||||
timeout 2m bash -c '
|
||||
until [ "$(kubectl --kubeconfig tenantkubeconfig get nodes -o jsonpath="{.items[*].metadata.name}" | wc -w)" -eq 2 ]; do
|
||||
sleep 3
|
||||
timeout 3m bash -c '
|
||||
until [ "$(kubectl --kubeconfig tenantkubeconfig-'"${test_name}"' get nodes -o jsonpath="{.items[*].metadata.name}" | wc -w)" -eq 2 ]; do
|
||||
sleep 2
|
||||
done
|
||||
'
|
||||
# Verify the nodes are ready
|
||||
kubectl --kubeconfig tenantkubeconfig wait node --all --timeout=2m --for=condition=Ready
|
||||
kubectl --kubeconfig tenantkubeconfig get nodes -o wide
|
||||
kubectl --kubeconfig tenantkubeconfig-${test_name} wait node --all --timeout=2m --for=condition=Ready
|
||||
kubectl --kubeconfig tenantkubeconfig-${test_name} get nodes -o wide
|
||||
|
||||
# Verify the kubelet version matches what we expect
|
||||
versions=$(kubectl --kubeconfig tenantkubeconfig get nodes -o jsonpath='{.items[*].status.nodeInfo.kubeletVersion}')
|
||||
versions=$(kubectl --kubeconfig "tenantkubeconfig-${test_name}" \
|
||||
get nodes -o jsonpath='{.items[*].status.nodeInfo.kubeletVersion}')
|
||||
|
||||
node_ok=true
|
||||
|
||||
if [[ "$k8s_version" == v1.32* ]]; then
|
||||
echo "⚠️ TODO: Temporary stub — allowing nodes with v1.33 while k8s_version is v1.32"
|
||||
fi
|
||||
|
||||
|
||||
for v in $versions; do
|
||||
case "$k8s_version" in
|
||||
v1.32|v1.32.*)
|
||||
case "$v" in
|
||||
v1.32 | v1.32.* | v1.32-* | v1.33 | v1.33.* | v1.33-*)
|
||||
;;
|
||||
*)
|
||||
node_ok=false
|
||||
break
|
||||
;;
|
||||
esac
|
||||
case "$v" in
|
||||
"${k8s_version}" | "${k8s_version}".* | "${k8s_version}"-*)
|
||||
# acceptable
|
||||
;;
|
||||
*)
|
||||
case "$v" in
|
||||
"${k8s_version}" | "${k8s_version}".* | "${k8s_version}"-*)
|
||||
;;
|
||||
*)
|
||||
node_ok=false
|
||||
break
|
||||
;;
|
||||
esac
|
||||
node_ok=false
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! $node_ok; then
|
||||
if [ "$node_ok" != true ]; then
|
||||
echo "Kubelet versions did not match expected ${k8s_version}" >&2
|
||||
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
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ EOF
|
||||
}
|
||||
|
||||
@test "Check Cozystack API service" {
|
||||
kubectl wait --for=condition=Available apiservices/v1alpha1.apps.cozystack.io --timeout=2m
|
||||
kubectl wait --for=condition=Available apiservices/v1alpha1.apps.cozystack.io apiservices/v1alpha1.core.cozystack.io --timeout=2m
|
||||
}
|
||||
|
||||
@test "Configure Tenant and wait for applications" {
|
||||
|
||||
@@ -132,7 +132,6 @@ machine:
|
||||
- usermode_helper=disabled
|
||||
- name: zfs
|
||||
- name: spl
|
||||
- name: lldpd
|
||||
registries:
|
||||
mirrors:
|
||||
docker.io:
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
@test "Test OpenAPI v3 endpoint" {
|
||||
kubectl get -v7 --raw '/openapi/v3/apis/apps.cozystack.io/v1alpha1' > /dev/null
|
||||
kubectl get -v7 --raw '/openapi/v3/apis/core.cozystack.io/v1alpha1' > /dev/null
|
||||
}
|
||||
|
||||
@test "Test OpenAPI v2 endpoint (protobuf)" {
|
||||
@@ -18,3 +19,35 @@
|
||||
curl -sS --fail 'http://localhost:21234/openapi/v2?timeout=32s' -H 'Accept: application/com.github.proto-openapi.spec.v2@v1.0+protobuf' > /dev/null
|
||||
)
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
59
hack/helm-unit-tests.sh
Executable file
59
hack/helm-unit-tests.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# Script to run unit tests for all Helm charts.
|
||||
# It iterates through directories in packages/apps, packages/extra,
|
||||
# packages/system, and packages/library and runs the 'test' Makefile
|
||||
# target if it exists.
|
||||
|
||||
FAILED_DIRS_FILE="$(mktemp)"
|
||||
trap 'rm -f "$FAILED_DIRS_FILE"' EXIT
|
||||
|
||||
tests_found=0
|
||||
|
||||
check_and_run_test() {
|
||||
dir="$1"
|
||||
makefile="$dir/Makefile"
|
||||
|
||||
if [ ! -f "$makefile" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if make -C "$dir" -n test >/dev/null 2>&1; then
|
||||
echo "Running tests in $dir"
|
||||
tests_found=$((tests_found + 1))
|
||||
if ! make -C "$dir" test; then
|
||||
printf '%s\n' "$dir" >> "$FAILED_DIRS_FILE"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
for package_dir in packages/apps packages/extra packages/system packages/library; do
|
||||
if [ ! -d "$package_dir" ]; then
|
||||
echo "Warning: Directory $package_dir does not exist, skipping..." >&2
|
||||
continue
|
||||
fi
|
||||
|
||||
for dir in "$package_dir"/*; do
|
||||
[ -d "$dir" ] || continue
|
||||
check_and_run_test "$dir" || true
|
||||
done
|
||||
done
|
||||
|
||||
if [ "$tests_found" -eq 0 ]; then
|
||||
echo "No directories with 'test' Makefile targets found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -s "$FAILED_DIRS_FILE" ]; then
|
||||
echo "ERROR: Tests failed in the following directories:" >&2
|
||||
while IFS= read -r dir; do
|
||||
echo " - $dir" >&2
|
||||
done < "$FAILED_DIRS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All Helm unit tests passed."
|
||||
@@ -23,6 +23,14 @@ CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-
|
||||
API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${SCRIPT_ROOT}/api/api-rules"}"
|
||||
UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}"
|
||||
CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4"
|
||||
TMPDIR=$(mktemp -d)
|
||||
OPERATOR_CRDDIR=packages/core/installer/definitions
|
||||
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/definitions
|
||||
COZY_RD_CRDDIR=packages/system/cozystack-resource-definition-crd/definition
|
||||
BACKUPS_CORE_CRDDIR=packages/system/backup-controller/definitions
|
||||
BACKUPSTRATEGY_CRDDIR=packages/system/backupstrategy-controller/definitions
|
||||
|
||||
trap 'rm -rf ${TMPDIR}' EXIT
|
||||
|
||||
source "${CODEGEN_PKG}/kube_codegen.sh"
|
||||
|
||||
@@ -53,6 +61,15 @@ kube::codegen::gen_openapi \
|
||||
"${SCRIPT_ROOT}/pkg/apis"
|
||||
|
||||
$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
|
||||
$CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=${TMPDIR}
|
||||
|
||||
mv ${TMPDIR}/cozystack.io_packages.yaml ${OPERATOR_CRDDIR}/cozystack.io_packages.yaml
|
||||
mv ${TMPDIR}/cozystack.io_packagesources.yaml ${OPERATOR_CRDDIR}/cozystack.io_packagesources.yaml
|
||||
|
||||
mv ${TMPDIR}/cozystack.io_cozystackresourcedefinitions.yaml \
|
||||
${COZY_RD_CRDDIR}/cozystack.io_cozystackresourcedefinitions.yaml
|
||||
|
||||
mv ${TMPDIR}/backups.cozystack.io*.yaml ${BACKUPS_CORE_CRDDIR}/
|
||||
mv ${TMPDIR}/strategy.backups.cozystack.io*.yaml ${BACKUPSTRATEGY_CRDDIR}/
|
||||
|
||||
mv ${TMPDIR}/*.yaml ${COZY_CONTROLLER_CRDDIR}/
|
||||
|
||||
@@ -8,7 +8,6 @@ need yq; need jq; need base64
|
||||
CHART_YAML="${CHART_YAML:-Chart.yaml}"
|
||||
VALUES_YAML="${VALUES_YAML:-values.yaml}"
|
||||
SCHEMA_JSON="${SCHEMA_JSON:-values.schema.json}"
|
||||
CRD_DIR="../../system/cozystack-resource-definitions/cozyrds"
|
||||
|
||||
[[ -f "$CHART_YAML" ]] || { echo "No $CHART_YAML found"; exit 1; }
|
||||
[[ -f "$SCHEMA_JSON" ]] || { echo "No $SCHEMA_JSON found"; exit 1; }
|
||||
@@ -22,6 +21,8 @@ if [[ -z "$NAME" ]]; then
|
||||
echo "Chart.yaml: .name is empty"; exit 1
|
||||
fi
|
||||
|
||||
CRD_DIR="../../system/${NAME}-rd/cozyrds"
|
||||
|
||||
# Resolve icon path
|
||||
# Accepts:
|
||||
# /logos/foo.svg -> ./logos/foo.svg
|
||||
|
||||
40
hack/upload-releasenotes.sh
Executable file
40
hack/upload-releasenotes.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 <version>"
|
||||
echo "Example: 0.37.*"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION_PATTERN="$1"
|
||||
|
||||
# Collect matching files first
|
||||
FILES=$(find docs/changelogs -name "v${VERSION_PATTERN}.md" 2>/dev/null || true)
|
||||
|
||||
if [ -z "$FILES" ]; then
|
||||
echo "No changelog files found matching pattern: v${VERSION_PATTERN}.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Process each file
|
||||
echo "$FILES" | while IFS= read -r file; do
|
||||
if [ -z "$file" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract version from filename safely (basename without extension)
|
||||
version=$(basename "$file" .md)
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
echo "Warning: Could not extract version from file: $file"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Uploading release notes for version: $version"
|
||||
|
||||
# Check exit status of gh release edit
|
||||
if ! gh release edit "$version" --notes-file "docs/changelogs/${version}.md"; then
|
||||
echo "Error: Failed to upload release notes for version: $version"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
93
internal/backupcontroller/backupjob_controller.go
Normal file
93
internal/backupcontroller/backupjob_controller.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/record"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
)
|
||||
|
||||
// BackupVeleroStrategyReconciler reconciles BackupJob with a strategy referencing
|
||||
// Velero.strategy.backups.cozystack.io objects.
|
||||
type BackupJobReconciler struct {
|
||||
client.Client
|
||||
dynamic.Interface
|
||||
meta.RESTMapper
|
||||
Scheme *runtime.Scheme
|
||||
Recorder record.EventRecorder
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.Info("reconciling BackupJob", "namespace", req.Namespace, "name", req.Name)
|
||||
|
||||
j := &backupsv1alpha1.BackupJob{}
|
||||
err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: req.Name}, j)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
logger.V(1).Info("BackupJob not found, skipping")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
logger.Error(err, "failed to get BackupJob")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if j.Spec.StrategyRef.APIGroup == nil {
|
||||
logger.V(1).Info("BackupJob has nil StrategyRef.APIGroup, skipping", "backupjob", j.Name)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if *j.Spec.StrategyRef.APIGroup != strategyv1alpha1.GroupVersion.Group {
|
||||
logger.V(1).Info("BackupJob StrategyRef.APIGroup doesn't match, skipping",
|
||||
"backupjob", j.Name,
|
||||
"expected", strategyv1alpha1.GroupVersion.Group,
|
||||
"got", *j.Spec.StrategyRef.APIGroup)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
logger.Info("processing BackupJob", "backupjob", j.Name, "strategyKind", j.Spec.StrategyRef.Kind)
|
||||
switch j.Spec.StrategyRef.Kind {
|
||||
case strategyv1alpha1.JobStrategyKind:
|
||||
return r.reconcileJob(ctx, j)
|
||||
case strategyv1alpha1.VeleroStrategyKind:
|
||||
return r.reconcileVelero(ctx, j)
|
||||
default:
|
||||
logger.V(1).Info("BackupJob StrategyRef.Kind not supported, skipping",
|
||||
"backupjob", j.Name,
|
||||
"kind", j.Spec.StrategyRef.Kind,
|
||||
"supported", []string{strategyv1alpha1.JobStrategyKind, strategyv1alpha1.VeleroStrategyKind})
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetupWithManager registers our controller with the Manager and sets up watches.
|
||||
func (r *BackupJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
cfg := mgr.GetConfig()
|
||||
var err error
|
||||
if r.Interface, err = dynamic.NewForConfig(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
var h *http.Client
|
||||
if h, err = rest.HTTPClientFor(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if r.RESTMapper, err = apiutil.NewDynamicRESTMapper(cfg, h); err != nil {
|
||||
return err
|
||||
}
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&backupsv1alpha1.BackupJob{}).
|
||||
Complete(r)
|
||||
}
|
||||
28
internal/backupcontroller/factory/backupjob.go
Normal file
28
internal/backupcontroller/factory/backupjob.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func BackupJob(p *backupsv1alpha1.Plan, scheduledFor time.Time) *backupsv1alpha1.BackupJob {
|
||||
job := &backupsv1alpha1.BackupJob{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s-%d", p.Name, scheduledFor.Unix()/60),
|
||||
Namespace: p.Namespace,
|
||||
},
|
||||
Spec: backupsv1alpha1.BackupJobSpec{
|
||||
PlanRef: &corev1.LocalObjectReference{
|
||||
Name: p.Name,
|
||||
},
|
||||
ApplicationRef: *p.Spec.ApplicationRef.DeepCopy(),
|
||||
StorageRef: *p.Spec.StorageRef.DeepCopy(),
|
||||
StrategyRef: *p.Spec.StrategyRef.DeepCopy(),
|
||||
},
|
||||
}
|
||||
return job
|
||||
}
|
||||
15
internal/backupcontroller/jobstrategy_controller.go
Normal file
15
internal/backupcontroller/jobstrategy_controller.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
)
|
||||
|
||||
func (r *BackupJobReconciler) reconcileJob(ctx context.Context, j *backupsv1alpha1.BackupJob) (ctrl.Result, error) {
|
||||
_ = log.FromContext(ctx)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
104
internal/backupcontroller/plan_controller.go
Normal file
104
internal/backupcontroller/plan_controller.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
cron "github.com/robfig/cron/v3"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
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/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/backupcontroller/factory"
|
||||
)
|
||||
|
||||
const (
|
||||
minRequeueDelay = 30 * time.Second
|
||||
startingDeadlineSeconds = 300 * time.Second
|
||||
)
|
||||
|
||||
// PlanReconciler reconciles a Plan object
|
||||
type PlanReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
func (r *PlanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
log := log.FromContext(ctx)
|
||||
|
||||
log.V(2).Info("reconciling")
|
||||
|
||||
p := &backupsv1alpha1.Plan{}
|
||||
|
||||
if err := r.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, p); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
log.V(3).Info("Plan not found")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
tCheck := time.Now().Add(-startingDeadlineSeconds)
|
||||
sch, err := cron.ParseStandard(p.Spec.Schedule.Cron)
|
||||
if err != nil {
|
||||
errWrapped := fmt.Errorf("could not parse cron %s: %w", p.Spec.Schedule.Cron, err)
|
||||
log.Error(err, "could not parse cron", "cron", p.Spec.Schedule.Cron)
|
||||
meta.SetStatusCondition(&p.Status.Conditions, metav1.Condition{
|
||||
Type: backupsv1alpha1.PlanConditionError,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Failed to parse cron spec",
|
||||
Message: errWrapped.Error(),
|
||||
})
|
||||
if err := r.Status().Update(ctx, p); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Clear error condition if cron parsing succeeds
|
||||
if condition := meta.FindStatusCondition(p.Status.Conditions, backupsv1alpha1.PlanConditionError); condition != nil && condition.Status == metav1.ConditionTrue {
|
||||
meta.SetStatusCondition(&p.Status.Conditions, metav1.Condition{
|
||||
Type: backupsv1alpha1.PlanConditionError,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "Cron spec is valid",
|
||||
Message: "The cron schedule has been successfully parsed",
|
||||
})
|
||||
if err := r.Status().Update(ctx, p); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
tNext := sch.Next(tCheck)
|
||||
|
||||
if time.Now().Before(tNext) {
|
||||
return ctrl.Result{RequeueAfter: tNext.Sub(time.Now())}, nil
|
||||
}
|
||||
|
||||
job := factory.BackupJob(p, tNext)
|
||||
if err := controllerutil.SetControllerReference(p, job, r.Scheme); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if err := r.Create(ctx, job); err != nil {
|
||||
if apierrors.IsAlreadyExists(err) {
|
||||
return ctrl.Result{RequeueAfter: startingDeadlineSeconds}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{RequeueAfter: startingDeadlineSeconds}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager registers our controller with the Manager and sets up watches.
|
||||
func (r *PlanReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&backupsv1alpha1.Plan{}).
|
||||
Complete(r)
|
||||
}
|
||||
627
internal/backupcontroller/velerostrategy_controller.go
Normal file
627
internal/backupcontroller/velerostrategy_controller.go
Normal file
@@ -0,0 +1,627 @@
|
||||
package backupcontroller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
strategyv1alpha1 "github.com/cozystack/cozystack/api/backups/strategy/v1alpha1"
|
||||
backupsv1alpha1 "github.com/cozystack/cozystack/api/backups/v1alpha1"
|
||||
"github.com/cozystack/cozystack/internal/template"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
|
||||
)
|
||||
|
||||
func getLogger(ctx context.Context) loggerWithDebug {
|
||||
return loggerWithDebug{Logger: log.FromContext(ctx)}
|
||||
}
|
||||
|
||||
// loggerWithDebug wraps a logr.Logger and provides a Debug() method
|
||||
// that maps to V(1).Info() for convenience.
|
||||
type loggerWithDebug struct {
|
||||
logr.Logger
|
||||
}
|
||||
|
||||
// Debug logs at debug level (equivalent to V(1).Info())
|
||||
func (l loggerWithDebug) Debug(msg string, keysAndValues ...interface{}) {
|
||||
l.Logger.V(1).Info(msg, keysAndValues...)
|
||||
}
|
||||
|
||||
// S3Credentials holds the discovered S3 credentials from a Bucket storageRef
|
||||
type S3Credentials struct {
|
||||
BucketName string
|
||||
Endpoint string
|
||||
Region string
|
||||
AccessKeyID string
|
||||
AccessSecretKey string
|
||||
}
|
||||
|
||||
// bucketInfo represents the structure of BucketInfo stored in the secret
|
||||
type bucketInfo struct {
|
||||
Spec struct {
|
||||
BucketName string `json:"bucketName"`
|
||||
SecretS3 struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Region string `json:"region"`
|
||||
AccessKeyID string `json:"accessKeyID"`
|
||||
AccessSecretKey string `json:"accessSecretKey"`
|
||||
} `json:"secretS3"`
|
||||
} `json:"spec"`
|
||||
}
|
||||
|
||||
const (
|
||||
defaultRequeueAfter = 5 * time.Second
|
||||
defaultActiveJobPollingInterval = defaultRequeueAfter
|
||||
// Velero requires API objects and secrets to be in the cozy-velero namespace
|
||||
veleroNamespace = "cozy-velero"
|
||||
virtualMachinePrefix = "virtual-machine-"
|
||||
)
|
||||
|
||||
func storageS3SecretName(namespace, backupJobName string) string {
|
||||
return fmt.Sprintf("backup-%s-%s-s3-credentials", namespace, backupJobName)
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) reconcileVelero(ctx context.Context, j *backupsv1alpha1.BackupJob) (ctrl.Result, error) {
|
||||
logger := getLogger(ctx)
|
||||
logger.Debug("reconciling Velero strategy", "backupjob", j.Name, "phase", j.Status.Phase)
|
||||
|
||||
// If already completed, no need to reconcile
|
||||
if j.Status.Phase == backupsv1alpha1.BackupJobPhaseSucceeded ||
|
||||
j.Status.Phase == backupsv1alpha1.BackupJobPhaseFailed {
|
||||
logger.Debug("BackupJob already completed, skipping", "phase", j.Status.Phase)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Step 1: On first reconcile, set startedAt (but not phase yet - phase will be set after backup creation)
|
||||
logger.Debug("checking BackupJob status", "startedAt", j.Status.StartedAt, "phase", j.Status.Phase)
|
||||
if j.Status.StartedAt == nil {
|
||||
logger.Debug("setting BackupJob StartedAt")
|
||||
now := metav1.Now()
|
||||
j.Status.StartedAt = &now
|
||||
// Don't set phase to Running yet - will be set after Velero backup is successfully created
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("set BackupJob StartedAt", "startedAt", j.Status.StartedAt)
|
||||
} else {
|
||||
logger.Debug("BackupJob already started", "startedAt", j.Status.StartedAt, "phase", j.Status.Phase)
|
||||
}
|
||||
|
||||
// Step 2: Resolve inputs - Read Strategy, Storage, Application, optionally Plan
|
||||
logger.Debug("fetching Velero strategy", "strategyName", j.Spec.StrategyRef.Name)
|
||||
veleroStrategy := &strategyv1alpha1.Velero{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: j.Spec.StrategyRef.Name}, veleroStrategy); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logger.Error(err, "Velero strategy not found", "strategyName", j.Spec.StrategyRef.Name)
|
||||
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("Velero strategy not found: %s", j.Spec.StrategyRef.Name))
|
||||
}
|
||||
logger.Error(err, "failed to get Velero strategy")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("fetched Velero strategy", "strategyName", veleroStrategy.Name)
|
||||
|
||||
// Step 3: Execute backup logic
|
||||
// Check if we already created a Velero Backup
|
||||
// Use human-readable timestamp: YYYY-MM-DD-HH-MM-SS
|
||||
if j.Status.StartedAt == nil {
|
||||
logger.Error(nil, "StartedAt is nil after status update, this should not happen")
|
||||
return ctrl.Result{RequeueAfter: defaultRequeueAfter}, nil
|
||||
}
|
||||
logger.Debug("checking for existing Velero Backup", "namespace", veleroNamespace)
|
||||
veleroBackupList := &velerov1.BackupList{}
|
||||
opts := []client.ListOption{
|
||||
client.InNamespace(veleroNamespace),
|
||||
client.MatchingLabels{
|
||||
backupsv1alpha1.OwningJobNamespaceLabel: j.Namespace,
|
||||
backupsv1alpha1.OwningJobNameLabel: j.Name,
|
||||
},
|
||||
}
|
||||
|
||||
if err := r.List(ctx, veleroBackupList, opts...); err != nil {
|
||||
logger.Error(err, "failed to get Velero Backup")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if len(veleroBackupList.Items) == 0 {
|
||||
// Create Velero Backup
|
||||
logger.Debug("Velero Backup not found, creating new one")
|
||||
if err := r.createVeleroBackup(ctx, j, veleroStrategy); err != nil {
|
||||
logger.Error(err, "failed to create Velero Backup")
|
||||
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("failed to create Velero Backup: %v", err))
|
||||
}
|
||||
// After successful Velero backup creation, set phase to Running
|
||||
if j.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning {
|
||||
logger.Debug("setting BackupJob phase to Running after successful Velero backup creation")
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseRunning
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob phase to Running")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
logger.Debug("created Velero Backup, requeuing")
|
||||
// Requeue to check status
|
||||
return ctrl.Result{RequeueAfter: defaultRequeueAfter}, nil
|
||||
}
|
||||
|
||||
if len(veleroBackupList.Items) > 1 {
|
||||
logger.Error(fmt.Errorf("too many Velero backups for BackupJob"), "found more than one Velero Backup referencing a single BackupJob as owner")
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseFailed
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status")
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
veleroBackup := veleroBackupList.Items[0].DeepCopy()
|
||||
logger.Debug("found existing Velero Backup", "phase", veleroBackup.Status.Phase)
|
||||
|
||||
// If Velero backup exists but phase is not Running, set it to Running
|
||||
// This handles the case where the backup was created but phase wasn't set yet
|
||||
if j.Status.Phase != backupsv1alpha1.BackupJobPhaseRunning {
|
||||
logger.Debug("setting BackupJob phase to Running (Velero backup already exists)")
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseRunning
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob phase to Running")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check Velero Backup status
|
||||
phase := string(veleroBackup.Status.Phase)
|
||||
if phase == "" {
|
||||
// Still in progress, requeue
|
||||
return ctrl.Result{RequeueAfter: defaultActiveJobPollingInterval}, nil
|
||||
}
|
||||
|
||||
// Step 4: On success - Create Backup resource and update status
|
||||
if phase == "Completed" {
|
||||
// Check if we already created the Backup resource
|
||||
if j.Status.BackupRef == nil {
|
||||
backup, err := r.createBackupResource(ctx, j, veleroBackup)
|
||||
if err != nil {
|
||||
return r.markBackupJobFailed(ctx, j, fmt.Sprintf("failed to create Backup resource: %v", err))
|
||||
}
|
||||
|
||||
now := metav1.Now()
|
||||
j.Status.BackupRef = &corev1.LocalObjectReference{Name: backup.Name}
|
||||
j.Status.CompletedAt = &now
|
||||
j.Status.Phase = backupsv1alpha1.BackupJobPhaseSucceeded
|
||||
if err := r.Status().Update(ctx, j); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("BackupJob succeeded", "backup", backup.Name)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Step 5: On failure
|
||||
if phase == "Failed" || phase == "PartiallyFailed" {
|
||||
message := fmt.Sprintf("Velero Backup failed with phase: %s", phase)
|
||||
if len(veleroBackup.Status.ValidationErrors) > 0 {
|
||||
message = fmt.Sprintf("%s: %v", message, veleroBackup.Status.ValidationErrors)
|
||||
}
|
||||
return r.markBackupJobFailed(ctx, j, message)
|
||||
}
|
||||
|
||||
// Still in progress (InProgress, New, etc.)
|
||||
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
|
||||
}
|
||||
|
||||
// resolveBucketStorageRef discovers S3 credentials from a Bucket storageRef
|
||||
// It follows this flow:
|
||||
// 1. Get the Bucket resource (apps.cozystack.io/v1alpha1)
|
||||
// 2. Find the BucketAccess that references this bucket
|
||||
// 3. Get the secret from BucketAccess.spec.credentialsSecretName
|
||||
// 4. Decode BucketInfo from secret.data.BucketInfo and extract S3 credentials
|
||||
func (r *BackupJobReconciler) resolveBucketStorageRef(ctx context.Context, storageRef corev1.TypedLocalObjectReference, namespace string) (*S3Credentials, error) {
|
||||
logger := getLogger(ctx)
|
||||
|
||||
// Step 1: Get the Bucket resource
|
||||
bucket := &unstructured.Unstructured{}
|
||||
bucket.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: *storageRef.APIGroup,
|
||||
Version: "v1alpha1",
|
||||
Kind: storageRef.Kind,
|
||||
})
|
||||
|
||||
if *storageRef.APIGroup != "apps.cozystack.io" {
|
||||
return nil, fmt.Errorf("Unsupported storage APIGroup: %v, expected apps.cozystack.io", storageRef.APIGroup)
|
||||
}
|
||||
bucketKey := client.ObjectKey{Namespace: namespace, Name: storageRef.Name}
|
||||
|
||||
if err := r.Get(ctx, bucketKey, bucket); err != nil {
|
||||
return nil, fmt.Errorf("failed to get Bucket %s: %w", storageRef.Name, err)
|
||||
}
|
||||
|
||||
// Step 2: Determine the bucket claim name
|
||||
// For apps.cozystack.io Bucket, the BucketClaim name is typically the same as the Bucket name
|
||||
// or follows a pattern. Based on the templates, it's usually the Release.Name which equals the Bucket name
|
||||
bucketName := storageRef.Name
|
||||
|
||||
// Step 3: Get BucketAccess by name (assuming BucketAccess name matches bucketName)
|
||||
bucketAccess := &unstructured.Unstructured{}
|
||||
bucketAccess.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: "objectstorage.k8s.io",
|
||||
Version: "v1alpha1",
|
||||
Kind: "BucketAccess",
|
||||
})
|
||||
|
||||
bucketAccessKey := client.ObjectKey{Name: "bucket-" + bucketName, Namespace: namespace}
|
||||
if err := r.Get(ctx, bucketAccessKey, bucketAccess); err != nil {
|
||||
return nil, fmt.Errorf("failed to get BucketAccess %s in namespace %s: %w", bucketName, namespace, err)
|
||||
}
|
||||
|
||||
// Step 4: Get the secret name from BucketAccess
|
||||
secretName, found, err := unstructured.NestedString(bucketAccess.Object, "spec", "credentialsSecretName")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get credentialsSecretName from BucketAccess: %w", err)
|
||||
}
|
||||
if !found || secretName == "" {
|
||||
return nil, fmt.Errorf("credentialsSecretName not found in BucketAccess %s", bucketAccessKey.Name)
|
||||
}
|
||||
|
||||
// Step 5: Get the secret
|
||||
secret := &corev1.Secret{}
|
||||
secretKey := client.ObjectKey{Namespace: namespace, Name: secretName}
|
||||
if err := r.Get(ctx, secretKey, secret); err != nil {
|
||||
return nil, fmt.Errorf("failed to get secret %s: %w", secretName, err)
|
||||
}
|
||||
|
||||
// Step 6: Decode BucketInfo from secret.data.BucketInfo
|
||||
bucketInfoData, found := secret.Data["BucketInfo"]
|
||||
if !found {
|
||||
return nil, fmt.Errorf("BucketInfo key not found in secret %s", secretName)
|
||||
}
|
||||
|
||||
// Parse JSON value
|
||||
var info bucketInfo
|
||||
if err := json.Unmarshal(bucketInfoData, &info); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal BucketInfo from secret %s: %w", secretName, err)
|
||||
}
|
||||
|
||||
// Step 7: Extract and return S3 credentials
|
||||
creds := &S3Credentials{
|
||||
BucketName: info.Spec.BucketName,
|
||||
Endpoint: info.Spec.SecretS3.Endpoint,
|
||||
Region: info.Spec.SecretS3.Region,
|
||||
AccessKeyID: info.Spec.SecretS3.AccessKeyID,
|
||||
AccessSecretKey: info.Spec.SecretS3.AccessSecretKey,
|
||||
}
|
||||
|
||||
logger.Debug("resolved S3 credentials from Bucket storageRef",
|
||||
"bucket", storageRef.Name,
|
||||
"bucketName", creds.BucketName,
|
||||
"endpoint", creds.Endpoint)
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
// createS3CredsForVelero creates or updates a Kubernetes Secret containing
|
||||
// Velero S3 credentials in the format expected by Velero's cloud-credentials plugin.
|
||||
func (r *BackupJobReconciler) createS3CredsForVelero(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, creds *S3Credentials) error {
|
||||
logger := getLogger(ctx)
|
||||
secretName := storageS3SecretName(backupJob.Namespace, backupJob.Name)
|
||||
secretNamespace := veleroNamespace
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: secretName,
|
||||
Namespace: secretNamespace,
|
||||
},
|
||||
Type: corev1.SecretTypeOpaque,
|
||||
StringData: map[string]string{
|
||||
"cloud": fmt.Sprintf(`[default]
|
||||
aws_access_key_id=%s
|
||||
aws_secret_access_key=%s
|
||||
|
||||
services = seaweed-s3
|
||||
[services seaweed-s3]
|
||||
s3 =
|
||||
endpoint_url = %s
|
||||
`, creds.AccessKeyID, creds.AccessSecretKey, creds.Endpoint),
|
||||
},
|
||||
}
|
||||
|
||||
foundSecret := &corev1.Secret{}
|
||||
secretKey := client.ObjectKey{Name: secretName, Namespace: secretNamespace}
|
||||
err := r.Get(ctx, secretKey, foundSecret)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// Create the Secret
|
||||
if err := r.Create(ctx, secret); err != nil {
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "SecretCreationFailed",
|
||||
fmt.Sprintf("Failed to create Velero credentials secret %s/%s: %v", secretNamespace, secretName, err))
|
||||
return fmt.Errorf("failed to create Velero credentials secret: %w", err)
|
||||
}
|
||||
logger.Debug("created Velero credentials secret", "secret", secretName)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "SecretCreated",
|
||||
fmt.Sprintf("Created Velero credentials secret %s/%s", secretNamespace, secretName))
|
||||
} else if err == nil {
|
||||
// Update if necessary - only update if the secret data has actually changed
|
||||
// Compare the new secret data with existing secret data
|
||||
existingData := foundSecret.Data
|
||||
if existingData == nil {
|
||||
existingData = make(map[string][]byte)
|
||||
}
|
||||
newData := make(map[string][]byte)
|
||||
for k, v := range secret.StringData {
|
||||
newData[k] = []byte(v)
|
||||
}
|
||||
|
||||
// Check if data has changed
|
||||
dataChanged := false
|
||||
if len(existingData) != len(newData) {
|
||||
dataChanged = true
|
||||
} else {
|
||||
for k, newVal := range newData {
|
||||
existingVal, exists := existingData[k]
|
||||
if !exists || !reflect.DeepEqual(existingVal, newVal) {
|
||||
dataChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if dataChanged {
|
||||
foundSecret.StringData = secret.StringData
|
||||
foundSecret.Data = nil // Clear .Data so .StringData will be used
|
||||
if err := r.Update(ctx, foundSecret); err != nil {
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "SecretUpdateFailed",
|
||||
fmt.Sprintf("Failed to update Velero credentials secret %s/%s: %v", secretNamespace, secretName, err))
|
||||
return fmt.Errorf("failed to update Velero credentials secret: %w", err)
|
||||
}
|
||||
logger.Debug("updated Velero credentials secret", "secret", secretName)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "SecretUpdated",
|
||||
fmt.Sprintf("Updated Velero credentials secret %s/%s", secretNamespace, secretName))
|
||||
} else {
|
||||
logger.Debug("Velero credentials secret data unchanged, skipping update", "secret", secretName)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error checking for existing Velero credentials secret: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createBackupStorageLocation creates or updates a Velero BackupStorageLocation resource.
|
||||
func (r *BackupJobReconciler) createBackupStorageLocation(ctx context.Context, bsl *velerov1.BackupStorageLocation) error {
|
||||
logger := getLogger(ctx)
|
||||
foundBSL := &velerov1.BackupStorageLocation{}
|
||||
bslKey := client.ObjectKey{Name: bsl.Name, Namespace: bsl.Namespace}
|
||||
|
||||
err := r.Get(ctx, bslKey, foundBSL)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// Create the BackupStorageLocation
|
||||
if err := r.Create(ctx, bsl); err != nil {
|
||||
return fmt.Errorf("failed to create BackupStorageLocation: %w", err)
|
||||
}
|
||||
logger.Debug("created BackupStorageLocation", "name", bsl.Name, "namespace", bsl.Namespace)
|
||||
} else if err == nil {
|
||||
// Update if necessary - use patch to avoid conflicts with Velero's status updates
|
||||
// Only update if the spec has actually changed
|
||||
if !reflect.DeepEqual(foundBSL.Spec, bsl.Spec) {
|
||||
// Retry on conflict since Velero may be updating status concurrently
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := r.Get(ctx, bslKey, foundBSL); err != nil {
|
||||
return fmt.Errorf("failed to get BackupStorageLocation for update: %w", err)
|
||||
}
|
||||
foundBSL.Spec = bsl.Spec
|
||||
if err := r.Update(ctx, foundBSL); err != nil {
|
||||
if errors.IsConflict(err) && i < 2 {
|
||||
logger.Debug("conflict updating BackupStorageLocation, retrying", "attempt", i+1)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to update BackupStorageLocation: %w", err)
|
||||
}
|
||||
logger.Debug("updated BackupStorageLocation", "name", bsl.Name, "namespace", bsl.Namespace)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
logger.Debug("BackupStorageLocation spec unchanged, skipping update", "name", bsl.Name, "namespace", bsl.Namespace)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error checking for existing BackupStorageLocation: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createVolumeSnapshotLocation creates or updates a Velero VolumeSnapshotLocation resource.
|
||||
func (r *BackupJobReconciler) createVolumeSnapshotLocation(ctx context.Context, vsl *velerov1.VolumeSnapshotLocation) error {
|
||||
logger := getLogger(ctx)
|
||||
foundVSL := &velerov1.VolumeSnapshotLocation{}
|
||||
vslKey := client.ObjectKey{Name: vsl.Name, Namespace: vsl.Namespace}
|
||||
|
||||
err := r.Get(ctx, vslKey, foundVSL)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// Create the VolumeSnapshotLocation
|
||||
if err := r.Create(ctx, vsl); err != nil {
|
||||
return fmt.Errorf("failed to create VolumeSnapshotLocation: %w", err)
|
||||
}
|
||||
logger.Debug("created VolumeSnapshotLocation", "name", vsl.Name, "namespace", vsl.Namespace)
|
||||
} else if err == nil {
|
||||
// Update if necessary - only update if the spec has actually changed
|
||||
if !reflect.DeepEqual(foundVSL.Spec, vsl.Spec) {
|
||||
// Retry on conflict since Velero may be updating status concurrently
|
||||
for i := 0; i < 3; i++ {
|
||||
if err := r.Get(ctx, vslKey, foundVSL); err != nil {
|
||||
return fmt.Errorf("failed to get VolumeSnapshotLocation for update: %w", err)
|
||||
}
|
||||
foundVSL.Spec = vsl.Spec
|
||||
if err := r.Update(ctx, foundVSL); err != nil {
|
||||
if errors.IsConflict(err) && i < 2 {
|
||||
logger.Debug("conflict updating VolumeSnapshotLocation, retrying", "attempt", i+1)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to update VolumeSnapshotLocation: %w", err)
|
||||
}
|
||||
logger.Debug("updated VolumeSnapshotLocation", "name", vsl.Name, "namespace", vsl.Namespace)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
logger.Debug("VolumeSnapshotLocation spec unchanged, skipping update", "name", vsl.Name, "namespace", vsl.Namespace)
|
||||
}
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("error checking for existing VolumeSnapshotLocation: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) markBackupJobFailed(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, message string) (ctrl.Result, error) {
|
||||
logger := getLogger(ctx)
|
||||
now := metav1.Now()
|
||||
backupJob.Status.CompletedAt = &now
|
||||
backupJob.Status.Phase = backupsv1alpha1.BackupJobPhaseFailed
|
||||
backupJob.Status.Message = message
|
||||
|
||||
// Add condition
|
||||
backupJob.Status.Conditions = append(backupJob.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "BackupFailed",
|
||||
Message: message,
|
||||
LastTransitionTime: now,
|
||||
})
|
||||
|
||||
if err := r.Status().Update(ctx, backupJob); err != nil {
|
||||
logger.Error(err, "failed to update BackupJob status to Failed")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Debug("BackupJob failed", "message", message)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) createVeleroBackup(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, strategy *strategyv1alpha1.Velero) error {
|
||||
logger := getLogger(ctx)
|
||||
logger.Debug("createVeleroBackup called", "strategy", strategy.Name)
|
||||
|
||||
mapping, err := r.RESTMapping(schema.GroupKind{Group: *backupJob.Spec.ApplicationRef.APIGroup, Kind: backupJob.Spec.ApplicationRef.Kind})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ns := backupJob.Namespace
|
||||
if mapping.Scope.Name() != meta.RESTScopeNameNamespace {
|
||||
ns = ""
|
||||
}
|
||||
app, err := r.Resource(mapping.Resource).Namespace(ns).Get(ctx, backupJob.Spec.ApplicationRef.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
veleroBackupSpec, err := template.Template(&strategy.Spec.Template.Spec, app.Object)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
veleroBackup := &velerov1.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: fmt.Sprintf("%s.%s-", backupJob.Namespace, backupJob.Name),
|
||||
Namespace: veleroNamespace,
|
||||
Labels: map[string]string{
|
||||
backupsv1alpha1.OwningJobNameLabel: backupJob.Name,
|
||||
backupsv1alpha1.OwningJobNamespaceLabel: backupJob.Namespace,
|
||||
},
|
||||
},
|
||||
Spec: *veleroBackupSpec,
|
||||
}
|
||||
name := veleroBackup.GenerateName
|
||||
if err := r.Create(ctx, veleroBackup); err != nil {
|
||||
if veleroBackup.Name != "" {
|
||||
name = veleroBackup.Name
|
||||
}
|
||||
logger.Error(err, "failed to create Velero Backup", "name", veleroBackup.Name)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeWarning, "VeleroBackupCreationFailed",
|
||||
fmt.Sprintf("Failed to create Velero Backup %s/%s: %v", veleroNamespace, name, err))
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("created Velero Backup", "name", veleroBackup.Name, "namespace", veleroBackup.Namespace)
|
||||
r.Recorder.Event(backupJob, corev1.EventTypeNormal, "VeleroBackupCreated",
|
||||
fmt.Sprintf("Created Velero Backup %s/%s", veleroNamespace, name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BackupJobReconciler) createBackupResource(ctx context.Context, backupJob *backupsv1alpha1.BackupJob, veleroBackup *velerov1.Backup) (*backupsv1alpha1.Backup, error) {
|
||||
logger := getLogger(ctx)
|
||||
// Extract artifact information from Velero Backup
|
||||
// Create a basic artifact referencing the Velero backup
|
||||
artifact := &backupsv1alpha1.BackupArtifact{
|
||||
URI: fmt.Sprintf("velero://%s/%s", backupJob.Namespace, veleroBackup.Name),
|
||||
}
|
||||
|
||||
// Get takenAt from Velero Backup creation timestamp or status
|
||||
takenAt := metav1.Now()
|
||||
if veleroBackup.Status.StartTimestamp != nil {
|
||||
takenAt = *veleroBackup.Status.StartTimestamp
|
||||
} else if !veleroBackup.CreationTimestamp.IsZero() {
|
||||
takenAt = veleroBackup.CreationTimestamp
|
||||
}
|
||||
|
||||
// Extract driver metadata (e.g., Velero backup name)
|
||||
driverMetadata := map[string]string{
|
||||
"velero.io/backup-name": veleroBackup.Name,
|
||||
"velero.io/backup-namespace": veleroBackup.Namespace,
|
||||
}
|
||||
|
||||
backup := &backupsv1alpha1.Backup{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("%s", backupJob.Name),
|
||||
Namespace: backupJob.Namespace,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: backupJob.APIVersion,
|
||||
Kind: backupJob.Kind,
|
||||
Name: backupJob.Name,
|
||||
UID: backupJob.UID,
|
||||
Controller: boolPtr(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
Spec: backupsv1alpha1.BackupSpec{
|
||||
ApplicationRef: backupJob.Spec.ApplicationRef,
|
||||
StorageRef: backupJob.Spec.StorageRef,
|
||||
StrategyRef: backupJob.Spec.StrategyRef,
|
||||
TakenAt: takenAt,
|
||||
DriverMetadata: driverMetadata,
|
||||
},
|
||||
Status: backupsv1alpha1.BackupStatus{
|
||||
Phase: backupsv1alpha1.BackupPhaseReady,
|
||||
},
|
||||
}
|
||||
|
||||
if backupJob.Spec.PlanRef != nil {
|
||||
backup.Spec.PlanRef = backupJob.Spec.PlanRef
|
||||
}
|
||||
|
||||
if artifact != nil {
|
||||
backup.Status.Artifact = artifact
|
||||
}
|
||||
|
||||
if err := r.Create(ctx, backup); err != nil {
|
||||
logger.Error(err, "failed to create Backup resource")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Debug("created Backup resource", "name", backup.Name)
|
||||
return backup, nil
|
||||
}
|
||||
@@ -5,28 +5,21 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cozystack/cozystack/internal/controller/dashboard"
|
||||
"github.com/cozystack/cozystack/internal/shared/crdmem"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
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/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
@@ -40,128 +33,22 @@ type CozystackResourceDefinitionReconciler struct {
|
||||
lastEvent time.Time
|
||||
lastHandled time.Time
|
||||
|
||||
mem *crdmem.Memory
|
||||
|
||||
// Track static resources initialization
|
||||
staticResourcesInitialized bool
|
||||
staticResourcesMutex sync.Mutex
|
||||
CozystackAPIKind string
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
crd := &cozyv1alpha1.CozystackResourceDefinition{}
|
||||
err := r.Get(ctx, types.NamespacedName{Name: req.Name}, crd)
|
||||
if err == nil {
|
||||
if r.mem != nil {
|
||||
r.mem.Upsert(crd)
|
||||
}
|
||||
|
||||
mgr := dashboard.NewManager(
|
||||
r.Client,
|
||||
r.Scheme,
|
||||
dashboard.WithCRDListFunc(func(c context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error) {
|
||||
if r.mem != nil {
|
||||
return r.mem.ListFromCacheOrAPI(c, r.Client)
|
||||
}
|
||||
var list cozyv1alpha1.CozystackResourceDefinitionList
|
||||
if err := r.Client.List(c, &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list.Items, nil
|
||||
}),
|
||||
)
|
||||
|
||||
if res, derr := mgr.EnsureForCRD(ctx, crd); derr != nil || res.Requeue || res.RequeueAfter > 0 {
|
||||
return res, derr
|
||||
}
|
||||
|
||||
// After processing CRD, perform cleanup of orphaned resources
|
||||
// This should be done after cache warming to ensure all current resources are known
|
||||
if cleanupErr := mgr.CleanupOrphanedResources(ctx); cleanupErr != nil {
|
||||
logger.Error(cleanupErr, "Failed to cleanup orphaned dashboard resources")
|
||||
// Don't fail the reconciliation, just log the error
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
r.lastEvent = time.Now()
|
||||
r.mu.Unlock()
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Handle error cases (err is guaranteed to be non-nil here)
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// If resource is not found, clean up from memory
|
||||
if r.mem != nil {
|
||||
r.mem.Delete(req.Name)
|
||||
}
|
||||
if req.Namespace == "cozy-system" && req.Name == "cozystack-api" {
|
||||
return r.debouncedRestart(ctx, logger)
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// initializeStaticResourcesOnce ensures static resources are created only once
|
||||
func (r *CozystackResourceDefinitionReconciler) initializeStaticResourcesOnce(ctx context.Context) error {
|
||||
r.staticResourcesMutex.Lock()
|
||||
defer r.staticResourcesMutex.Unlock()
|
||||
|
||||
if r.staticResourcesInitialized {
|
||||
return nil // Already initialized
|
||||
}
|
||||
|
||||
// Create dashboard manager and initialize static resources
|
||||
mgr := dashboard.NewManager(
|
||||
r.Client,
|
||||
r.Scheme,
|
||||
dashboard.WithCRDListFunc(func(c context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error) {
|
||||
if r.mem != nil {
|
||||
return r.mem.ListFromCacheOrAPI(c, r.Client)
|
||||
}
|
||||
var list cozyv1alpha1.CozystackResourceDefinitionList
|
||||
if err := r.Client.List(c, &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list.Items, nil
|
||||
}),
|
||||
)
|
||||
|
||||
if err := mgr.InitializeStaticResources(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.staticResourcesInitialized = true
|
||||
log.FromContext(ctx).Info("Static dashboard resources initialized successfully")
|
||||
return nil
|
||||
// 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
|
||||
}
|
||||
if r.mem == nil {
|
||||
r.mem = crdmem.Global()
|
||||
}
|
||||
if err := r.mem.EnsurePrimingWithManager(mgr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize static resources once during controller startup using manager.Runnable
|
||||
if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
|
||||
if err := r.initializeStaticResourcesOnce(ctx); err != nil {
|
||||
log.FromContext(ctx).Error(err, "Failed to initialize static resources")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystackresource-controller").
|
||||
For(&cozyv1alpha1.CozystackResourceDefinition{}, builder.WithPredicates()).
|
||||
Watches(
|
||||
&cozyv1alpha1.CozystackResourceDefinition{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
@@ -176,9 +63,6 @@ func (r *CozystackResourceDefinitionReconciler) SetupWithManager(mgr ctrl.Manage
|
||||
}}
|
||||
}),
|
||||
).
|
||||
WithOptions(controller.Options{
|
||||
MaxConcurrentReconciles: 5, // Allow more concurrent reconciles with proper rate limiting
|
||||
}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
@@ -188,22 +72,18 @@ type crdHashView struct {
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) computeConfigHash(ctx context.Context) (string, error) {
|
||||
var items []cozyv1alpha1.CozystackResourceDefinition
|
||||
if r.mem != nil {
|
||||
list, err := r.mem.ListFromCacheOrAPI(ctx, r.Client)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
items = list
|
||||
list := &cozyv1alpha1.CozystackResourceDefinitionList{}
|
||||
if err := r.List(ctx, list); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name })
|
||||
slices.SortFunc(list.Items, sortCozyRDs)
|
||||
|
||||
views := make([]crdHashView, 0, len(items))
|
||||
for i := range items {
|
||||
views := make([]crdHashView, 0, len(list.Items))
|
||||
for i := range list.Items {
|
||||
views = append(views, crdHashView{
|
||||
Name: items[i].Name,
|
||||
Spec: items[i].Spec,
|
||||
Name: list.Items[i].Name,
|
||||
Spec: list.Items[i].Spec,
|
||||
})
|
||||
}
|
||||
b, err := json.Marshal(views)
|
||||
@@ -214,7 +94,9 @@ func (r *CozystackResourceDefinitionReconciler) computeConfigHash(ctx context.Co
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Context, logger logr.Logger) (ctrl.Result, error) {
|
||||
func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Context) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
r.mu.Lock()
|
||||
le := r.lastEvent
|
||||
lh := r.lastHandled
|
||||
@@ -239,15 +121,12 @@ func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Con
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
deploy := &appsv1.Deployment{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"}, deploy); err != nil {
|
||||
tpl, obj, patch, err := r.getWorkload(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack-api"})
|
||||
if err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
if deploy.Spec.Template.Annotations == nil {
|
||||
deploy.Spec.Template.Annotations = map[string]string{}
|
||||
}
|
||||
oldHash := deploy.Spec.Template.Annotations["cozystack.io/config-hash"]
|
||||
oldHash := tpl.Annotations["cozystack.io/config-hash"]
|
||||
|
||||
if oldHash == newHash && oldHash != "" {
|
||||
r.mu.Lock()
|
||||
@@ -257,10 +136,9 @@ func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Con
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
patch := client.MergeFrom(deploy.DeepCopy())
|
||||
deploy.Spec.Template.Annotations["cozystack.io/config-hash"] = newHash
|
||||
tpl.Annotations["cozystack.io/config-hash"] = newHash
|
||||
|
||||
if err := r.Patch(ctx, deploy, patch); err != nil {
|
||||
if err := r.Patch(ctx, obj, patch); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
@@ -272,3 +150,40 @@ func (r *CozystackResourceDefinitionReconciler) debouncedRestart(ctx context.Con
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=cozystackresourcedefinitions,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;update;patch
|
||||
|
||||
// CozystackResourceDefinitionHelmReconciler reconciles CozystackResourceDefinitions
|
||||
// and updates related HelmReleases when a CozyRD changes.
|
||||
// This controller does NOT watch HelmReleases to avoid mutual reconciliation storms
|
||||
// with Flux's helm-controller.
|
||||
type CozystackResourceDefinitionHelmReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionHelmReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Get the CozystackResourceDefinition that triggered this reconciliation
|
||||
crd := &cozyv1alpha1.CozystackResourceDefinition{}
|
||||
if err := r.Get(ctx, req.NamespacedName, crd); err != nil {
|
||||
logger.Error(err, "failed to get CozystackResourceDefinition", "name", req.Name)
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// Update HelmReleases related to this specific CozyRD
|
||||
if err := r.updateHelmReleasesForCRD(ctx, crd); err != nil {
|
||||
logger.Error(err, "failed to update HelmReleases for CRD", "crd", crd.Name)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionHelmReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystackresourcedefinition-helm-reconciler").
|
||||
For(&cozyv1alpha1.CozystackResourceDefinition{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
// updateHelmReleasesForCRD updates all HelmReleases that match the application labels from CozystackResourceDefinition
|
||||
func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleasesForCRD(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Use application labels to find HelmReleases
|
||||
// Labels: apps.cozystack.io/application.kind and apps.cozystack.io/application.group
|
||||
applicationKind := crd.Spec.Application.Kind
|
||||
|
||||
// Validate that applicationKind is non-empty
|
||||
if applicationKind == "" {
|
||||
logger.V(4).Info("Skipping HelmRelease update: Application.Kind is empty", "crd", crd.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
applicationGroup := "apps.cozystack.io" // All applications use this group
|
||||
|
||||
// Build label selector for HelmReleases
|
||||
// Only reconcile HelmReleases with cozystack.io/ui=true label
|
||||
labelSelector := client.MatchingLabels{
|
||||
"apps.cozystack.io/application.kind": applicationKind,
|
||||
"apps.cozystack.io/application.group": applicationGroup,
|
||||
"cozystack.io/ui": "true",
|
||||
}
|
||||
|
||||
// List all HelmReleases with matching labels
|
||||
hrList := &helmv2.HelmReleaseList{}
|
||||
if err := r.List(ctx, hrList, labelSelector); err != nil {
|
||||
logger.Error(err, "failed to list HelmReleases", "kind", applicationKind, "group", applicationGroup)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.V(4).Info("Found HelmReleases to update", "crd", crd.Name, "kind", applicationKind, "count", len(hrList.Items))
|
||||
|
||||
// Update each HelmRelease
|
||||
for i := range hrList.Items {
|
||||
hr := &hrList.Items[i]
|
||||
if err := r.updateHelmReleaseChart(ctx, hr, crd); err != nil {
|
||||
logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// expectedValuesFrom returns the expected valuesFrom configuration for HelmReleases
|
||||
func expectedValuesFrom() []helmv2.ValuesReference {
|
||||
return []helmv2.ValuesReference{
|
||||
{
|
||||
Kind: "Secret",
|
||||
Name: "cozystack-values",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// valuesFromEqual compares two ValuesReference slices
|
||||
func valuesFromEqual(a, b []helmv2.ValuesReference) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i].Kind != b[i].Kind ||
|
||||
a[i].Name != b[i].Name ||
|
||||
a[i].ValuesKey != b[i].ValuesKey ||
|
||||
a[i].TargetPath != b[i].TargetPath ||
|
||||
a[i].Optional != b[i].Optional {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// updateHelmReleaseChart updates the chart and valuesFrom in HelmRelease based on CozystackResourceDefinition
|
||||
func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
logger := log.FromContext(ctx)
|
||||
hrCopy := hr.DeepCopy()
|
||||
updated := false
|
||||
|
||||
// Validate ChartRef configuration exists
|
||||
if crd.Spec.Release.ChartRef == nil ||
|
||||
crd.Spec.Release.ChartRef.Kind == "" ||
|
||||
crd.Spec.Release.ChartRef.Name == "" ||
|
||||
crd.Spec.Release.ChartRef.Namespace == "" {
|
||||
logger.Error(fmt.Errorf("invalid ChartRef in CRD"), "Skipping HelmRelease chartRef update: ChartRef is nil or incomplete",
|
||||
"crd", crd.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use ChartRef directly from CRD
|
||||
expectedChartRef := crd.Spec.Release.ChartRef
|
||||
|
||||
// Check if chartRef needs to be updated
|
||||
if hrCopy.Spec.ChartRef == nil {
|
||||
hrCopy.Spec.ChartRef = expectedChartRef
|
||||
// Clear the old chart field when switching to chartRef
|
||||
hrCopy.Spec.Chart = nil
|
||||
updated = true
|
||||
} else if hrCopy.Spec.ChartRef.Kind != expectedChartRef.Kind ||
|
||||
hrCopy.Spec.ChartRef.Name != expectedChartRef.Name ||
|
||||
hrCopy.Spec.ChartRef.Namespace != expectedChartRef.Namespace {
|
||||
hrCopy.Spec.ChartRef = expectedChartRef
|
||||
updated = true
|
||||
}
|
||||
|
||||
// Check and update valuesFrom configuration
|
||||
expected := expectedValuesFrom()
|
||||
if !valuesFromEqual(hrCopy.Spec.ValuesFrom, expected) {
|
||||
logger.V(4).Info("Updating HelmRelease valuesFrom", "name", hr.Name, "namespace", hr.Namespace)
|
||||
hrCopy.Spec.ValuesFrom = expected
|
||||
updated = true
|
||||
}
|
||||
|
||||
if updated {
|
||||
logger.V(4).Info("Updating HelmRelease chartRef", "name", hr.Name, "namespace", hr.Namespace)
|
||||
if err := r.Update(ctx, hrCopy); err != nil {
|
||||
return fmt.Errorf("failed to update HelmRelease: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -58,8 +58,8 @@ func (m *Manager) ensureBreadcrumb(ctx context.Context, crd *cozyv1alpha1.Cozyst
|
||||
"breadcrumbItems": items,
|
||||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
|
||||
@@ -30,10 +30,6 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al
|
||||
name := fmt.Sprintf("stock-namespace-%s.%s.%s", g, v, plural)
|
||||
id := fmt.Sprintf("stock-namespace-/%s/%s/%s", g, v, plural)
|
||||
|
||||
// Badge content & color derived from kind
|
||||
badgeText := initialsFromKind(kind) // e.g., "VirtualMachine" -> "VM", "Bucket" -> "B"
|
||||
badgeColor := hexColorForKind(kind) // deterministic, dark enough for white text
|
||||
|
||||
obj := &dashv1alpha1.CustomColumnsOverride{}
|
||||
obj.SetName(name)
|
||||
|
||||
@@ -62,25 +58,11 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al
|
||||
},
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"type": "ResourceBadge",
|
||||
"data": map[string]any{
|
||||
"id": "header-badge",
|
||||
"text": badgeText,
|
||||
"title": strings.ToLower(kind), // optional tooltip
|
||||
"style": map[string]any{
|
||||
"backgroundColor": badgeColor,
|
||||
"borderRadius": "20px",
|
||||
"color": "#fff",
|
||||
"display": "inline-block",
|
||||
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
|
||||
"fontSize": "15px",
|
||||
"fontWeight": 400,
|
||||
"lineHeight": "24px",
|
||||
"minWidth": 24,
|
||||
"padding": "0 9px",
|
||||
"textAlign": "center",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
"value": kind,
|
||||
// abbreviation auto-generated by ResourceBadge from value
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
@@ -145,8 +127,8 @@ func (m *Manager) ensureCustomColumnsOverride(ctx context.Context, crd *cozyv1al
|
||||
},
|
||||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// ensureCustomFormsOverride creates or updates a CustomFormsOverride resource for the given CRD
|
||||
@@ -45,16 +46,25 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph
|
||||
}
|
||||
}
|
||||
|
||||
// Build schema with multilineString for string fields without enum
|
||||
l := log.FromContext(ctx)
|
||||
schema, err := buildMultilineStringSchema(crd.Spec.Application.OpenAPISchema)
|
||||
if err != nil {
|
||||
// If schema parsing fails, log the error and use an empty schema
|
||||
l.Error(err, "failed to build multiline string schema, using empty schema", "crd", crd.Name)
|
||||
schema = map[string]any{}
|
||||
}
|
||||
|
||||
spec := map[string]any{
|
||||
"customizationId": customizationID,
|
||||
"hidden": hidden,
|
||||
"sort": sort,
|
||||
"schema": map[string]any{}, // {}
|
||||
"schema": schema,
|
||||
"strategy": "merge",
|
||||
}
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
@@ -73,3 +83,112 @@ func (m *Manager) ensureCustomFormsOverride(ctx context.Context, crd *cozyv1alph
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// buildMultilineStringSchema parses OpenAPI schema and creates schema with multilineString
|
||||
// for all string fields inside spec that don't have enum
|
||||
func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
|
||||
if openAPISchema == "" {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
var root map[string]any
|
||||
if err := json.Unmarshal([]byte(openAPISchema), &root); err != nil {
|
||||
return nil, fmt.Errorf("cannot parse openAPISchema: %w", err)
|
||||
}
|
||||
|
||||
props, _ := root["properties"].(map[string]any)
|
||||
if props == nil {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
schema := map[string]any{
|
||||
"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))
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
// processSpecProperties recursively processes spec properties and adds multilineString type
|
||||
// for string fields without enum
|
||||
func processSpecProperties(props map[string]any, schemaProps map[string]any) {
|
||||
for pname, raw := range props {
|
||||
sub, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
typ, _ := sub["type"].(string)
|
||||
|
||||
switch typ {
|
||||
case "string":
|
||||
// Check if this string field has enum
|
||||
if !hasEnum(sub) {
|
||||
// Add multilineString type for this field
|
||||
if schemaProps[pname] == nil {
|
||||
schemaProps[pname] = map[string]any{}
|
||||
}
|
||||
fieldSchema := schemaProps[pname].(map[string]any)
|
||||
fieldSchema["type"] = "multilineString"
|
||||
}
|
||||
case "object":
|
||||
// Recursively process nested objects
|
||||
if childProps, ok := sub["properties"].(map[string]any); ok {
|
||||
fieldSchema, ok := schemaProps[pname].(map[string]any)
|
||||
if !ok {
|
||||
fieldSchema = map[string]any{}
|
||||
schemaProps[pname] = fieldSchema
|
||||
}
|
||||
nestedSchemaProps, ok := fieldSchema["properties"].(map[string]any)
|
||||
if !ok {
|
||||
nestedSchemaProps = map[string]any{}
|
||||
fieldSchema["properties"] = nestedSchemaProps
|
||||
}
|
||||
processSpecProperties(childProps, nestedSchemaProps)
|
||||
}
|
||||
case "array":
|
||||
// Check if array items are objects with properties
|
||||
if items, ok := sub["items"].(map[string]any); ok {
|
||||
if itemProps, ok := items["properties"].(map[string]any); ok {
|
||||
// Create array item schema
|
||||
fieldSchema, ok := schemaProps[pname].(map[string]any)
|
||||
if !ok {
|
||||
fieldSchema = map[string]any{}
|
||||
schemaProps[pname] = fieldSchema
|
||||
}
|
||||
itemSchema, ok := fieldSchema["items"].(map[string]any)
|
||||
if !ok {
|
||||
itemSchema = map[string]any{}
|
||||
fieldSchema["items"] = itemSchema
|
||||
}
|
||||
itemSchemaProps, ok := itemSchema["properties"].(map[string]any)
|
||||
if !ok {
|
||||
itemSchemaProps = map[string]any{}
|
||||
itemSchema["properties"] = itemSchemaProps
|
||||
}
|
||||
processSpecProperties(itemProps, itemSchemaProps)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
171
internal/controller/dashboard/customformsoverride_test.go
Normal file
171
internal/controller/dashboard/customformsoverride_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildMultilineStringSchema(t *testing.T) {
|
||||
// Test OpenAPI schema with various field types
|
||||
openAPISchema := `{
|
||||
"properties": {
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
schema, err := buildMultilineStringSchema(openAPISchema)
|
||||
if err != nil {
|
||||
t.Fatalf("buildMultilineStringSchema failed: %v", err)
|
||||
}
|
||||
|
||||
// Marshal to JSON for easier inspection
|
||||
schemaJSON, err := json.MarshalIndent(schema, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal schema: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Generated schema:\n%s", schemaJSON)
|
||||
|
||||
// Verify that simpleString has multilineString type
|
||||
props, ok := schema["properties"].(map[string]any)
|
||||
if !ok {
|
||||
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)
|
||||
if !ok {
|
||||
t.Fatal("simpleString not found in spec.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["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["type"] != nil {
|
||||
t.Error("numberField should not have any type override")
|
||||
}
|
||||
}
|
||||
|
||||
// Check nested object
|
||||
nestedObject, ok := specProps["nestedObject"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("nestedObject not found in spec.properties")
|
||||
}
|
||||
nestedProps, ok := nestedObject["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("nestedObject.properties is not a map")
|
||||
}
|
||||
|
||||
// Check nestedString
|
||||
nestedString, ok := nestedProps["nestedString"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("nestedString not found in nestedObject.properties")
|
||||
}
|
||||
if nestedString["type"] != "multilineString" {
|
||||
t.Errorf("nestedString should have type multilineString, got %v", nestedString["type"])
|
||||
}
|
||||
|
||||
// Check array of objects
|
||||
arrayOfObjects, ok := specProps["arrayOfObjects"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("arrayOfObjects not found in spec.properties")
|
||||
}
|
||||
items, ok := arrayOfObjects["items"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("arrayOfObjects.items is not a map")
|
||||
}
|
||||
itemProps, ok := items["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("arrayOfObjects.items.properties is not a map")
|
||||
}
|
||||
itemString, ok := itemProps["itemString"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("itemString not found in arrayOfObjects.items.properties")
|
||||
}
|
||||
if itemString["type"] != "multilineString" {
|
||||
t.Errorf("itemString should have type multilineString, got %v", itemString["type"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMultilineStringSchemaEmpty(t *testing.T) {
|
||||
schema, err := buildMultilineStringSchema("")
|
||||
if err != nil {
|
||||
t.Fatalf("buildMultilineStringSchema failed on empty string: %v", err)
|
||||
}
|
||||
if len(schema) != 0 {
|
||||
t.Errorf("Expected empty schema for empty input, got %v", schema)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMultilineStringSchemaInvalidJSON(t *testing.T) {
|
||||
schema, err := buildMultilineStringSchema("{invalid json")
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid JSON")
|
||||
}
|
||||
if schema != nil {
|
||||
t.Errorf("Expected nil schema for invalid JSON, got %v", schema)
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,8 @@ func (m *Manager) ensureCustomFormsPrefill(ctx context.Context, crd *cozyv1alpha
|
||||
return reconcile.Result{}, err
|
||||
}
|
||||
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, m.client, cfp, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, cfp, m.scheme); err != nil {
|
||||
_, err = controllerutil.CreateOrUpdate(ctx, m.Client, cfp, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, cfp, m.Scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
|
||||
@@ -44,6 +44,9 @@ func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.Cozystack
|
||||
if flags.Secrets {
|
||||
tabs = append(tabs, secretsTab(kind))
|
||||
}
|
||||
if prefix, ok := vncTabPrefix(kind); ok {
|
||||
tabs = append(tabs, vncTab(prefix))
|
||||
}
|
||||
tabs = append(tabs, yamlTab(plural))
|
||||
|
||||
// Use unified factory creation
|
||||
@@ -53,7 +56,6 @@ func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.Cozystack
|
||||
Kind: kind,
|
||||
Plural: plural,
|
||||
Title: strings.ToLower(plural),
|
||||
Size: BadgeSizeLarge,
|
||||
}
|
||||
|
||||
spec := createUnifiedFactory(config, tabs, []any{resourceFetch})
|
||||
@@ -61,8 +63,8 @@ func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.Cozystack
|
||||
obj := &dashv1alpha1.Factory{}
|
||||
obj.SetName(factoryName)
|
||||
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.scheme); err != nil {
|
||||
_, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error {
|
||||
if err := controllerutil.SetOwnerReference(crd, obj, m.Scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add dashboard labels to dynamic resources
|
||||
@@ -115,7 +117,7 @@ func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[str
|
||||
"gap": float64(6),
|
||||
},
|
||||
"children": []any{
|
||||
createUnifiedBadgeFromKind("ns-badge", "Namespace", "namespace", BadgeSizeMedium),
|
||||
createUnifiedBadgeFromKind("ns-badge", "Namespace"),
|
||||
antdLink("namespace-link",
|
||||
"{reqsJsonPath[0]['.metadata.namespace']['-']}",
|
||||
"/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
|
||||
@@ -151,6 +153,27 @@ func detailsTab(kind, endpoint, schemaJSON string, keysOrder [][]string) map[str
|
||||
}),
|
||||
paramsList,
|
||||
}
|
||||
if kind == "VirtualPrivateCloud" {
|
||||
rightColStack = append(rightColStack,
|
||||
antdFlexVertical("vpc-subnets-block", 4, []any{
|
||||
antdText("vpc-subnets-label", true, "Subnets", nil),
|
||||
map[string]any{
|
||||
"type": "EnrichedTable",
|
||||
"data": map[string]any{
|
||||
"id": "vpc-subnets-table",
|
||||
"baseprefix": "/openapi-ui",
|
||||
"clusterNamePartOfUrl": "{2}",
|
||||
"customizationId": "virtualprivatecloud-subnets",
|
||||
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/configmaps",
|
||||
"fieldSelector": map[string]any{
|
||||
"metadata.name": "virtualprivatecloud-{6}-subnets",
|
||||
},
|
||||
"pathToItems": []any{"items"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"key": "details",
|
||||
@@ -222,7 +245,7 @@ func workloadsTab(kind string) map[string]any {
|
||||
"baseprefix": "/openapi-ui",
|
||||
"customizationId": "factory-details-v1alpha1.cozystack.io.workloadmonitors",
|
||||
"pathToItems": []any{"items"},
|
||||
"labelsSelector": map[string]any{
|
||||
"labelSelector": map[string]any{
|
||||
"apps.cozystack.io/application.group": "apps.cozystack.io",
|
||||
"apps.cozystack.io/application.kind": kind,
|
||||
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",
|
||||
@@ -247,7 +270,7 @@ func servicesTab(kind string) map[string]any {
|
||||
"baseprefix": "/openapi-ui",
|
||||
"customizationId": "factory-details-v1.services",
|
||||
"pathToItems": []any{"items"},
|
||||
"labelsSelector": map[string]any{
|
||||
"labelSelector": map[string]any{
|
||||
"apps.cozystack.io/application.group": "apps.cozystack.io",
|
||||
"apps.cozystack.io/application.kind": kind,
|
||||
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",
|
||||
@@ -273,7 +296,7 @@ func ingressesTab(kind string) map[string]any {
|
||||
"baseprefix": "/openapi-ui",
|
||||
"customizationId": "factory-details-networking.k8s.io.v1.ingresses",
|
||||
"pathToItems": []any{"items"},
|
||||
"labelsSelector": map[string]any{
|
||||
"labelSelector": map[string]any{
|
||||
"apps.cozystack.io/application.group": "apps.cozystack.io",
|
||||
"apps.cozystack.io/application.kind": kind,
|
||||
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",
|
||||
@@ -294,12 +317,12 @@ func secretsTab(kind string) map[string]any {
|
||||
"type": "EnrichedTable",
|
||||
"data": map[string]any{
|
||||
"id": "secrets-table",
|
||||
"fetchUrl": "/api/clusters/{2}/k8s/apis/core.cozystack.io/v1alpha1/namespaces/{3}/tenantsecretstables",
|
||||
"fetchUrl": "/api/clusters/{2}/k8s/apis/core.cozystack.io/v1alpha1/namespaces/{3}/tenantsecrets",
|
||||
"clusterNamePartOfUrl": "{2}",
|
||||
"baseprefix": "/openapi-ui",
|
||||
"customizationId": "factory-details-v1alpha1.core.cozystack.io.tenantsecretstables",
|
||||
"customizationId": "factory-details-v1alpha1.core.cozystack.io.tenantsecrets",
|
||||
"pathToItems": []any{"items"},
|
||||
"labelsSelector": map[string]any{
|
||||
"labelSelector": map[string]any{
|
||||
"apps.cozystack.io/application.group": "apps.cozystack.io",
|
||||
"apps.cozystack.io/application.kind": kind,
|
||||
"apps.cozystack.io/application.name": "{reqs[0]['metadata','name']}",
|
||||
@@ -324,6 +347,7 @@ func yamlTab(plural string) map[string]any {
|
||||
"type": "builtin",
|
||||
"typeName": plural,
|
||||
"prefillValuesRequestIndex": float64(0),
|
||||
"readOnly": true,
|
||||
"substractHeight": float64(400),
|
||||
},
|
||||
},
|
||||
@@ -331,6 +355,36 @@ func yamlTab(plural string) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func vncTabPrefix(kind string) (string, bool) {
|
||||
switch kind {
|
||||
case "VirtualMachine":
|
||||
return "virtual-machine", true
|
||||
case "VMInstance":
|
||||
return "vm-instance", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func vncTab(prefix string) map[string]any {
|
||||
return map[string]any{
|
||||
"key": "vnc",
|
||||
"label": "VNC",
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "VMVNC",
|
||||
"data": map[string]any{
|
||||
"id": "vm-vnc",
|
||||
"cluster": "{2}",
|
||||
"namespace": "{reqsJsonPath[0]['.metadata.namespace']['-']}",
|
||||
"substractHeight": float64(400),
|
||||
"vmName": fmt.Sprintf("%s-{reqsJsonPath[0]['.metadata.name']['-']}", prefix),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- OpenAPI → Right column ----------------
|
||||
|
||||
func buildOpenAPIParamsBlocks(schemaJSON string, keysOrder [][]string) []any {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
package dashboard
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -57,97 +54,6 @@ func pickPlural(kind string, crd *cozyv1alpha1.CozystackResourceDefinition) stri
|
||||
return k + "s"
|
||||
}
|
||||
|
||||
// initialsFromKind splits CamelCase and returns the first letters in upper case.
|
||||
// "VirtualMachine" -> "VM"; "Bucket" -> "B".
|
||||
func initialsFromKind(kind string) string {
|
||||
parts := splitCamel(kind)
|
||||
if len(parts) == 0 {
|
||||
return strings.ToUpper(kind)
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
b.WriteString(strings.ToUpper(string(p[0])))
|
||||
// Limit to 3 chars to keep the badge compact (VM, PVC, etc.)
|
||||
if b.Len() >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// hexColorForKind returns a dark, saturated color (hex) derived from a stable hash of the kind.
|
||||
// We map the hash to an HSL hue; fix S/L for consistent readability with white text.
|
||||
func hexColorForKind(kind string) string {
|
||||
// Stable short hash (sha1 → bytes → hue)
|
||||
sum := sha1.Sum([]byte(kind))
|
||||
// Use first two bytes for hue [0..359]
|
||||
hue := int(sum[0])<<8 | int(sum[1])
|
||||
hue = hue % 360
|
||||
|
||||
// Fixed S/L chosen to contrast with white text:
|
||||
// S = 80%, L = 35% (dark enough so #fff is readable)
|
||||
r, g, b := hslToRGB(float64(hue), 0.80, 0.35)
|
||||
|
||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
|
||||
}
|
||||
|
||||
// hslToRGB converts HSL (0..360, 0..1, 0..1) to sRGB (0..255).
|
||||
func hslToRGB(h float64, s float64, l float64) (uint8, uint8, uint8) {
|
||||
c := (1 - absFloat(2*l-1)) * s
|
||||
hp := h / 60.0
|
||||
x := c * (1 - absFloat(modFloat(hp, 2)-1))
|
||||
var r1, g1, b1 float64
|
||||
switch {
|
||||
case 0 <= hp && hp < 1:
|
||||
r1, g1, b1 = c, x, 0
|
||||
case 1 <= hp && hp < 2:
|
||||
r1, g1, b1 = x, c, 0
|
||||
case 2 <= hp && hp < 3:
|
||||
r1, g1, b1 = 0, c, x
|
||||
case 3 <= hp && hp < 4:
|
||||
r1, g1, b1 = 0, x, c
|
||||
case 4 <= hp && hp < 5:
|
||||
r1, g1, b1 = x, 0, c
|
||||
default:
|
||||
r1, g1, b1 = c, 0, x
|
||||
}
|
||||
m := l - c/2
|
||||
r := uint8(clamp01(r1+m) * 255.0)
|
||||
g := uint8(clamp01(g1+m) * 255.0)
|
||||
b := uint8(clamp01(b1+m) * 255.0)
|
||||
return r, g, b
|
||||
}
|
||||
|
||||
func absFloat(v float64) float64 {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func modFloat(a, b float64) float64 {
|
||||
return a - b*float64(int(a/b))
|
||||
}
|
||||
|
||||
func clamp01(v float64) float64 {
|
||||
if v < 0 {
|
||||
return 0
|
||||
}
|
||||
if v > 1 {
|
||||
return 1
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// optional: tiny helper to expose the compact color hash (useful for debugging)
|
||||
func shortHashHex(s string) string {
|
||||
sum := sha1.Sum([]byte(s))
|
||||
return hex.EncodeToString(sum[:4])
|
||||
}
|
||||
|
||||
// ----------------------- Helpers (OpenAPI → values) -----------------------
|
||||
|
||||
// defaultOrZero returns the schema default if present; otherwise a reasonable zero value.
|
||||
@@ -295,12 +201,6 @@ func normalizeJSON(v any) any {
|
||||
}
|
||||
}
|
||||
|
||||
var camelSplitter = regexp.MustCompile(`(?m)([A-Z]+[a-z0-9]*|[a-z0-9]+)`)
|
||||
|
||||
func splitCamel(s string) []string {
|
||||
return camelSplitter.FindAllString(s, -1)
|
||||
}
|
||||
|
||||
// --- helpers for schema inspection ---
|
||||
|
||||
func isScalarType(n map[string]any) bool {
|
||||
|
||||
@@ -10,9 +10,12 @@ import (
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"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/log"
|
||||
managerpkg "sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
@@ -40,28 +43,51 @@ func AddToScheme(s *runtime.Scheme) error {
|
||||
// Manager owns logic for creating/updating dashboard resources derived from CRDs.
|
||||
// It’s easy to extend: add new ensure* methods and wire them into EnsureForCRD.
|
||||
type Manager struct {
|
||||
client client.Client
|
||||
scheme *runtime.Scheme
|
||||
crdListFn func(context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error)
|
||||
}
|
||||
|
||||
// Option pattern so callers can inject a custom lister.
|
||||
type Option func(*Manager)
|
||||
|
||||
// WithCRDListFunc overrides how Manager lists all CozystackResourceDefinitions.
|
||||
func WithCRDListFunc(fn func(context.Context) ([]cozyv1alpha1.CozystackResourceDefinition, error)) Option {
|
||||
return func(m *Manager) { m.crdListFn = fn }
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// NewManager constructs a dashboard Manager.
|
||||
func NewManager(c client.Client, scheme *runtime.Scheme, opts ...Option) *Manager {
|
||||
m := &Manager{client: c, scheme: scheme}
|
||||
for _, o := range opts {
|
||||
o(m)
|
||||
}
|
||||
func NewManager(c client.Client, scheme *runtime.Scheme) *Manager {
|
||||
m := &Manager{Client: c, Scheme: scheme}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Manager) SetupWithManager(mgr ctrl.Manager) error {
|
||||
if err := ctrl.NewControllerManagedBy(mgr).
|
||||
Named("dashboard-reconciler").
|
||||
For(&cozyv1alpha1.CozystackResourceDefinition{}).
|
||||
Complete(m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mgr.Add(managerpkg.RunnableFunc(func(ctx context.Context) error {
|
||||
if !mgr.GetCache().WaitForCacheSync(ctx) {
|
||||
return fmt.Errorf("dashboard static resources cache sync failed")
|
||||
}
|
||||
return m.ensureStaticResources(ctx)
|
||||
}))
|
||||
}
|
||||
|
||||
func (m *Manager) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
l := log.FromContext(ctx)
|
||||
|
||||
crd := &cozyv1alpha1.CozystackResourceDefinition{}
|
||||
|
||||
err := m.Get(ctx, types.NamespacedName{Name: req.Name}, crd)
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
if err := m.CleanupOrphanedResources(ctx); err != nil {
|
||||
l.Error(err, "Failed to cleanup orphaned dashboard resources")
|
||||
}
|
||||
return ctrl.Result{}, nil // no point in requeuing here
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
return m.EnsureForCRD(ctx, crd)
|
||||
}
|
||||
|
||||
// EnsureForCRD is the single entry-point used by the controller.
|
||||
// Add more ensure* calls here as you implement support for other resources:
|
||||
//
|
||||
@@ -171,21 +197,11 @@ 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 {
|
||||
// Get all current CRDs to determine which resources should exist
|
||||
var allCRDs []cozyv1alpha1.CozystackResourceDefinition
|
||||
if m.crdListFn != nil {
|
||||
s, err := m.crdListFn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allCRDs = s
|
||||
} else {
|
||||
var crdList cozyv1alpha1.CozystackResourceDefinitionList
|
||||
if err := m.client.List(ctx, &crdList, &client.ListOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
allCRDs = crdList.Items
|
||||
var crdList cozyv1alpha1.CozystackResourceDefinitionList
|
||||
if err := m.List(ctx, &crdList, &client.ListOptions{}); err != nil {
|
||||
return err
|
||||
}
|
||||
allCRDs := crdList.Items
|
||||
|
||||
// Build a set of expected resource names for each type
|
||||
expectedResources := m.buildExpectedResourceSet(allCRDs)
|
||||
@@ -349,7 +365,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O
|
||||
}
|
||||
|
||||
// List with dashboard labels
|
||||
if err := m.client.List(ctx, list, m.getDashboardResourceSelector()); err != nil {
|
||||
if err := m.List(ctx, list, m.getDashboardResourceSelector()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -358,7 +374,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O
|
||||
case *dashv1alpha1.CustomColumnsOverrideList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if err := m.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
@@ -369,7 +385,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O
|
||||
case *dashv1alpha1.CustomFormsOverrideList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if err := m.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
@@ -380,7 +396,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O
|
||||
case *dashv1alpha1.CustomFormsPrefillList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if err := m.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
@@ -391,7 +407,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O
|
||||
case *dashv1alpha1.MarketplacePanelList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if err := m.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
@@ -402,7 +418,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O
|
||||
case *dashv1alpha1.SidebarList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if err := m.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
@@ -413,7 +429,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O
|
||||
case *dashv1alpha1.TableUriMappingList:
|
||||
for _, item := range l.Items {
|
||||
if !expected[item.Name] {
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if err := m.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
@@ -426,7 +442,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O
|
||||
if !expected[item.Name] {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.Info("Deleting orphaned Breadcrumb resource", "name", item.Name)
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if err := m.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
@@ -438,7 +454,7 @@ func (m *Manager) cleanupResourceType(ctx context.Context, resourceType client.O
|
||||
if !expected[item.Name] {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.Info("Deleting orphaned Factory resource", "name", item.Name)
|
||||
if err := m.client.Delete(ctx, &item); err != nil {
|
||||
if err := m.Delete(ctx, &item); err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user