mirror of
https://github.com/cozystack/cozystack.git
synced 2026-03-06 23:18:53 +00:00
Compare commits
22 Commits
cozystack-
...
ci-changel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
976b0011ac | ||
|
|
656e00d182 | ||
|
|
202410a438 | ||
|
|
f7877b9bce | ||
|
|
ab6c6bad16 | ||
|
|
09e0b5f4ec | ||
|
|
e31ee932c0 | ||
|
|
517369629f | ||
|
|
00cef35214 | ||
|
|
e213b068e8 | ||
|
|
98488100e4 | ||
|
|
0ed52f670d | ||
|
|
b6900a258a | ||
|
|
c919ea6e39 | ||
|
|
7fa875db52 | ||
|
|
cf15ad1073 | ||
|
|
ba50d01877 | ||
|
|
7e7716aa44 | ||
|
|
669bf3d2f5 | ||
|
|
8d43f993e4 | ||
|
|
97e8d2aa49 | ||
|
|
3975da93c6 |
163
.github/workflows/tags.yaml
vendored
163
.github/workflows/tags.yaml
vendored
@@ -239,3 +239,166 @@ jobs:
|
||||
} else {
|
||||
console.log(`PR already exists from ${head} to ${base}`);
|
||||
}
|
||||
|
||||
generate-changelog:
|
||||
name: Generate Changelog
|
||||
runs-on: [self-hosted]
|
||||
needs: [prepare-release]
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
if: needs.prepare-release.result == 'success'
|
||||
steps:
|
||||
- name: Parse tag and get base branch
|
||||
id: tag
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const ref = context.ref.replace('refs/tags/', '');
|
||||
const m = ref.match(/^v(\d+\.\d+\.\d+)(-(?:alpha|beta|rc)\.\d+)?$/);
|
||||
if (!m) {
|
||||
core.setFailed(`❌ tag '${ref}' must match 'vX.Y.Z' or 'vX.Y.Z-(alpha|beta|rc).N'`);
|
||||
return;
|
||||
}
|
||||
const version = m[1] + (m[2] ?? '');
|
||||
const [maj, min] = m[1].split('.');
|
||||
|
||||
// Determine base branch: if patch release (Z > 0), use release-X.Y, else use main
|
||||
const patch = parseInt(m[1].split('.')[2]);
|
||||
const baseBranch = patch > 0 ? `release-${maj}.${min}` : 'main';
|
||||
|
||||
core.setOutput('version', version);
|
||||
core.setOutput('tag', ref);
|
||||
core.setOutput('base_branch', baseBranch);
|
||||
|
||||
- name: Checkout main branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
token: ${{ secrets.GH_PAT }}
|
||||
|
||||
- name: Check if changelog already exists
|
||||
id: check_changelog
|
||||
run: |
|
||||
CHANGELOG_FILE="docs/changelogs/v${{ steps.tag.outputs.version }}.md"
|
||||
if [ -f "$CHANGELOG_FILE" ]; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Changelog file $CHANGELOG_FILE already exists"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Changelog file $CHANGELOG_FILE does not exist"
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check_changelog.outputs.exists == 'false'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install GitHub Copilot CLI
|
||||
if: steps.check_changelog.outputs.exists == 'false'
|
||||
run: npm i -g @github/copilot
|
||||
|
||||
- name: Generate changelog using AI
|
||||
if: steps.check_changelog.outputs.exists == 'false'
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
copilot --prompt "prepare changelog file for tagged release v${{ steps.tag.outputs.version }}, use @docs/agents/changelog.md for it. Create the changelog file at docs/changelogs/v${{ steps.tag.outputs.version }}.md" \
|
||||
--allow-all-tools --allow-all-paths < /dev/null
|
||||
|
||||
- name: Create changelog branch and commit
|
||||
if: steps.check_changelog.outputs.exists == 'false'
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
git config user.name "cozystack-bot"
|
||||
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
|
||||
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
|
||||
|
||||
CHANGELOG_FILE="docs/changelogs/v${{ steps.tag.outputs.version }}.md"
|
||||
CHANGELOG_BRANCH="changelog-v${{ steps.tag.outputs.version }}"
|
||||
|
||||
if [ -f "$CHANGELOG_FILE" ]; then
|
||||
# Fetch latest main branch
|
||||
git fetch origin main
|
||||
|
||||
# Delete local branch if it exists
|
||||
git branch -D "$CHANGELOG_BRANCH" 2>/dev/null || true
|
||||
|
||||
# Create and checkout new branch from main
|
||||
git checkout -b "$CHANGELOG_BRANCH" origin/main
|
||||
|
||||
# Add and commit changelog
|
||||
git add "$CHANGELOG_FILE"
|
||||
if git diff --staged --quiet; then
|
||||
echo "⚠️ No changes to commit (file may already be committed)"
|
||||
else
|
||||
git commit -m "docs: add changelog for v${{ steps.tag.outputs.version }}" -s
|
||||
echo "✅ Changelog committed to branch $CHANGELOG_BRANCH"
|
||||
fi
|
||||
|
||||
# Push the branch (force push to update if it exists)
|
||||
git push -f origin "$CHANGELOG_BRANCH"
|
||||
else
|
||||
echo "⚠️ Changelog file was not generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create PR for changelog
|
||||
if: steps.check_changelog.outputs.exists == 'false'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GH_PAT }}
|
||||
script: |
|
||||
const version = '${{ steps.tag.outputs.version }}';
|
||||
const changelogBranch = `changelog-v${version}`;
|
||||
const baseBranch = 'main';
|
||||
|
||||
// Check if PR already exists
|
||||
const prs = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: `${context.repo.owner}:${changelogBranch}`,
|
||||
base: baseBranch,
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
if (prs.data.length > 0) {
|
||||
const pr = prs.data[0];
|
||||
console.log(`PR #${pr.number} already exists for changelog branch ${changelogBranch}`);
|
||||
|
||||
// Update PR body with latest info
|
||||
const body = `This PR adds the changelog for release \`v${version}\`.\n\n✅ Changelog has been automatically generated in \`docs/changelogs/v${version}.md\`.`;
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr.number,
|
||||
body: body
|
||||
});
|
||||
console.log(`Updated existing PR #${pr.number}`);
|
||||
} else {
|
||||
// Create new PR
|
||||
const pr = await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: changelogBranch,
|
||||
base: baseBranch,
|
||||
title: `docs: add changelog for v${version}`,
|
||||
body: `This PR adds the changelog for release \`v${version}\`.\n\n✅ Changelog has been automatically generated in \`docs/changelogs/v${version}.md\`.`,
|
||||
draft: false
|
||||
});
|
||||
|
||||
// Add label if needed
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.data.number,
|
||||
labels: ['documentation', 'automated']
|
||||
});
|
||||
|
||||
console.log(`Created PR #${pr.data.number} for changelog`);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ type PackageSpec struct {
|
||||
Components map[string]PackageComponent `json:"components,omitempty"`
|
||||
}
|
||||
|
||||
// PackageRelease defines overrides for a specific component
|
||||
// PackageComponent defines overrides for a specific component
|
||||
type PackageComponent struct {
|
||||
// Enabled indicates whether this component should be installed
|
||||
// If false, the component will be disabled even if it's defined in the PackageSource
|
||||
|
||||
@@ -81,19 +81,6 @@ type Variant struct {
|
||||
Components []Component `json:"components,omitempty"`
|
||||
}
|
||||
|
||||
// DependencyTarget defines a named group of packages that can be referenced
|
||||
// by other package sources via dependsOn
|
||||
type DependencyTarget struct {
|
||||
// Name is the unique identifier for this dependency target
|
||||
// +required
|
||||
Name string `json:"name"`
|
||||
|
||||
// Packages is a list of package names that belong to this target
|
||||
// These packages will be added as dependencies when this target is referenced
|
||||
// +required
|
||||
Packages []string `json:"packages"`
|
||||
}
|
||||
|
||||
// Library defines a Helm library chart
|
||||
type Library struct {
|
||||
// Name is the optional name for library placed in charts
|
||||
|
||||
@@ -308,26 +308,6 @@ 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 *DependencyTarget) DeepCopyInto(out *DependencyTarget) {
|
||||
*out = *in
|
||||
if in.Packages != nil {
|
||||
in, out := &in.Packages, &out.Packages
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DependencyTarget.
|
||||
func (in *DependencyTarget) DeepCopy() *DependencyTarget {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(DependencyTarget)
|
||||
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
|
||||
|
||||
@@ -1,565 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
var addCmdFlags struct {
|
||||
files []string
|
||||
kubeconfig string
|
||||
}
|
||||
|
||||
var addCmd = &cobra.Command{
|
||||
Use: "add [package]...",
|
||||
Short: "Install PackageSource and its dependencies interactively",
|
||||
Long: `Install PackageSource and its dependencies interactively.
|
||||
|
||||
You can specify packages as arguments or use -f flag to read from files.
|
||||
Multiple -f flags can be specified, and they can point to files or directories.`,
|
||||
Args: cobra.ArbitraryArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Collect package names from arguments and files
|
||||
packageNames := make(map[string]bool)
|
||||
packagesFromFiles := make(map[string]string) // packageName -> filePath
|
||||
|
||||
for _, arg := range args {
|
||||
packageNames[arg] = true
|
||||
}
|
||||
|
||||
// Read packages from files
|
||||
for _, filePath := range addCmdFlags.files {
|
||||
packages, err := readPackagesFromFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read packages from %s: %w", filePath, err)
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
packageNames[pkg] = true
|
||||
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
|
||||
if obj.GetKind() == "PackageList" {
|
||||
items, found, err := unstructured.NestedSlice(obj.Object, "items")
|
||||
if err == nil && found {
|
||||
for _, item := range items {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
if metadata, ok := itemMap["metadata"].(map[string]interface{}); ok {
|
||||
if name, ok := metadata["name"].(string); ok && name != "" {
|
||||
packages = append(packages, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to parse as PackageSourceList
|
||||
if obj.GetKind() == "PackageSourceList" {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if len(packages) == 0 {
|
||||
return nil, fmt.Errorf("no valid packages found in file")
|
||||
}
|
||||
|
||||
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)")
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"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 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)
|
||||
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 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)
|
||||
}
|
||||
|
||||
// Delete each package
|
||||
for packageName := range packageNames {
|
||||
pkg := &cozyv1alpha1.Package{}
|
||||
pkg.Name = packageName
|
||||
if err := k8sClient.Delete(ctx, pkg); err != nil {
|
||||
if client.IgnoreNotFound(err) == nil {
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
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)")
|
||||
}
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/emicklei/dot"
|
||||
"github.com/spf13/cobra"
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
var dotCmdFlags struct {
|
||||
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, err := buildGraphFromCluster(ctx, dotCmdFlags.kubeconfig, packagesOnly, dotCmdFlags.installed, packageName, selectedPackages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting PackageSource dependencies: %w", err)
|
||||
}
|
||||
|
||||
dotGraph := generateDOTGraph(graph, allNodes, packagesOnly)
|
||||
dotGraph.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", true, "show component-level dependencies (default: true)")
|
||||
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.
|
||||
func buildGraphFromCluster(ctx context.Context, kubeconfig string, packagesOnly bool, installedOnly bool, packageName string, selectedPackages []string) (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, fmt.Errorf("failed to load kubeconfig from %s: %w", kubeconfig, err)
|
||||
}
|
||||
} else {
|
||||
// Use default kubeconfig loading (from env var or ~/.kube/config)
|
||||
config, err = ctrl.GetConfig()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get kubeconfig: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
k8sClient, err := client.New(config, client.Options{Scheme: dependenciesScheme})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create k8s client: %w", err)
|
||||
}
|
||||
|
||||
// Get installed Packages if needed
|
||||
installedPackages := make(map[string]bool)
|
||||
if installedOnly || packageName != "" {
|
||||
var packageList cozyv1alpha1.PackageList
|
||||
if err := k8sClient.List(ctx, &packageList); err != nil {
|
||||
return 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, fmt.Errorf("failed to list PackageSources: %w", err)
|
||||
}
|
||||
|
||||
graph := make(map[string][]string)
|
||||
allNodes := make(map[string]bool)
|
||||
|
||||
// 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
|
||||
|
||||
// Extract dependencies from variants
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
// Variant-level dependencies
|
||||
for _, dep := range variant.DependsOn {
|
||||
// If installedOnly is set, only include dependencies that are installed
|
||||
if installedOnly && !installedPackages[dep] {
|
||||
continue
|
||||
}
|
||||
graph[psName] = append(graph[psName], dep)
|
||||
allNodes[dep] = true
|
||||
}
|
||||
|
||||
// Component-level dependencies
|
||||
if !packagesOnly {
|
||||
for _, component := range variant.Components {
|
||||
componentName := fmt.Sprintf("%s.%s", psName, component.Name)
|
||||
allNodes[componentName] = true
|
||||
|
||||
if component.Install != nil {
|
||||
for _, dep := range component.Install.DependsOn {
|
||||
// Check if it's a local component dependency or external
|
||||
if strings.Contains(dep, ".") {
|
||||
graph[componentName] = append(graph[componentName], dep)
|
||||
allNodes[dep] = true
|
||||
} else {
|
||||
// Local component dependency
|
||||
localDep := fmt.Sprintf("%s.%s", psName, dep)
|
||||
graph[componentName] = append(graph[componentName], localDep)
|
||||
allNodes[localDep] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return graph, allNodes, nil
|
||||
}
|
||||
|
||||
// generateDOTGraph generates a DOT graph from the dependency graph.
|
||||
func generateDOTGraph(graph map[string][]string, allNodes map[string]bool, packagesOnly bool) *dot.Graph {
|
||||
g := dot.NewGraph(dot.Directed)
|
||||
g.Attr("rankdir", "LR")
|
||||
g.Attr("nodesep", "0.5")
|
||||
g.Attr("ranksep", "1.0")
|
||||
|
||||
// Add nodes
|
||||
for node := range allNodes {
|
||||
if packagesOnly && strings.Contains(node, ".") && !strings.HasPrefix(node, "cozystack.") {
|
||||
// Skip component nodes when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
|
||||
n := g.Node(node)
|
||||
|
||||
// Style nodes based on type
|
||||
if strings.Contains(node, ".") && !strings.HasPrefix(node, "cozystack.") {
|
||||
// Component node
|
||||
n.Attr("shape", "box")
|
||||
n.Attr("style", "rounded,filled")
|
||||
n.Attr("fillcolor", "lightyellow")
|
||||
n.Attr("label", strings.Split(node, ".")[len(strings.Split(node, "."))-1])
|
||||
} else {
|
||||
// Package node
|
||||
n.Attr("shape", "box")
|
||||
n.Attr("style", "rounded,filled")
|
||||
n.Attr("fillcolor", "lightblue")
|
||||
n.Attr("label", node)
|
||||
}
|
||||
}
|
||||
|
||||
// Add edges
|
||||
for source, targets := range graph {
|
||||
if packagesOnly && strings.Contains(source, ".") && !strings.HasPrefix(source, "cozystack.") {
|
||||
// Skip component edges when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
|
||||
for _, target := range targets {
|
||||
if packagesOnly && strings.Contains(target, ".") && !strings.HasPrefix(target, "cozystack.") {
|
||||
// Skip component edges when packages-only is enabled
|
||||
continue
|
||||
}
|
||||
|
||||
// Only add edge if both nodes exist
|
||||
if allNodes[source] && allNodes[target] {
|
||||
g.Edge(g.Node(source), g.Node(target))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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)
|
||||
}
|
||||
|
||||
// Print header
|
||||
fmt.Fprintf(os.Stdout, "%-50s %-30s %-10s %s\n", "NAME", "VARIANTS", "READY", "STATUS")
|
||||
fmt.Fprintf(os.Stdout, "%-50s %-30s %-10s %s\n", strings.Repeat("-", 50), strings.Repeat("-", 30), strings.Repeat("-", 10), strings.Repeat("-", 50))
|
||||
|
||||
// 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(os.Stdout, "%-50s %-30s %-10s %s\n", ps.Name, variantsStr, ready, status)
|
||||
|
||||
// Show components if requested
|
||||
if showComponents {
|
||||
for _, variant := range ps.Spec.Variants {
|
||||
for _, component := range variant.Components {
|
||||
fmt.Fprintf(os.Stdout, " %-48s %-30s %-10s %s\n",
|
||||
fmt.Sprintf("%s.%s", ps.Name, component.Name),
|
||||
variant.Name, "", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Print header
|
||||
fmt.Fprintf(os.Stdout, "%-50s %-20s %-10s %s\n", "NAME", "VARIANT", "READY", "STATUS")
|
||||
fmt.Fprintf(os.Stdout, "%-50s %-20s %-10s %s\n", strings.Repeat("-", 50), strings.Repeat("-", 20), strings.Repeat("-", 10), strings.Repeat("-", 50))
|
||||
|
||||
// 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(os.Stdout, "%-50s %-20s %-10s %s\n", pkg.Name, variant, ready, status)
|
||||
|
||||
// Show components if requested
|
||||
if showComponents {
|
||||
// Get PackageSource to show components
|
||||
ps := &cozyv1alpha1.PackageSource{}
|
||||
if err := k8sClient.Get(ctx, client.ObjectKey{Name: pkg.Name}, ps); err == nil {
|
||||
// Find the variant
|
||||
for _, v := range ps.Spec.Variants {
|
||||
if v.Name == variant {
|
||||
for _, component := range v.Components {
|
||||
fmt.Fprintf(os.Stdout, " %-48s %-20s %-10s %s\n",
|
||||
fmt.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)")
|
||||
}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/cozystack/cozystack/cmd/cozypkg/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,14 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controller.CozystackResourceDefinitionHelmReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "CozystackResourceDefinitionHelmReconciler")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dashboardManager := &dashboard.Manager{
|
||||
Client: mgr.GetClient(),
|
||||
Scheme: mgr.GetScheme(),
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook"
|
||||
|
||||
"github.com/cozystack/cozystack/internal/fluxinstall"
|
||||
"github.com/cozystack/cozystack/internal/operator"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
setupLog = ctrl.Log.WithName("setup")
|
||||
)
|
||||
|
||||
// stringSliceFlag is a custom flag type that allows multiple values
|
||||
type stringSliceFlag []string
|
||||
|
||||
func (f *stringSliceFlag) String() string {
|
||||
return strings.Join(*f, ",")
|
||||
}
|
||||
|
||||
func (f *stringSliceFlag) Set(value string) error {
|
||||
*f = append(*f, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
|
||||
utilruntime.Must(cozyv1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(helmv2.AddToScheme(scheme))
|
||||
utilruntime.Must(sourcev1.AddToScheme(scheme))
|
||||
utilruntime.Must(sourcewatcherv1beta1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
func main() {
|
||||
var metricsAddr string
|
||||
var enableLeaderElection bool
|
||||
var probeAddr string
|
||||
var secureMetrics bool
|
||||
var enableHTTP2 bool
|
||||
var installFlux bool
|
||||
var cozystackVersion string
|
||||
var installFluxResources stringSliceFlag
|
||||
var platformSource string
|
||||
var platformSourceName 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.Var(&installFluxResources, "install-flux-resource", "Install Flux resource (JSON format). Can be specified multiple times. Applied after Flux installation.")
|
||||
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
|
||||
"Version of Cozystack")
|
||||
flag.StringVar(&platformSource, "platform-source", "", "Platform source URL (oci:// or git://). If specified, generates OCIRepository or GitRepository resource.")
|
||||
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
|
||||
|
||||
opts := zap.Options{
|
||||
Development: true,
|
||||
}
|
||||
opts.BindFlags(flag.CommandLine)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
config := ctrl.GetConfigOrDie()
|
||||
|
||||
// Start the controller manager
|
||||
setupLog.Info("Starting controller manager")
|
||||
mgr, err := ctrl.NewManager(config, ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsserver.Options{
|
||||
BindAddress: metricsAddr,
|
||||
SecureServing: secureMetrics,
|
||||
},
|
||||
WebhookServer: webhook.NewServer(webhook.Options{
|
||||
Port: 9443,
|
||||
}),
|
||||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "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()
|
||||
|
||||
// The namespace will be automatically extracted from the embedded manifests
|
||||
if err := fluxinstall.Install(installCtx, mgr.GetClient(), fluxinstall.WriteEmbeddedManifests); err != nil {
|
||||
setupLog.Error(err, "failed to install Flux, continuing anyway")
|
||||
// Don't exit - allow operator to start even if Flux install fails
|
||||
// This allows the operator to work in environments where Flux is already installed
|
||||
} else {
|
||||
setupLog.Info("Flux installation completed successfully")
|
||||
}
|
||||
}
|
||||
|
||||
// Install Flux resources after Flux installation
|
||||
if len(installFluxResources) > 0 {
|
||||
setupLog.Info("Installing Flux resources", "count", len(installFluxResources))
|
||||
installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer installCancel()
|
||||
|
||||
if err := installFluxResourcesFunc(installCtx, mgr.GetClient(), installFluxResources); err != nil {
|
||||
setupLog.Error(err, "failed to install Flux resources, continuing anyway")
|
||||
// Don't exit - allow operator to start even if resource installation fails
|
||||
} else {
|
||||
setupLog.Info("Flux resources installation completed successfully")
|
||||
}
|
||||
}
|
||||
|
||||
// Generate and install platform source resource if specified
|
||||
if platformSource != "" {
|
||||
setupLog.Info("Generating platform source resource", "source", platformSource, "name", platformSourceName)
|
||||
installCtx, installCancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer installCancel()
|
||||
|
||||
if err := installPlatformSourceResource(installCtx, mgr.GetClient(), platformSource, platformSourceName); 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)
|
||||
}
|
||||
|
||||
// +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)
|
||||
}
|
||||
}
|
||||
|
||||
// installFluxResourcesFunc installs Flux resources from JSON strings
|
||||
func installFluxResourcesFunc(ctx context.Context, k8sClient client.Client, resources []string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
for i, resourceJSON := range resources {
|
||||
logger.Info("Installing Flux resource", "index", i+1, "total", len(resources))
|
||||
|
||||
// Parse JSON into unstructured object
|
||||
var obj unstructured.Unstructured
|
||||
if err := json.Unmarshal([]byte(resourceJSON), &obj.Object); err != nil {
|
||||
return fmt.Errorf("failed to parse resource JSON at index %d: %w", i, err)
|
||||
}
|
||||
|
||||
// Validate that it has required fields
|
||||
if obj.GetAPIVersion() == "" {
|
||||
return fmt.Errorf("resource at index %d missing apiVersion", i)
|
||||
}
|
||||
if obj.GetKind() == "" {
|
||||
return fmt.Errorf("resource at index %d missing kind", i)
|
||||
}
|
||||
if obj.GetName() == "" {
|
||||
return fmt.Errorf("resource at index %d missing metadata.name", i)
|
||||
}
|
||||
|
||||
// Apply the resource (create or update)
|
||||
logger.Info("Applying Flux resource",
|
||||
"apiVersion", obj.GetAPIVersion(),
|
||||
"kind", obj.GetKind(),
|
||||
"name", obj.GetName(),
|
||||
"namespace", obj.GetNamespace(),
|
||||
)
|
||||
|
||||
// Use server-side apply or create/update
|
||||
existing := &unstructured.Unstructured{}
|
||||
existing.SetGroupVersionKind(obj.GroupVersionKind())
|
||||
key := client.ObjectKey{
|
||||
Name: obj.GetName(),
|
||||
Namespace: obj.GetNamespace(),
|
||||
}
|
||||
|
||||
err := k8sClient.Get(ctx, key, existing)
|
||||
if err != nil {
|
||||
if client.IgnoreNotFound(err) == nil {
|
||||
// Resource doesn't exist, create it
|
||||
if err := k8sClient.Create(ctx, &obj); err != nil {
|
||||
return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
|
||||
}
|
||||
logger.Info("Created Flux resource", "kind", obj.GetKind(), "name", obj.GetName())
|
||||
} else {
|
||||
return fmt.Errorf("failed to check if resource exists: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Resource exists, update it
|
||||
obj.SetResourceVersion(existing.GetResourceVersion())
|
||||
if err := k8sClient.Update(ctx, &obj); err != nil {
|
||||
return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
|
||||
}
|
||||
logger.Info("Updated Flux resource", "kind", obj.GetKind(), "name", obj.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// installPlatformSourceResource generates and installs a Flux source resource (OCIRepository or GitRepository)
|
||||
// based on the platform source URL
|
||||
func installPlatformSourceResource(ctx context.Context, k8sClient client.Client, sourceURL, resourceName string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Parse the source URL to determine type
|
||||
sourceType, repoURL, ref, err := parsePlatformSource(sourceURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse platform source URL: %w", err)
|
||||
}
|
||||
|
||||
var obj *unstructured.Unstructured
|
||||
switch sourceType {
|
||||
case "oci":
|
||||
obj, err = generateOCIRepository(resourceName, repoURL, ref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate OCIRepository: %w", err)
|
||||
}
|
||||
case "git":
|
||||
obj, err = generateGitRepository(resourceName, repoURL, ref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to generate GitRepository: %w", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported source type: %s (expected oci:// or git://)", sourceType)
|
||||
}
|
||||
|
||||
// Apply the resource (create or update)
|
||||
logger.Info("Applying platform source resource",
|
||||
"apiVersion", obj.GetAPIVersion(),
|
||||
"kind", obj.GetKind(),
|
||||
"name", obj.GetName(),
|
||||
"namespace", obj.GetNamespace(),
|
||||
)
|
||||
|
||||
existing := &unstructured.Unstructured{}
|
||||
existing.SetGroupVersionKind(obj.GroupVersionKind())
|
||||
key := client.ObjectKey{
|
||||
Name: obj.GetName(),
|
||||
Namespace: obj.GetNamespace(),
|
||||
}
|
||||
|
||||
err = k8sClient.Get(ctx, key, existing)
|
||||
if err != nil {
|
||||
if client.IgnoreNotFound(err) == nil {
|
||||
// Resource doesn't exist, create it
|
||||
if err := k8sClient.Create(ctx, obj); err != nil {
|
||||
return fmt.Errorf("failed to create resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
|
||||
}
|
||||
logger.Info("Created platform source resource", "kind", obj.GetKind(), "name", obj.GetName())
|
||||
} else {
|
||||
return fmt.Errorf("failed to check if resource exists: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Resource exists, update it
|
||||
obj.SetResourceVersion(existing.GetResourceVersion())
|
||||
if err := k8sClient.Update(ctx, obj); err != nil {
|
||||
return fmt.Errorf("failed to update resource %s/%s: %w", obj.GetKind(), obj.GetName(), err)
|
||||
}
|
||||
logger.Info("Updated platform source resource", "kind", obj.GetKind(), "name", obj.GetName())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsePlatformSource parses the source URL and returns the type, repository URL, and reference
|
||||
// Supports formats:
|
||||
// - oci://registry.example.com/repo@sha256:digest
|
||||
// - oci://registry.example.com/repo (ref will be empty)
|
||||
// - git://github.com/user/repo@branch
|
||||
// - git://github.com/user/repo (ref will default to "main")
|
||||
// - https://github.com/user/repo@branch (treated as git)
|
||||
func parsePlatformSource(sourceURL string) (sourceType, repoURL, ref string, err error) {
|
||||
// Normalize the URL by trimming whitespace
|
||||
sourceURL = strings.TrimSpace(sourceURL)
|
||||
|
||||
// Check for oci:// prefix
|
||||
if strings.HasPrefix(sourceURL, "oci://") {
|
||||
// Remove oci:// prefix
|
||||
rest := strings.TrimPrefix(sourceURL, "oci://")
|
||||
|
||||
// Check for @sha256: digest (look for @ followed by sha256:)
|
||||
// We need to find the last @ before sha256: to handle paths with @ symbols
|
||||
sha256Idx := strings.Index(rest, "@sha256:")
|
||||
if sha256Idx != -1 {
|
||||
repoURL = "oci://" + rest[:sha256Idx]
|
||||
ref = rest[sha256Idx+1:] // sha256:digest
|
||||
} else {
|
||||
// Check for @ without sha256: (might be a tag)
|
||||
if atIdx := strings.LastIndex(rest, "@"); atIdx != -1 {
|
||||
// Could be a tag, but for OCI we expect sha256: digest
|
||||
// For now, treat everything after @ as the ref
|
||||
repoURL = "oci://" + rest[:atIdx]
|
||||
ref = rest[atIdx+1:]
|
||||
} else {
|
||||
repoURL = "oci://" + rest
|
||||
ref = "" // No digest specified
|
||||
}
|
||||
}
|
||||
return "oci", repoURL, ref, nil
|
||||
}
|
||||
|
||||
// Check for git:// prefix or treat as git for http/https
|
||||
if strings.HasPrefix(sourceURL, "git://") || strings.HasPrefix(sourceURL, "http://") || strings.HasPrefix(sourceURL, "https://") || strings.HasPrefix(sourceURL, "ssh://") {
|
||||
// Parse URL to extract ref if present
|
||||
parsedURL, err := url.Parse(sourceURL)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("invalid URL: %w", err)
|
||||
}
|
||||
|
||||
// Check for @ref in the path (e.g., git://host/path@branch)
|
||||
path := parsedURL.Path
|
||||
if idx := strings.LastIndex(path, "@"); idx != -1 {
|
||||
repoURL = fmt.Sprintf("%s://%s%s", parsedURL.Scheme, parsedURL.Host, path[:idx])
|
||||
if parsedURL.RawQuery != "" {
|
||||
repoURL += "?" + parsedURL.RawQuery
|
||||
}
|
||||
ref = path[idx+1:]
|
||||
} else {
|
||||
// Default to main branch if no ref specified
|
||||
repoURL = sourceURL
|
||||
ref = "main"
|
||||
}
|
||||
|
||||
// Normalize git:// to https:// for GitRepository
|
||||
if strings.HasPrefix(repoURL, "git://") {
|
||||
repoURL = strings.Replace(repoURL, "git://", "https://", 1)
|
||||
}
|
||||
|
||||
return "git", repoURL, ref, nil
|
||||
}
|
||||
|
||||
return "", "", "", fmt.Errorf("unsupported source URL scheme (expected oci:// or git://): %s", sourceURL)
|
||||
}
|
||||
|
||||
// generateOCIRepository creates an OCIRepository resource
|
||||
func generateOCIRepository(name, repoURL, digest string) (*unstructured.Unstructured, error) {
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetAPIVersion("source.toolkit.fluxcd.io/v1")
|
||||
obj.SetKind("OCIRepository")
|
||||
obj.SetName(name)
|
||||
obj.SetNamespace("cozy-system")
|
||||
|
||||
spec := map[string]interface{}{
|
||||
"interval": "5m0s",
|
||||
"url": repoURL,
|
||||
}
|
||||
|
||||
if digest != "" {
|
||||
// Ensure digest starts with sha256:
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
digest = "sha256:" + digest
|
||||
}
|
||||
spec["ref"] = map[string]interface{}{
|
||||
"digest": digest,
|
||||
}
|
||||
}
|
||||
|
||||
if err := unstructured.SetNestedField(obj.Object, spec, "spec"); err != nil {
|
||||
return nil, fmt.Errorf("failed to set spec: %w", err)
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// generateGitRepository creates a GitRepository resource
|
||||
func generateGitRepository(name, repoURL, ref string) (*unstructured.Unstructured, error) {
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetAPIVersion("source.toolkit.fluxcd.io/v1")
|
||||
obj.SetKind("GitRepository")
|
||||
obj.SetName(name)
|
||||
obj.SetNamespace("cozy-system")
|
||||
|
||||
spec := map[string]interface{}{
|
||||
"interval": "5m0s",
|
||||
"url": repoURL,
|
||||
"ref": map[string]interface{}{
|
||||
"branch": ref,
|
||||
},
|
||||
}
|
||||
|
||||
if err := unstructured.SetNestedField(obj.Object, spec, "spec"); err != nil {
|
||||
return nil, fmt.Errorf("failed to set spec: %w", err)
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
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)
|
||||
|
||||
73
docs/changelogs/v0.39.0.md
Normal file
73
docs/changelogs/v0.39.0.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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, #1745).
|
||||
* **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, #1744).
|
||||
* **SeaweedFS Updates**: Updated to SeaweedFS v4.02 with improved S3 daemon performance and fixes ([**@kvaps**](https://github.com/kvaps) in #1725, #1732).
|
||||
|
||||
---
|
||||
|
||||
## 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, #1745).
|
||||
|
||||
### 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, #1744).
|
||||
|
||||
### 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, #1732).
|
||||
|
||||
## 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).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[apps] Refactor apiserver to use typed objects and fix UnstructuredList GVK**: Refactored the apiserver REST handlers to use typed objects (`appsv1alpha1.Application`) instead of `unstructured.Unstructured`, eliminating the need for runtime conversions and simplifying the codebase. Additionally, fixed an issue where `UnstructuredList` objects were using the first registered kind from `typeToGVK` instead of the kind from the object's field when multiple kinds are registered with the same Go type. This fix includes the upstream fix from kubernetes/kubernetes#135537 ([**@kvaps**](https://github.com/kvaps) in #1679, #1709).
|
||||
* **[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, #1700).
|
||||
* **[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, #1701).
|
||||
* **[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, #1697).
|
||||
* **[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).
|
||||
|
||||
## Dependencies
|
||||
|
||||
* **Update SeaweedFS v4.02**: Updated SeaweedFS to version 4.02 ([**@kvaps**](https://github.com/kvaps) in #1725, #1732).
|
||||
* **[linstor] Update piraeus-operator v2.10.2**: Updated piraeus-operator to version 2.10.2 ([**@kvaps**](https://github.com/kvaps) in #1689, #1697).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[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)
|
||||
|
||||
6
go.mod
6
go.mod
@@ -5,10 +5,7 @@ module github.com/cozystack/cozystack
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/emicklei/dot v1.10.0
|
||||
github.com/fluxcd/helm-controller/api v1.4.3
|
||||
github.com/fluxcd/source-controller/api v1.6.2
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.2
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/google/gofuzz v1.2.0
|
||||
@@ -19,7 +16,6 @@ require (
|
||||
github.com/spf13/cobra v1.9.1
|
||||
go.uber.org/zap v1.27.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.34.1
|
||||
k8s.io/apiextensions-apiserver v0.34.1
|
||||
k8s.io/apimachinery v0.34.1
|
||||
@@ -48,7 +44,6 @@ require (
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.13.0 // indirect
|
||||
github.com/fluxcd/pkg/apis/meta v1.22.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
@@ -121,6 +116,7 @@ require (
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/kms v0.34.1 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -27,8 +27,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
|
||||
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/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=
|
||||
@@ -39,16 +37,10 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fluxcd/helm-controller/api v1.4.3 h1:CdZwjL1liXmYCWyk2jscmFEB59tICIlnWB9PfDDW5q4=
|
||||
github.com/fluxcd/helm-controller/api v1.4.3/go.mod h1:0XrBhKEaqvxyDj/FziG1Q8Fmx2UATdaqLgYqmZh6wW4=
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0 h1:wBpgsKT+jcyZEcM//OmZr9RiF8klL3ebrDp2u2ThsnA=
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0/go.mod h1:TttNS+gocsGLwnvmgVi3/Yscwqrjc17+vhgYfqkfrV4=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.13.0 h1:GGf0UBVRIku+gebY944icVeEIhyg1P/KE3IrhOyJJnE=
|
||||
github.com/fluxcd/pkg/apis/kustomize v1.13.0/go.mod h1:TLKVqbtnzkhDuhWnAsN35977HvRfIjs+lgMuNro/LEc=
|
||||
github.com/fluxcd/pkg/apis/meta v1.22.0 h1:EHWQH5ZWml7i8eZ/AMjm1jxid3j/PQ31p+hIwCt6crM=
|
||||
github.com/fluxcd/pkg/apis/meta v1.22.0/go.mod h1:Kc1+bWe5p0doROzuV9XiTfV/oL3ddsemYXt8ZYWdVVg=
|
||||
github.com/fluxcd/source-controller/api v1.6.2 h1:UmodAeqLIeF29HdTqf2GiacZyO+hJydJlepDaYsMvhc=
|
||||
github.com/fluxcd/source-controller/api v1.6.2/go.mod h1:ZJcAi0nemsnBxjVgmJl0WQzNvB0rMETxQMTdoFosmMw=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.2 h1:fWSxsDqYN7My2AEpQwbP7O6Qjix8nGBX+UE/qWHtZfM=
|
||||
github.com/fluxcd/source-watcher/api/v2 v2.0.2/go.mod h1:Hs6ueayPt23jlkIr/d1pGPZ+OHiibQwWjxvU6xqljzg=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
|
||||
@@ -24,9 +24,8 @@ API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${SCRIPT_ROOT}/api/api-ru
|
||||
UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}"
|
||||
CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4"
|
||||
TMPDIR=$(mktemp -d)
|
||||
OPERATOR_CRDDIR=packages/core/installer/crds
|
||||
BACKUPSTRATEGY_CRDDIR=packages/system/backupstrategy-controller/definitions
|
||||
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/crds
|
||||
OPERATOR_CRDDIR=packages/core/installer/definitions
|
||||
COZY_CONTROLLER_CRDDIR=packages/system/cozystack-controller/definitions
|
||||
COZY_RD_CRDDIR=packages/system/cozystack-resource-definition-crd/definition
|
||||
BACKUPS_CORE_CRDDIR=packages/system/backup-controller/definitions
|
||||
BACKUPSTRATEGY_CRDDIR=packages/system/backupstrategy-controller/definitions
|
||||
|
||||
@@ -5,13 +5,11 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
@@ -39,11 +37,8 @@ type CozystackResourceDefinitionReconciler struct {
|
||||
}
|
||||
|
||||
func (r *CozystackResourceDefinitionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
if err := r.reconcileCozyRDAndUpdateHelmReleases(ctx); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Continue with debounced restart logic
|
||||
// Only handle debounced restart logic
|
||||
// HelmRelease reconciliation is handled by CozystackResourceDefinitionHelmReconciler
|
||||
return r.debouncedRestart(ctx)
|
||||
}
|
||||
|
||||
@@ -192,138 +187,3 @@ func sortCozyRDs(a, b cozyv1alpha1.CozystackResourceDefinition) int {
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// reconcileCozyRDAndUpdateHelmReleases reconciles all CozystackResourceDefinitions and updates HelmReleases from them
|
||||
func (r *CozystackResourceDefinitionReconciler) reconcileCozyRDAndUpdateHelmReleases(ctx context.Context) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// List all CozystackResourceDefinitions
|
||||
crdList := &cozyv1alpha1.CozystackResourceDefinitionList{}
|
||||
if err := r.List(ctx, crdList); err != nil {
|
||||
logger.Error(err, "failed to list CozystackResourceDefinitions")
|
||||
return err
|
||||
}
|
||||
|
||||
// Update HelmReleases for each CRD
|
||||
for i := range crdList.Items {
|
||||
crd := &crdList.Items[i]
|
||||
if err := r.updateHelmReleasesForCRD(ctx, crd); err != nil {
|
||||
logger.Error(err, "failed to update HelmReleases for CRD", "crd", crd.Name)
|
||||
// Continue with other CRDs even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateHelmReleasesForCRD updates all HelmReleases that match the application labels from CozystackResourceDefinition
|
||||
func (r *CozystackResourceDefinitionReconciler) updateHelmReleasesForCRD(ctx context.Context, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Use application labels to find HelmReleases
|
||||
// Labels: apps.cozystack.io/application.kind and apps.cozystack.io/application.group
|
||||
applicationKind := crd.Spec.Application.Kind
|
||||
|
||||
// Validate that applicationKind is non-empty
|
||||
if applicationKind == "" {
|
||||
logger.Error(fmt.Errorf("Application.Kind is empty"), "Skipping HelmRelease update: invalid CozystackResourceDefinition", "crd", crd.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
applicationGroup := "apps.cozystack.io" // All applications use this group
|
||||
|
||||
// Build label selector for HelmReleases
|
||||
// Only reconcile HelmReleases with cozystack.io/ui=true label
|
||||
labelSelector := client.MatchingLabels{
|
||||
"apps.cozystack.io/application.kind": applicationKind,
|
||||
"apps.cozystack.io/application.group": applicationGroup,
|
||||
"cozystack.io/ui": "true",
|
||||
}
|
||||
|
||||
// List all HelmReleases with matching labels
|
||||
hrList := &helmv2.HelmReleaseList{}
|
||||
if err := r.List(ctx, hrList, labelSelector); err != nil {
|
||||
logger.Error(err, "failed to list HelmReleases", "kind", applicationKind, "group", applicationGroup)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.V(4).Info("Found HelmReleases to update", "crd", crd.Name, "kind", applicationKind, "count", len(hrList.Items))
|
||||
|
||||
// Update each HelmRelease
|
||||
for i := range hrList.Items {
|
||||
hr := &hrList.Items[i]
|
||||
if err := r.updateHelmReleaseChart(ctx, hr, crd); err != nil {
|
||||
logger.Error(err, "failed to update HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateHelmReleaseChart updates the chart in HelmRelease based on CozystackResourceDefinition
|
||||
func (r *CozystackResourceDefinitionReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
logger := log.FromContext(ctx)
|
||||
hrCopy := hr.DeepCopy()
|
||||
updated := false
|
||||
|
||||
// Validate Chart configuration exists
|
||||
if crd.Spec.Release.Chart.Name == "" {
|
||||
logger.V(4).Info("Skipping HelmRelease chart update: Chart.Name is empty", "crd", crd.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate SourceRef fields
|
||||
if crd.Spec.Release.Chart.SourceRef.Kind == "" ||
|
||||
crd.Spec.Release.Chart.SourceRef.Name == "" ||
|
||||
crd.Spec.Release.Chart.SourceRef.Namespace == "" {
|
||||
logger.Error(fmt.Errorf("invalid SourceRef in CRD"), "Skipping HelmRelease chart update: SourceRef fields are incomplete",
|
||||
"crd", crd.Name,
|
||||
"kind", crd.Spec.Release.Chart.SourceRef.Kind,
|
||||
"name", crd.Spec.Release.Chart.SourceRef.Name,
|
||||
"namespace", crd.Spec.Release.Chart.SourceRef.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get version and reconcileStrategy from CRD or use defaults
|
||||
version := ">= 0.0.0-0"
|
||||
reconcileStrategy := "Revision"
|
||||
// TODO: Add Version and ReconcileStrategy fields to CozystackResourceDefinitionChart if needed
|
||||
|
||||
// Build expected SourceRef
|
||||
expectedSourceRef := helmv2.CrossNamespaceObjectReference{
|
||||
Kind: crd.Spec.Release.Chart.SourceRef.Kind,
|
||||
Name: crd.Spec.Release.Chart.SourceRef.Name,
|
||||
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
|
||||
}
|
||||
|
||||
if hrCopy.Spec.Chart == nil {
|
||||
// Need to create Chart spec
|
||||
hrCopy.Spec.Chart = &helmv2.HelmChartTemplate{
|
||||
Spec: helmv2.HelmChartTemplateSpec{
|
||||
Chart: crd.Spec.Release.Chart.Name,
|
||||
Version: version,
|
||||
ReconcileStrategy: reconcileStrategy,
|
||||
SourceRef: expectedSourceRef,
|
||||
},
|
||||
}
|
||||
updated = true
|
||||
} else {
|
||||
// Update existing Chart spec
|
||||
if hrCopy.Spec.Chart.Spec.Chart != crd.Spec.Release.Chart.Name ||
|
||||
hrCopy.Spec.Chart.Spec.SourceRef != expectedSourceRef {
|
||||
hrCopy.Spec.Chart.Spec.Chart = crd.Spec.Release.Chart.Name
|
||||
hrCopy.Spec.Chart.Spec.SourceRef = expectedSourceRef
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
if updated {
|
||||
logger.V(4).Info("Updating HelmRelease chart", "name", hr.Name, "namespace", hr.Namespace)
|
||||
if err := r.Update(ctx, hrCopy); err != nil {
|
||||
return fmt.Errorf("failed to update HelmRelease: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
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
|
||||
}
|
||||
|
||||
// updateHelmReleaseChart updates the chart in HelmRelease based on CozystackResourceDefinition
|
||||
func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.CozystackResourceDefinition) error {
|
||||
logger := log.FromContext(ctx)
|
||||
hrCopy := hr.DeepCopy()
|
||||
updated := false
|
||||
|
||||
// Validate Chart configuration exists
|
||||
if crd.Spec.Release.Chart.Name == "" {
|
||||
logger.V(4).Info("Skipping HelmRelease chart update: Chart.Name is empty", "crd", crd.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate SourceRef fields
|
||||
if crd.Spec.Release.Chart.SourceRef.Kind == "" ||
|
||||
crd.Spec.Release.Chart.SourceRef.Name == "" ||
|
||||
crd.Spec.Release.Chart.SourceRef.Namespace == "" {
|
||||
logger.Error(fmt.Errorf("invalid SourceRef in CRD"), "Skipping HelmRelease chart update: SourceRef fields are incomplete",
|
||||
"crd", crd.Name,
|
||||
"kind", crd.Spec.Release.Chart.SourceRef.Kind,
|
||||
"name", crd.Spec.Release.Chart.SourceRef.Name,
|
||||
"namespace", crd.Spec.Release.Chart.SourceRef.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get version and reconcileStrategy from CRD or use defaults
|
||||
version := ">= 0.0.0-0"
|
||||
reconcileStrategy := "Revision"
|
||||
// TODO: Add Version and ReconcileStrategy fields to CozystackResourceDefinitionChart if needed
|
||||
|
||||
// Build expected SourceRef
|
||||
expectedSourceRef := helmv2.CrossNamespaceObjectReference{
|
||||
Kind: crd.Spec.Release.Chart.SourceRef.Kind,
|
||||
Name: crd.Spec.Release.Chart.SourceRef.Name,
|
||||
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
|
||||
}
|
||||
|
||||
if hrCopy.Spec.Chart == nil {
|
||||
// Need to create Chart spec
|
||||
hrCopy.Spec.Chart = &helmv2.HelmChartTemplate{
|
||||
Spec: helmv2.HelmChartTemplateSpec{
|
||||
Chart: crd.Spec.Release.Chart.Name,
|
||||
Version: version,
|
||||
ReconcileStrategy: reconcileStrategy,
|
||||
SourceRef: expectedSourceRef,
|
||||
},
|
||||
}
|
||||
updated = true
|
||||
} else {
|
||||
// Update existing Chart spec
|
||||
if hrCopy.Spec.Chart.Spec.Chart != crd.Spec.Release.Chart.Name ||
|
||||
hrCopy.Spec.Chart.Spec.SourceRef != expectedSourceRef {
|
||||
hrCopy.Spec.Chart.Spec.Chart = crd.Spec.Release.Chart.Name
|
||||
hrCopy.Spec.Chart.Spec.SourceRef = expectedSourceRef
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
|
||||
if updated {
|
||||
logger.V(4).Info("Updating HelmRelease chart", "name", hr.Name, "namespace", hr.Namespace)
|
||||
if err := r.Update(ctx, hrCopy); err != nil {
|
||||
return fmt.Errorf("failed to update HelmRelease: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package fluxinstall
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/yaml"
|
||||
k8syaml "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// Install installs Flux components using embedded manifests.
|
||||
// It extracts the manifests and applies them to the cluster.
|
||||
// The namespace is automatically determined from the Namespace object in the manifests.
|
||||
func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifests func(string) error) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Create temporary directory for manifests
|
||||
tmpDir, err := os.MkdirTemp("", "flux-install-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Extract embedded manifests (generated by cozypkg)
|
||||
manifestsDir := filepath.Join(tmpDir, "manifests")
|
||||
if err := os.MkdirAll(manifestsDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create manifests directory: %w", err)
|
||||
}
|
||||
|
||||
if err := writeEmbeddedManifests(manifestsDir); err != nil {
|
||||
return fmt.Errorf("failed to extract embedded manifests: %w", err)
|
||||
}
|
||||
|
||||
// Find the manifest file (should be fluxcd.yaml from cozypkg)
|
||||
manifestPath := filepath.Join(manifestsDir, "fluxcd.yaml")
|
||||
if _, err := os.Stat(manifestPath); err != nil {
|
||||
// Try to find any YAML file if fluxcd.yaml doesn't exist
|
||||
entries, err := os.ReadDir(manifestsDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read manifests directory: %w", err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if strings.HasSuffix(entry.Name(), ".yaml") {
|
||||
manifestPath = filepath.Join(manifestsDir, entry.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and apply manifests
|
||||
objects, err := parseManifests(manifestPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse manifests: %w", err)
|
||||
}
|
||||
|
||||
if len(objects) == 0 {
|
||||
return fmt.Errorf("no objects found in manifests")
|
||||
}
|
||||
|
||||
// Inject KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT if set in operator environment
|
||||
if err := injectKubernetesServiceEnv(objects); err != nil {
|
||||
logger.Info("Failed to inject KUBERNETES_SERVICE_* env vars, continuing anyway", "error", err)
|
||||
}
|
||||
|
||||
// Extract namespace from Namespace object in manifests
|
||||
namespace, err := extractNamespace(objects)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to extract namespace from manifests: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Installing Flux components", "namespace", namespace)
|
||||
|
||||
// Apply manifests using server-side apply
|
||||
logger.Info("Applying Flux manifests", "count", len(objects), "manifest", manifestPath, "namespace", namespace)
|
||||
if err := applyManifests(ctx, k8sClient, objects); err != nil {
|
||||
return fmt.Errorf("failed to apply manifests: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Flux installation completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseManifests parses YAML manifests into unstructured objects.
|
||||
func parseManifests(manifestPath string) ([]*unstructured.Unstructured, error) {
|
||||
data, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read manifest file: %w", err)
|
||||
}
|
||||
|
||||
return readYAMLObjects(bytes.NewReader(data))
|
||||
}
|
||||
|
||||
// readYAMLObjects parses multi-document YAML into unstructured objects.
|
||||
func readYAMLObjects(reader io.Reader) ([]*unstructured.Unstructured, error) {
|
||||
var objects []*unstructured.Unstructured
|
||||
yamlReader := k8syaml.NewYAMLReader(bufio.NewReader(reader))
|
||||
|
||||
for {
|
||||
doc, err := yamlReader.Read()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("failed to read YAML document: %w", err)
|
||||
}
|
||||
|
||||
// Skip empty documents
|
||||
if len(bytes.TrimSpace(doc)) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
obj := &unstructured.Unstructured{}
|
||||
decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewReader(doc), len(doc))
|
||||
if err := decoder.Decode(obj); err != nil {
|
||||
// Skip documents that can't be decoded (might be comments or empty)
|
||||
if err == io.EOF {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("failed to decode YAML document: %w", err)
|
||||
}
|
||||
|
||||
// Skip empty objects (no kind)
|
||||
if obj.GetKind() == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
objects = append(objects, obj)
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
// applyManifests applies Kubernetes objects using server-side apply.
|
||||
func applyManifests(ctx context.Context, k8sClient client.Client, objects []*unstructured.Unstructured) error {
|
||||
logger := log.FromContext(ctx)
|
||||
decoder := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
|
||||
|
||||
// Separate CRDs and namespaces from other resources
|
||||
var stageOne []*unstructured.Unstructured // CRDs and Namespaces
|
||||
var stageTwo []*unstructured.Unstructured // Everything else
|
||||
|
||||
for _, obj := range objects {
|
||||
if isClusterDefinition(obj) {
|
||||
stageOne = append(stageOne, obj)
|
||||
} else {
|
||||
stageTwo = append(stageTwo, obj)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply stage one (CRDs and Namespaces) first
|
||||
if len(stageOne) > 0 {
|
||||
logger.Info("Applying cluster definitions", "count", len(stageOne))
|
||||
if err := applyObjects(ctx, k8sClient, decoder, stageOne); err != nil {
|
||||
return fmt.Errorf("failed to apply cluster definitions: %w", err)
|
||||
}
|
||||
|
||||
// Wait a bit for CRDs to be registered
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
// Apply stage two (everything else)
|
||||
if len(stageTwo) > 0 {
|
||||
logger.Info("Applying resources", "count", len(stageTwo))
|
||||
if err := applyObjects(ctx, k8sClient, decoder, stageTwo); err != nil {
|
||||
return fmt.Errorf("failed to apply resources: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyObjects applies a list of objects using server-side apply.
|
||||
func applyObjects(ctx context.Context, k8sClient client.Client, decoder runtime.Decoder, objects []*unstructured.Unstructured) error {
|
||||
for _, obj := range objects {
|
||||
// Use server-side apply with force ownership and field manager
|
||||
// FieldManager is required for apply patch operations
|
||||
patchOptions := &client.PatchOptions{
|
||||
FieldManager: "cozystack-operator",
|
||||
Force: func() *bool { b := true; return &b }(),
|
||||
}
|
||||
|
||||
if err := k8sClient.Patch(ctx, obj, client.Apply, patchOptions); err != nil {
|
||||
return fmt.Errorf("failed to apply object %s/%s: %w", obj.GetKind(), obj.GetName(), err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// extractNamespace extracts the namespace name from the Namespace object in the manifests.
|
||||
func extractNamespace(objects []*unstructured.Unstructured) (string, error) {
|
||||
for _, obj := range objects {
|
||||
if obj.GetKind() == "Namespace" {
|
||||
namespace := obj.GetName()
|
||||
if namespace == "" {
|
||||
return "", fmt.Errorf("Namespace object has no name")
|
||||
}
|
||||
return namespace, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no Namespace object found in manifests")
|
||||
}
|
||||
|
||||
// isClusterDefinition checks if an object is a CRD or Namespace.
|
||||
func isClusterDefinition(obj *unstructured.Unstructured) bool {
|
||||
kind := obj.GetKind()
|
||||
return kind == "CustomResourceDefinition" || kind == "Namespace"
|
||||
}
|
||||
|
||||
// injectKubernetesServiceEnv injects KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT
|
||||
// environment variables into all containers of Deployment, StatefulSet, and DaemonSet objects
|
||||
// if these variables are set in the operator's environment.
|
||||
func injectKubernetesServiceEnv(objects []*unstructured.Unstructured) error {
|
||||
kubernetesHost := os.Getenv("KUBERNETES_SERVICE_HOST")
|
||||
kubernetesPort := os.Getenv("KUBERNETES_SERVICE_PORT")
|
||||
|
||||
// If neither variable is set, nothing to do
|
||||
if kubernetesHost == "" && kubernetesPort == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, obj := range objects {
|
||||
kind := obj.GetKind()
|
||||
if kind != "Deployment" && kind != "StatefulSet" && kind != "DaemonSet" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Navigate to spec.template.spec.containers
|
||||
spec, found, err := unstructured.NestedMap(obj.Object, "spec", "template", "spec")
|
||||
if !found || err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update containers
|
||||
containers, found, err := unstructured.NestedSlice(spec, "containers")
|
||||
if found && err == nil {
|
||||
containers = updateContainersEnv(containers, kubernetesHost, kubernetesPort)
|
||||
if err := unstructured.SetNestedSlice(spec, containers, "containers"); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Update initContainers
|
||||
initContainers, found, err := unstructured.NestedSlice(spec, "initContainers")
|
||||
if found && err == nil {
|
||||
initContainers = updateContainersEnv(initContainers, kubernetesHost, kubernetesPort)
|
||||
if err := unstructured.SetNestedSlice(spec, initContainers, "initContainers"); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Update spec in the object
|
||||
if err := unstructured.SetNestedMap(obj.Object, spec, "spec", "template", "spec"); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateContainersEnv updates environment variables for a slice of containers.
|
||||
func updateContainersEnv(containers []interface{}, kubernetesHost, kubernetesPort string) []interface{} {
|
||||
for i, container := range containers {
|
||||
containerMap, ok := container.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
env, found, err := unstructured.NestedSlice(containerMap, "env")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !found {
|
||||
env = []interface{}{}
|
||||
}
|
||||
|
||||
// Update or add KUBERNETES_SERVICE_HOST
|
||||
if kubernetesHost != "" {
|
||||
env = setEnvVar(env, "KUBERNETES_SERVICE_HOST", kubernetesHost)
|
||||
}
|
||||
|
||||
// Update or add KUBERNETES_SERVICE_PORT
|
||||
if kubernetesPort != "" {
|
||||
env = setEnvVar(env, "KUBERNETES_SERVICE_PORT", kubernetesPort)
|
||||
}
|
||||
|
||||
// Update the container's env
|
||||
if err := unstructured.SetNestedSlice(containerMap, env, "env"); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Update the container in the slice
|
||||
containers[i] = containerMap
|
||||
}
|
||||
|
||||
return containers
|
||||
}
|
||||
|
||||
// setEnvVar updates or adds an environment variable in the env slice.
|
||||
func setEnvVar(env []interface{}, name, value string) []interface{} {
|
||||
// Check if variable already exists
|
||||
for i, envVar := range env {
|
||||
envVarMap, ok := envVar.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if envVarMap["name"] == name {
|
||||
// Update existing variable
|
||||
envVarMap["value"] = value
|
||||
env[i] = envVarMap
|
||||
return env
|
||||
}
|
||||
}
|
||||
|
||||
// Add new variable
|
||||
env = append(env, map[string]interface{}{
|
||||
"name": name,
|
||||
"value": value,
|
||||
})
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package fluxinstall
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
//go:embed manifests/*.yaml
|
||||
var embeddedFluxManifests embed.FS
|
||||
|
||||
// WriteEmbeddedManifests extracts embedded Flux manifests to a temporary directory.
|
||||
func WriteEmbeddedManifests(dir string) error {
|
||||
manifests, err := fs.ReadDir(embeddedFluxManifests, "manifests")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read embedded manifests: %w", err)
|
||||
}
|
||||
|
||||
for _, manifest := range manifests {
|
||||
data, err := fs.ReadFile(embeddedFluxManifests, path.Join("manifests", manifest.Name()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file %s: %w", manifest.Name(), err)
|
||||
}
|
||||
|
||||
outputPath := path.Join(dir, manifest.Name())
|
||||
if err := os.WriteFile(outputPath, data, 0666); err != nil {
|
||||
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,889 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package operator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
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"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
// PackageReconciler reconciles Package resources
|
||||
type PackageReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=packages,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=packages/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=packagesources,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=helm.toolkit.fluxcd.io,resources=helmreleases,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;create;update;patch
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop
|
||||
func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
pkg := &cozyv1alpha1.Package{}
|
||||
if err := r.Get(ctx, req.NamespacedName, pkg); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Resource not found, return (ownerReference will handle cleanup)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Get PackageSource with the same name
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "PackageSourceNotFound",
|
||||
Message: fmt.Sprintf("PackageSource %s not found", pkg.Name),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Determine variant (default to "default" if not specified)
|
||||
variantName := pkg.Spec.Variant
|
||||
if variantName == "" {
|
||||
variantName = "default"
|
||||
}
|
||||
|
||||
// Find the variant in PackageSource
|
||||
var variant *cozyv1alpha1.Variant
|
||||
for i := range packageSource.Spec.Variants {
|
||||
if packageSource.Spec.Variants[i].Name == variantName {
|
||||
variant = &packageSource.Spec.Variants[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if variant == nil {
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "VariantNotFound",
|
||||
Message: fmt.Sprintf("Variant %s not found in PackageSource %s", variantName, pkg.Name),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Reconcile namespaces from components
|
||||
if err := r.reconcileNamespaces(ctx, pkg, variant); err != nil {
|
||||
logger.Error(err, "failed to reconcile namespaces")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Validate variant dependencies before creating HelmReleases
|
||||
// If dependencies are missing, we don't create new HelmReleases but don't delete existing ones
|
||||
if err := r.validateVariantDependencies(ctx, pkg, variant); err != nil {
|
||||
logger.Info("variant dependencies not ready, skipping HelmRelease creation", "package", pkg.Name, "error", err)
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "DependenciesNotReady",
|
||||
Message: fmt.Sprintf("Variant dependencies not ready: %v", err),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// Return success to avoid requeue, but don't create HelmReleases
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Create HelmReleases for components with Install section
|
||||
helmReleaseCount := 0
|
||||
for _, component := range variant.Components {
|
||||
// Skip components without Install section
|
||||
if component.Install == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if component is disabled via Package spec
|
||||
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok {
|
||||
if pkgComponent.Enabled != nil && !*pkgComponent.Enabled {
|
||||
logger.V(1).Info("skipping disabled component", "package", pkg.Name, "component", component.Name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Build artifact name: <packagesource>-<variant>-<componentname> (with dots replaced by dashes)
|
||||
artifactName := fmt.Sprintf("%s-%s-%s",
|
||||
strings.ReplaceAll(packageSource.Name, ".", "-"),
|
||||
strings.ReplaceAll(variantName, ".", "-"),
|
||||
strings.ReplaceAll(component.Name, ".", "-"))
|
||||
|
||||
// Determine namespace (from Install or default to cozy-system)
|
||||
namespace := component.Install.Namespace
|
||||
if namespace == "" {
|
||||
namespace = "cozy-system"
|
||||
}
|
||||
|
||||
// Determine release name (from Install or use component name)
|
||||
releaseName := component.Install.ReleaseName
|
||||
if releaseName == "" {
|
||||
releaseName = component.Name
|
||||
}
|
||||
|
||||
// Build labels
|
||||
labels := make(map[string]string)
|
||||
labels["cozystack.io/package"] = pkg.Name
|
||||
if component.Install.Privileged {
|
||||
labels["cozystack.io/privileged"] = "true"
|
||||
}
|
||||
|
||||
// Create HelmRelease
|
||||
hr := &helmv2.HelmRelease{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: releaseName,
|
||||
Namespace: namespace,
|
||||
Labels: labels,
|
||||
},
|
||||
Spec: helmv2.HelmReleaseSpec{
|
||||
Interval: metav1.Duration{Duration: 5 * 60 * 1000000000}, // 5m
|
||||
ChartRef: &helmv2.CrossNamespaceSourceReference{
|
||||
Kind: "ExternalArtifact",
|
||||
Name: artifactName,
|
||||
Namespace: "cozy-system",
|
||||
},
|
||||
Install: &helmv2.Install{
|
||||
Remediation: &helmv2.InstallRemediation{
|
||||
Retries: -1,
|
||||
},
|
||||
},
|
||||
Upgrade: &helmv2.Upgrade{
|
||||
Remediation: &helmv2.UpgradeRemediation{
|
||||
Retries: -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Set ownerReference
|
||||
hr.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: pkg.APIVersion,
|
||||
Kind: pkg.Kind,
|
||||
Name: pkg.Name,
|
||||
UID: pkg.UID,
|
||||
Controller: func() *bool { b := true; return &b }(),
|
||||
},
|
||||
}
|
||||
|
||||
// Merge values from Package spec if provided
|
||||
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok && pkgComponent.Values != nil {
|
||||
hr.Spec.Values = pkgComponent.Values
|
||||
}
|
||||
|
||||
// Build DependsOn from component Install and variant DependsOn
|
||||
dependsOn, err := r.buildDependsOn(ctx, pkg, packageSource, variant, &component)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to build DependsOn", "component", component.Name)
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "DependsOnFailed",
|
||||
Message: fmt.Sprintf("Failed to build DependsOn for component %s: %v", component.Name, err),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
if len(dependsOn) > 0 {
|
||||
hr.Spec.DependsOn = dependsOn
|
||||
}
|
||||
|
||||
if err := r.createOrUpdateHelmRelease(ctx, hr); err != nil {
|
||||
logger.Error(err, "failed to reconcile HelmRelease", "name", releaseName, "namespace", namespace)
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "HelmReleaseFailed",
|
||||
Message: fmt.Sprintf("Failed to create HelmRelease %s: %v", releaseName, err),
|
||||
})
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
helmReleaseCount++
|
||||
logger.Info("reconciled HelmRelease", "package", pkg.Name, "component", component.Name, "releaseName", releaseName, "namespace", namespace)
|
||||
}
|
||||
|
||||
// Cleanup orphaned HelmReleases
|
||||
if err := r.cleanupOrphanedHelmReleases(ctx, pkg, variant); err != nil {
|
||||
logger.Error(err, "failed to cleanup orphaned HelmReleases")
|
||||
// Don't return error, continue with status update
|
||||
}
|
||||
|
||||
// Update status with success message
|
||||
message := fmt.Sprintf("reconciliation succeeded, generated %d helmrelease(s)", helmReleaseCount)
|
||||
meta.SetStatusCondition(&pkg.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "ReconciliationSucceeded",
|
||||
Message: message,
|
||||
})
|
||||
|
||||
if err := r.Status().Update(ctx, pkg); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
logger.Info("reconciled Package", "name", pkg.Name, "helmReleaseCount", helmReleaseCount)
|
||||
|
||||
// Trigger reconcile for Packages that depend on this Package
|
||||
if err := r.triggerDependentPackages(ctx, pkg.Name); err != nil {
|
||||
logger.Error(err, "failed to trigger dependent Packages", "package", pkg.Name)
|
||||
// Don't return error, this is best-effort
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// createOrUpdateHelmRelease creates or updates a HelmRelease
|
||||
func (r *PackageReconciler) createOrUpdateHelmRelease(ctx context.Context, hr *helmv2.HelmRelease) error {
|
||||
existing := &helmv2.HelmRelease{}
|
||||
key := types.NamespacedName{
|
||||
Name: hr.Name,
|
||||
Namespace: hr.Namespace,
|
||||
}
|
||||
|
||||
err := r.Get(ctx, key, existing)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return r.Create(ctx, hr)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preserve resource version
|
||||
hr.SetResourceVersion(existing.GetResourceVersion())
|
||||
|
||||
// Merge labels
|
||||
labels := hr.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetLabels() {
|
||||
if _, ok := labels[k]; !ok {
|
||||
labels[k] = v
|
||||
}
|
||||
}
|
||||
hr.SetLabels(labels)
|
||||
|
||||
// Merge annotations
|
||||
annotations := hr.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetAnnotations() {
|
||||
if _, ok := annotations[k]; !ok {
|
||||
annotations[k] = v
|
||||
}
|
||||
}
|
||||
hr.SetAnnotations(annotations)
|
||||
|
||||
// Update Spec
|
||||
existing.Spec = hr.Spec
|
||||
existing.SetLabels(hr.GetLabels())
|
||||
existing.SetAnnotations(hr.GetAnnotations())
|
||||
existing.SetOwnerReferences(hr.GetOwnerReferences())
|
||||
|
||||
return r.Update(ctx, existing)
|
||||
}
|
||||
|
||||
// buildDependsOn builds DependsOn list for a component
|
||||
// Includes:
|
||||
// 1. Dependencies from component.Install.DependsOn (with namespace from referenced component)
|
||||
// 2. Dependencies from variant.DependsOn (all components with Install from referenced Package)
|
||||
func (r *PackageReconciler) buildDependsOn(ctx context.Context, pkg *cozyv1alpha1.Package, packageSource *cozyv1alpha1.PackageSource, variant *cozyv1alpha1.Variant, component *cozyv1alpha1.Component) ([]helmv2.DependencyReference, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
dependsOn := []helmv2.DependencyReference{}
|
||||
|
||||
// Build map of component names to their release names and namespaces in current variant
|
||||
componentMap := make(map[string]struct {
|
||||
releaseName string
|
||||
namespace string
|
||||
})
|
||||
for _, comp := range variant.Components {
|
||||
if comp.Install == nil {
|
||||
continue
|
||||
}
|
||||
compNamespace := comp.Install.Namespace
|
||||
if compNamespace == "" {
|
||||
compNamespace = "cozy-system"
|
||||
}
|
||||
compReleaseName := comp.Install.ReleaseName
|
||||
if compReleaseName == "" {
|
||||
compReleaseName = comp.Name
|
||||
}
|
||||
componentMap[comp.Name] = struct {
|
||||
releaseName string
|
||||
namespace string
|
||||
}{
|
||||
releaseName: compReleaseName,
|
||||
namespace: compNamespace,
|
||||
}
|
||||
}
|
||||
|
||||
// Add dependencies from component.Install.DependsOn
|
||||
if len(component.Install.DependsOn) > 0 {
|
||||
for _, depName := range component.Install.DependsOn {
|
||||
depComp, ok := componentMap[depName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("component %s not found in variant for dependency %s", depName, component.Name)
|
||||
}
|
||||
dependsOn = append(dependsOn, helmv2.DependencyReference{
|
||||
Name: depComp.releaseName,
|
||||
Namespace: depComp.namespace,
|
||||
})
|
||||
logger.V(1).Info("added component dependency", "component", component.Name, "dependsOn", depName, "releaseName", depComp.releaseName, "namespace", depComp.namespace)
|
||||
}
|
||||
}
|
||||
|
||||
// Add dependencies from variant.DependsOn
|
||||
if len(variant.DependsOn) > 0 {
|
||||
for _, depPackageName := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == depPackageName {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
logger.V(1).Info("ignoring dependency", "package", pkg.Name, "dependency", depPackageName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the Package
|
||||
depPackage := &cozyv1alpha1.Package{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackage); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("dependent Package %s not found", depPackageName)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get dependent Package %s: %w", depPackageName, err)
|
||||
}
|
||||
|
||||
// Get the variant from dependent Package
|
||||
depVariantName := depPackage.Spec.Variant
|
||||
if depVariantName == "" {
|
||||
depVariantName = "default"
|
||||
}
|
||||
|
||||
// Get the PackageSource
|
||||
depPackageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("dependent PackageSource %s not found", depPackageName)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get dependent PackageSource %s: %w", depPackageName, err)
|
||||
}
|
||||
|
||||
// Find the variant in PackageSource
|
||||
var depVariant *cozyv1alpha1.Variant
|
||||
for i := range depPackageSource.Spec.Variants {
|
||||
if depPackageSource.Spec.Variants[i].Name == depVariantName {
|
||||
depVariant = &depPackageSource.Spec.Variants[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if depVariant == nil {
|
||||
return nil, fmt.Errorf("dependent variant %s not found in PackageSource %s", depVariantName, depPackageName)
|
||||
}
|
||||
|
||||
// Add all components with Install from dependent variant
|
||||
for _, depComp := range depVariant.Components {
|
||||
if depComp.Install == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if component is disabled in dependent Package
|
||||
if depPkgComponent, ok := depPackage.Spec.Components[depComp.Name]; ok {
|
||||
if depPkgComponent.Enabled != nil && !*depPkgComponent.Enabled {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
depCompNamespace := depComp.Install.Namespace
|
||||
if depCompNamespace == "" {
|
||||
depCompNamespace = "cozy-system"
|
||||
}
|
||||
depCompReleaseName := depComp.Install.ReleaseName
|
||||
if depCompReleaseName == "" {
|
||||
depCompReleaseName = depComp.Name
|
||||
}
|
||||
|
||||
dependsOn = append(dependsOn, helmv2.DependencyReference{
|
||||
Name: depCompReleaseName,
|
||||
Namespace: depCompNamespace,
|
||||
})
|
||||
logger.V(1).Info("added variant dependency", "package", pkg.Name, "dependency", depPackageName, "component", depComp.Name, "releaseName", depCompReleaseName, "namespace", depCompNamespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dependsOn, nil
|
||||
}
|
||||
|
||||
// validateVariantDependencies validates that all variant dependencies exist
|
||||
// Returns error if any dependency is missing
|
||||
func (r *PackageReconciler) validateVariantDependencies(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
if len(variant.DependsOn) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, depPackageName := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == depPackageName {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
logger.V(1).Info("ignoring dependency", "package", pkg.Name, "dependency", depPackageName)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the Package
|
||||
depPackage := &cozyv1alpha1.Package{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackage); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("dependent Package %s not found", depPackageName)
|
||||
}
|
||||
return fmt.Errorf("failed to get dependent Package %s: %w", depPackageName, err)
|
||||
}
|
||||
|
||||
// Get the PackageSource
|
||||
depPackageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: depPackageName}, depPackageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return fmt.Errorf("dependent PackageSource %s not found", depPackageName)
|
||||
}
|
||||
return fmt.Errorf("failed to get dependent PackageSource %s: %w", depPackageName, err)
|
||||
}
|
||||
|
||||
// Get the variant from dependent Package
|
||||
depVariantName := depPackage.Spec.Variant
|
||||
if depVariantName == "" {
|
||||
depVariantName = "default"
|
||||
}
|
||||
|
||||
// Find the variant in PackageSource
|
||||
var depVariant *cozyv1alpha1.Variant
|
||||
for i := range depPackageSource.Spec.Variants {
|
||||
if depPackageSource.Spec.Variants[i].Name == depVariantName {
|
||||
depVariant = &depPackageSource.Spec.Variants[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if depVariant == nil {
|
||||
return fmt.Errorf("dependent variant %s not found in PackageSource %s", depVariantName, depPackageName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconcileNamespaces creates or updates namespaces based on components in the variant
|
||||
func (r *PackageReconciler) reconcileNamespaces(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Collect namespaces from components
|
||||
// Map: namespace -> {isPrivileged}
|
||||
type namespaceInfo struct {
|
||||
privileged bool
|
||||
}
|
||||
namespacesMap := make(map[string]namespaceInfo)
|
||||
|
||||
for _, component := range variant.Components {
|
||||
// Skip components without Install section
|
||||
if component.Install == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if component is disabled via Package spec
|
||||
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok {
|
||||
if pkgComponent.Enabled != nil && !*pkgComponent.Enabled {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace must be set
|
||||
namespace := component.Install.Namespace
|
||||
if namespace == "" {
|
||||
return fmt.Errorf("component %s has empty namespace in Install section", component.Name)
|
||||
}
|
||||
|
||||
info, exists := namespacesMap[namespace]
|
||||
if !exists {
|
||||
info = namespaceInfo{
|
||||
privileged: false,
|
||||
}
|
||||
}
|
||||
|
||||
// If component is privileged, mark namespace as privileged
|
||||
if component.Install.Privileged {
|
||||
info.privileged = true
|
||||
}
|
||||
|
||||
namespacesMap[namespace] = info
|
||||
}
|
||||
|
||||
// Create or update all namespaces
|
||||
for nsName, info := range namespacesMap {
|
||||
namespace := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: nsName,
|
||||
Labels: make(map[string]string),
|
||||
Annotations: map[string]string{
|
||||
"helm.sh/resource-policy": "keep",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Add system label only for non-tenant namespaces
|
||||
if !strings.HasPrefix(nsName, "tenant-") {
|
||||
namespace.Labels["cozystack.io/system"] = "true"
|
||||
}
|
||||
|
||||
// Add privileged label if needed
|
||||
if info.privileged {
|
||||
namespace.Labels["pod-security.kubernetes.io/enforce"] = "privileged"
|
||||
}
|
||||
|
||||
if err := r.createOrUpdateNamespace(ctx, namespace); err != nil {
|
||||
logger.Error(err, "failed to reconcile namespace", "name", nsName, "privileged", info.privileged)
|
||||
return fmt.Errorf("failed to reconcile namespace %s: %w", nsName, err)
|
||||
}
|
||||
logger.Info("reconciled namespace", "name", nsName, "privileged", info.privileged)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createOrUpdateNamespace creates or updates a namespace
|
||||
func (r *PackageReconciler) createOrUpdateNamespace(ctx context.Context, namespace *corev1.Namespace) error {
|
||||
existing := &corev1.Namespace{}
|
||||
key := types.NamespacedName{Name: namespace.Name}
|
||||
|
||||
err := r.Get(ctx, key, existing)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return r.Create(ctx, namespace)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preserve resource version
|
||||
namespace.SetResourceVersion(existing.GetResourceVersion())
|
||||
|
||||
// Merge labels
|
||||
labels := namespace.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetLabels() {
|
||||
if _, ok := labels[k]; !ok {
|
||||
labels[k] = v
|
||||
}
|
||||
}
|
||||
namespace.SetLabels(labels)
|
||||
|
||||
// Merge annotations
|
||||
annotations := namespace.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetAnnotations() {
|
||||
if _, ok := annotations[k]; !ok {
|
||||
annotations[k] = v
|
||||
}
|
||||
}
|
||||
namespace.SetAnnotations(annotations)
|
||||
|
||||
return r.Update(ctx, namespace)
|
||||
}
|
||||
|
||||
// cleanupOrphanedHelmReleases removes HelmReleases that are no longer needed
|
||||
func (r *PackageReconciler) cleanupOrphanedHelmReleases(ctx context.Context, pkg *cozyv1alpha1.Package, variant *cozyv1alpha1.Variant) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Build map of desired HelmRelease names (from components with Install)
|
||||
desiredReleases := make(map[types.NamespacedName]bool)
|
||||
for _, component := range variant.Components {
|
||||
if component.Install == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if component is disabled via Package spec
|
||||
if pkgComponent, ok := pkg.Spec.Components[component.Name]; ok {
|
||||
if pkgComponent.Enabled != nil && !*pkgComponent.Enabled {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
namespace := component.Install.Namespace
|
||||
if namespace == "" {
|
||||
namespace = "cozy-system"
|
||||
}
|
||||
|
||||
releaseName := component.Install.ReleaseName
|
||||
if releaseName == "" {
|
||||
releaseName = component.Name
|
||||
}
|
||||
|
||||
desiredReleases[types.NamespacedName{
|
||||
Name: releaseName,
|
||||
Namespace: namespace,
|
||||
}] = true
|
||||
}
|
||||
|
||||
// Find all HelmReleases owned by this Package
|
||||
hrList := &helmv2.HelmReleaseList{}
|
||||
if err := r.List(ctx, hrList, client.MatchingLabels{
|
||||
"cozystack.io/package": pkg.Name,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete HelmReleases that are not in desired list
|
||||
for _, hr := range hrList.Items {
|
||||
key := types.NamespacedName{
|
||||
Name: hr.Name,
|
||||
Namespace: hr.Namespace,
|
||||
}
|
||||
if !desiredReleases[key] {
|
||||
logger.Info("deleting orphaned HelmRelease", "name", hr.Name, "namespace", hr.Namespace, "package", pkg.Name)
|
||||
if err := r.Delete(ctx, &hr); err != nil && !apierrors.IsNotFound(err) {
|
||||
logger.Error(err, "failed to delete orphaned HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// triggerDependentPackages triggers reconcile for all Packages that depend on the given Package
|
||||
func (r *PackageReconciler) triggerDependentPackages(ctx context.Context, packageName string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Get all Packages
|
||||
packageList := &cozyv1alpha1.PackageList{}
|
||||
if err := r.List(ctx, packageList); err != nil {
|
||||
return fmt.Errorf("failed to list Packages: %w", err)
|
||||
}
|
||||
|
||||
// For each Package, check if it depends on the given Package
|
||||
for _, pkg := range packageList.Items {
|
||||
// Skip the Package itself
|
||||
if pkg.Name == packageName {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get PackageSource
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
logger.V(1).Error(err, "failed to get PackageSource", "package", pkg.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine variant
|
||||
variantName := pkg.Spec.Variant
|
||||
if variantName == "" {
|
||||
variantName = "default"
|
||||
}
|
||||
|
||||
// Find variant
|
||||
var variant *cozyv1alpha1.Variant
|
||||
for i := range packageSource.Spec.Variants {
|
||||
if packageSource.Spec.Variants[i].Name == variantName {
|
||||
variant = &packageSource.Spec.Variants[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if variant == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this Package depends on the given Package
|
||||
dependsOn := false
|
||||
for _, dep := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == dep {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
|
||||
if dep == packageName {
|
||||
dependsOn = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if dependsOn {
|
||||
logger.V(1).Info("triggering reconcile for dependent Package", "package", pkg.Name, "dependency", packageName)
|
||||
// Trigger reconcile by updating the Package (add annotation or just requeue)
|
||||
// We can't directly requeue from here, but we can update the Package to trigger reconcile
|
||||
// Actually, we can use the client to trigger an update, but that might cause infinite loop
|
||||
// Better approach: use event handler or just log and let the watch handle it
|
||||
// For now, we'll just log - the watch on PackageSource should handle it
|
||||
// But we need a way to trigger reconcile...
|
||||
// Let's add an annotation to trigger reconcile
|
||||
if pkg.Annotations == nil {
|
||||
pkg.Annotations = make(map[string]string)
|
||||
}
|
||||
pkg.Annotations["cozystack.io/trigger-reconcile"] = fmt.Sprintf("%d", metav1.Now().Unix())
|
||||
if err := r.Update(ctx, &pkg); err != nil {
|
||||
logger.V(1).Error(err, "failed to trigger reconcile for dependent Package", "package", pkg.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *PackageReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystack-package").
|
||||
For(&cozyv1alpha1.Package{}).
|
||||
Watches(
|
||||
&cozyv1alpha1.PackageSource{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
ps, ok := obj.(*cozyv1alpha1.PackageSource)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Find Package with the same name as PackageSource
|
||||
// PackageSource and Package share the same name
|
||||
pkg := &cozyv1alpha1.Package{}
|
||||
if err := mgr.GetClient().Get(ctx, types.NamespacedName{Name: ps.Name}, pkg); err != nil {
|
||||
// Package not found, that's ok - it might not exist yet
|
||||
return nil
|
||||
}
|
||||
// Trigger reconcile for the corresponding Package
|
||||
return []reconcile.Request{{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: pkg.Name,
|
||||
},
|
||||
}}
|
||||
}),
|
||||
).
|
||||
Watches(
|
||||
&cozyv1alpha1.Package{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
updatedPkg, ok := obj.(*cozyv1alpha1.Package)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Find all Packages that depend on this Package
|
||||
packageList := &cozyv1alpha1.PackageList{}
|
||||
if err := mgr.GetClient().List(ctx, packageList); err != nil {
|
||||
return nil
|
||||
}
|
||||
var requests []reconcile.Request
|
||||
for _, pkg := range packageList.Items {
|
||||
if pkg.Name == updatedPkg.Name {
|
||||
continue // Skip the Package itself
|
||||
}
|
||||
// Get PackageSource to check dependencies
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := mgr.GetClient().Get(ctx, types.NamespacedName{Name: pkg.Name}, packageSource); err != nil {
|
||||
continue
|
||||
}
|
||||
// Determine variant
|
||||
variantName := pkg.Spec.Variant
|
||||
if variantName == "" {
|
||||
variantName = "default"
|
||||
}
|
||||
// Find variant
|
||||
for _, variant := range packageSource.Spec.Variants {
|
||||
if variant.Name == variantName {
|
||||
// Check if this variant depends on updatedPkg
|
||||
for _, dep := range variant.DependsOn {
|
||||
// Check if dependency is in IgnoreDependencies
|
||||
ignore := false
|
||||
for _, ignoreDep := range pkg.Spec.IgnoreDependencies {
|
||||
if ignoreDep == dep {
|
||||
ignore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if ignore {
|
||||
continue
|
||||
}
|
||||
if dep == updatedPkg.Name {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: pkg.Name,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return requests
|
||||
}),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -1,468 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package operator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
|
||||
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"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
// PackageSourceReconciler reconciles PackageSource resources
|
||||
type PackageSourceReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=packagesources,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=packagesources/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=source.extensions.fluxcd.io,resources=artifactgenerators,verbs=get;list;watch;create;update;patch;delete
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop
|
||||
func (r *PackageSourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
packageSource := &cozyv1alpha1.PackageSource{}
|
||||
if err := r.Get(ctx, req.NamespacedName, packageSource); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// Resource not found, return (ownerReference will handle cleanup)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Generate ArtifactGenerator for package source
|
||||
if err := r.reconcileArtifactGenerators(ctx, packageSource); err != nil {
|
||||
logger.Error(err, "failed to reconcile ArtifactGenerator")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Update PackageSource status (variants and conditions from ArtifactGenerator)
|
||||
if err := r.updateStatus(ctx, packageSource); err != nil {
|
||||
logger.Error(err, "failed to update status")
|
||||
// Don't return error, status update is not critical
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// reconcileArtifactGenerators generates a single ArtifactGenerator for the package source
|
||||
// Creates one ArtifactGenerator per package source with all OutputArtifacts from components
|
||||
func (r *PackageSourceReconciler) reconcileArtifactGenerators(ctx context.Context, packageSource *cozyv1alpha1.PackageSource) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Check if SourceRef is set
|
||||
if packageSource.Spec.SourceRef == nil {
|
||||
logger.Info("skipping ArtifactGenerator creation, SourceRef not set", "packageSource", packageSource.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build library map from all variants
|
||||
// Map key is the library name (from lib.Name or extracted from path)
|
||||
// This allows components to reference libraries by name
|
||||
libraryMap := make(map[string]cozyv1alpha1.Library)
|
||||
for _, variant := range packageSource.Spec.Variants {
|
||||
for _, lib := range variant.Libraries {
|
||||
libName := lib.Name
|
||||
if libName == "" {
|
||||
// If library name is not set, extract from path
|
||||
libName = r.getPackageNameFromPath(lib.Path)
|
||||
}
|
||||
if libName != "" {
|
||||
// Store library with the resolved name
|
||||
libraryMap[libName] = lib
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace is always cozy-system
|
||||
namespace := "cozy-system"
|
||||
// ArtifactGenerator name is the package source name
|
||||
agName := packageSource.Name
|
||||
|
||||
// Collect all OutputArtifacts
|
||||
outputArtifacts := []sourcewatcherv1beta1.OutputArtifact{}
|
||||
|
||||
// Process all variants and their components
|
||||
for _, variant := range packageSource.Spec.Variants {
|
||||
for _, component := range variant.Components {
|
||||
// Skip components without path
|
||||
if component.Path == "" {
|
||||
logger.V(1).Info("skipping component without path", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.V(1).Info("processing component", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name, "path", component.Path)
|
||||
|
||||
// Extract component name from path (last component)
|
||||
componentPathName := r.getPackageNameFromPath(component.Path)
|
||||
if componentPathName == "" {
|
||||
logger.Info("skipping component with invalid path", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name, "path", component.Path)
|
||||
continue
|
||||
}
|
||||
|
||||
// Get basePath with default values
|
||||
basePath := r.getBasePath(packageSource)
|
||||
|
||||
// Build copy operations
|
||||
copyOps := []sourcewatcherv1beta1.CopyOperation{
|
||||
{
|
||||
From: r.buildSourcePath(packageSource.Spec.SourceRef.Name, basePath, component.Path),
|
||||
To: fmt.Sprintf("@artifact/%s/", componentPathName),
|
||||
},
|
||||
}
|
||||
|
||||
// Add libraries if specified
|
||||
for _, libName := range component.Libraries {
|
||||
if lib, ok := libraryMap[libName]; ok {
|
||||
copyOps = append(copyOps, sourcewatcherv1beta1.CopyOperation{
|
||||
From: r.buildSourcePath(packageSource.Spec.SourceRef.Name, basePath, lib.Path),
|
||||
To: fmt.Sprintf("@artifact/%s/charts/%s/", componentPathName, libName),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add valuesFiles if specified
|
||||
for i, valuesFile := range component.ValuesFiles {
|
||||
strategy := "Merge"
|
||||
if i == 0 {
|
||||
strategy = "Overwrite"
|
||||
}
|
||||
copyOps = append(copyOps, sourcewatcherv1beta1.CopyOperation{
|
||||
From: r.buildSourceFilePath(packageSource.Spec.SourceRef.Name, basePath, fmt.Sprintf("%s/%s", component.Path, valuesFile)),
|
||||
To: fmt.Sprintf("@artifact/%s/values.yaml", componentPathName),
|
||||
Strategy: strategy,
|
||||
})
|
||||
}
|
||||
|
||||
// Artifact name: <packagesource>-<variant>-<componentname>
|
||||
// Replace dots with dashes to comply with Kubernetes naming requirements
|
||||
artifactName := fmt.Sprintf("%s-%s-%s",
|
||||
strings.ReplaceAll(packageSource.Name, ".", "-"),
|
||||
strings.ReplaceAll(variant.Name, ".", "-"),
|
||||
strings.ReplaceAll(component.Name, ".", "-"))
|
||||
|
||||
outputArtifacts = append(outputArtifacts, sourcewatcherv1beta1.OutputArtifact{
|
||||
Name: artifactName,
|
||||
Copy: copyOps,
|
||||
})
|
||||
|
||||
logger.Info("added OutputArtifact for component", "packageSource", packageSource.Name, "variant", variant.Name, "component", component.Name, "artifactName", artifactName)
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no OutputArtifacts, return (ownerReference will handle cleanup if needed)
|
||||
if len(outputArtifacts) == 0 {
|
||||
logger.Info("no OutputArtifacts to generate, skipping ArtifactGenerator creation", "packageSource", packageSource.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build labels
|
||||
labels := make(map[string]string)
|
||||
labels["cozystack.io/packagesource"] = packageSource.Name
|
||||
|
||||
// Create single ArtifactGenerator for the package source
|
||||
ag := &sourcewatcherv1beta1.ArtifactGenerator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: agName,
|
||||
Namespace: namespace,
|
||||
Labels: labels,
|
||||
},
|
||||
Spec: sourcewatcherv1beta1.ArtifactGeneratorSpec{
|
||||
Sources: []sourcewatcherv1beta1.SourceReference{
|
||||
{
|
||||
Alias: packageSource.Spec.SourceRef.Name,
|
||||
Kind: packageSource.Spec.SourceRef.Kind,
|
||||
Name: packageSource.Spec.SourceRef.Name,
|
||||
Namespace: packageSource.Spec.SourceRef.Namespace,
|
||||
},
|
||||
},
|
||||
OutputArtifacts: outputArtifacts,
|
||||
},
|
||||
}
|
||||
|
||||
// Set ownerReference
|
||||
ag.OwnerReferences = []metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: packageSource.APIVersion,
|
||||
Kind: packageSource.Kind,
|
||||
Name: packageSource.Name,
|
||||
UID: packageSource.UID,
|
||||
Controller: func() *bool { b := true; return &b }(),
|
||||
},
|
||||
}
|
||||
|
||||
logger.Info("creating ArtifactGenerator for package source", "packageSource", packageSource.Name, "agName", agName, "namespace", namespace, "outputArtifactCount", len(outputArtifacts))
|
||||
|
||||
if err := r.createOrUpdate(ctx, ag); err != nil {
|
||||
return fmt.Errorf("failed to reconcile ArtifactGenerator %s: %w", agName, err)
|
||||
}
|
||||
|
||||
logger.Info("reconciled ArtifactGenerator for package source", "name", agName, "namespace", namespace, "outputArtifactCount", len(outputArtifacts))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func (r *PackageSourceReconciler) getPackageNameFromPath(path string) string {
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) > 0 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// getBasePath returns the basePath with default values based on source kind
|
||||
func (r *PackageSourceReconciler) getBasePath(packageSource *cozyv1alpha1.PackageSource) string {
|
||||
// If path is explicitly set in SourceRef, use it (but normalize "/" to empty)
|
||||
if packageSource.Spec.SourceRef.Path != "" {
|
||||
path := strings.Trim(packageSource.Spec.SourceRef.Path, "/")
|
||||
// If path is "/" or empty after trim, return empty string
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
return path
|
||||
}
|
||||
// Default values based on kind
|
||||
if packageSource.Spec.SourceRef.Kind == "OCIRepository" {
|
||||
return "" // Root for OCI
|
||||
}
|
||||
// Default for GitRepository
|
||||
return "packages"
|
||||
}
|
||||
|
||||
// buildSourcePath builds the full source path using basePath with glob pattern
|
||||
func (r *PackageSourceReconciler) buildSourcePath(sourceName, basePath, path string) string {
|
||||
// Remove leading/trailing slashes and combine
|
||||
parts := []string{}
|
||||
if basePath != "" {
|
||||
trimmed := strings.Trim(basePath, "/")
|
||||
if trimmed != "" {
|
||||
parts = append(parts, trimmed)
|
||||
}
|
||||
}
|
||||
if path != "" {
|
||||
trimmed := strings.Trim(path, "/")
|
||||
if trimmed != "" {
|
||||
parts = append(parts, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
fullPath := strings.Join(parts, "/")
|
||||
if fullPath == "" {
|
||||
return fmt.Sprintf("@%s/**", sourceName)
|
||||
}
|
||||
return fmt.Sprintf("@%s/%s/**", sourceName, fullPath)
|
||||
}
|
||||
|
||||
// buildSourceFilePath builds the full source path for a specific file (without glob pattern)
|
||||
func (r *PackageSourceReconciler) buildSourceFilePath(sourceName, basePath, path string) string {
|
||||
// Remove leading/trailing slashes and combine
|
||||
parts := []string{}
|
||||
if basePath != "" {
|
||||
trimmed := strings.Trim(basePath, "/")
|
||||
if trimmed != "" {
|
||||
parts = append(parts, trimmed)
|
||||
}
|
||||
}
|
||||
if path != "" {
|
||||
trimmed := strings.Trim(path, "/")
|
||||
if trimmed != "" {
|
||||
parts = append(parts, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
fullPath := strings.Join(parts, "/")
|
||||
if fullPath == "" {
|
||||
return fmt.Sprintf("@%s", sourceName)
|
||||
}
|
||||
return fmt.Sprintf("@%s/%s", sourceName, fullPath)
|
||||
}
|
||||
|
||||
// createOrUpdate creates or updates a resource
|
||||
func (r *PackageSourceReconciler) createOrUpdate(ctx context.Context, obj client.Object) error {
|
||||
existing := obj.DeepCopyObject().(client.Object)
|
||||
key := client.ObjectKeyFromObject(obj)
|
||||
|
||||
err := r.Get(ctx, key, existing)
|
||||
if apierrors.IsNotFound(err) {
|
||||
return r.Create(ctx, obj)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Preserve resource version
|
||||
obj.SetResourceVersion(existing.GetResourceVersion())
|
||||
// Merge labels and annotations
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetLabels() {
|
||||
if _, ok := labels[k]; !ok {
|
||||
labels[k] = v
|
||||
}
|
||||
}
|
||||
obj.SetLabels(labels)
|
||||
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
for k, v := range existing.GetAnnotations() {
|
||||
if _, ok := annotations[k]; !ok {
|
||||
annotations[k] = v
|
||||
}
|
||||
}
|
||||
obj.SetAnnotations(annotations)
|
||||
|
||||
// For ArtifactGenerator, explicitly update Spec (OutputArtifacts and Sources)
|
||||
if ag, ok := obj.(*sourcewatcherv1beta1.ArtifactGenerator); ok {
|
||||
if existingAG, ok := existing.(*sourcewatcherv1beta1.ArtifactGenerator); ok {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.V(1).Info("updating ArtifactGenerator Spec", "name", ag.Name, "namespace", ag.Namespace,
|
||||
"outputArtifactCount", len(ag.Spec.OutputArtifacts))
|
||||
// Update Spec from obj (which contains the desired state with all OutputArtifacts)
|
||||
existingAG.Spec = ag.Spec
|
||||
// Preserve metadata updates we made above
|
||||
existingAG.SetLabels(ag.GetLabels())
|
||||
existingAG.SetAnnotations(ag.GetAnnotations())
|
||||
existingAG.SetOwnerReferences(ag.GetOwnerReferences())
|
||||
// Use existingAG for Update
|
||||
obj = existingAG
|
||||
}
|
||||
}
|
||||
|
||||
return r.Update(ctx, obj)
|
||||
}
|
||||
|
||||
// updateStatus updates PackageSource status (variants and conditions from ArtifactGenerator)
|
||||
func (r *PackageSourceReconciler) updateStatus(ctx context.Context, packageSource *cozyv1alpha1.PackageSource) error {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// Update variants in status from spec
|
||||
variantNames := make([]string, 0, len(packageSource.Spec.Variants))
|
||||
for _, variant := range packageSource.Spec.Variants {
|
||||
variantNames = append(variantNames, variant.Name)
|
||||
}
|
||||
packageSource.Status.Variants = strings.Join(variantNames, ",")
|
||||
|
||||
// Check if SourceRef is set
|
||||
if packageSource.Spec.SourceRef == nil {
|
||||
// Set status to unknown if SourceRef is not set
|
||||
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: "SourceRefNotSet",
|
||||
Message: "SourceRef is not configured",
|
||||
})
|
||||
return r.Status().Update(ctx, packageSource)
|
||||
}
|
||||
|
||||
// Get ArtifactGenerator
|
||||
ag := &sourcewatcherv1beta1.ArtifactGenerator{}
|
||||
agKey := types.NamespacedName{
|
||||
Name: packageSource.Name,
|
||||
Namespace: "cozy-system",
|
||||
}
|
||||
|
||||
if err := r.Get(ctx, agKey, ag); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
// ArtifactGenerator not found, set status to unknown
|
||||
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: "ArtifactGeneratorNotFound",
|
||||
Message: "ArtifactGenerator not found",
|
||||
})
|
||||
return r.Status().Update(ctx, packageSource)
|
||||
}
|
||||
return fmt.Errorf("failed to get ArtifactGenerator: %w", err)
|
||||
}
|
||||
|
||||
// Find Ready condition in ArtifactGenerator
|
||||
readyCondition := meta.FindStatusCondition(ag.Status.Conditions, "Ready")
|
||||
if readyCondition == nil {
|
||||
// No Ready condition in ArtifactGenerator, set status to unknown
|
||||
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: "ArtifactGeneratorNotReady",
|
||||
Message: "ArtifactGenerator Ready condition not found",
|
||||
})
|
||||
return r.Status().Update(ctx, packageSource)
|
||||
}
|
||||
|
||||
// Copy Ready condition from ArtifactGenerator to PackageSource
|
||||
meta.SetStatusCondition(&packageSource.Status.Conditions, metav1.Condition{
|
||||
Type: "Ready",
|
||||
Status: readyCondition.Status,
|
||||
Reason: readyCondition.Reason,
|
||||
Message: readyCondition.Message,
|
||||
ObservedGeneration: packageSource.Generation,
|
||||
LastTransitionTime: readyCondition.LastTransitionTime,
|
||||
})
|
||||
|
||||
logger.V(1).Info("updated PackageSource status from ArtifactGenerator",
|
||||
"packageSource", packageSource.Name,
|
||||
"status", readyCondition.Status,
|
||||
"reason", readyCondition.Reason)
|
||||
|
||||
return r.Status().Update(ctx, packageSource)
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *PackageSourceReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
Named("cozystack-packagesource").
|
||||
For(&cozyv1alpha1.PackageSource{}).
|
||||
Watches(
|
||||
&sourcewatcherv1beta1.ArtifactGenerator{},
|
||||
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
|
||||
ag, ok := obj.(*sourcewatcherv1beta1.ArtifactGenerator)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
// Find the PackageSource that owns this ArtifactGenerator by ownerReference
|
||||
for _, ownerRef := range ag.OwnerReferences {
|
||||
if ownerRef.Kind == "PackageSource" {
|
||||
return []reconcile.Request{{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: ownerRef.Name,
|
||||
},
|
||||
}}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,15 @@ kind: HelmRelease
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-system
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-bucket-application-default-bucket-system
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-bucket
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cert-manager-crds
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-cert-manager-crds
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-cert-manager-crds
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cert-manager
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-cert-manager
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-cert-manager
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -22,10 +22,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cilium
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-cilium
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-cilium
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -13,10 +13,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: coredns
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-coredns
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-coredns
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
spec:
|
||||
interval: 5m
|
||||
releaseName: csi
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-kubevirt-csi-node
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-kubevirt-csi-node
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: fluxcd-operator
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-fluxcd-operator
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-fluxcd-operator
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
@@ -51,10 +56,15 @@ metadata:
|
||||
spec:
|
||||
interval: 5m
|
||||
releaseName: fluxcd
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-fluxcd
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-fluxcd
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: gateway-api-crds
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-gateway-api-crds
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-gateway-api-crds
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: gpu-operator
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-gpu-operator
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-gpu-operator
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -27,10 +27,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: ingress-nginx
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-ingress-nginx
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-ingress-nginx
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -7,10 +7,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: metrics-server
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-metrics-server
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-metrics-server
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -10,10 +10,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cozy-monitoring-agents
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-monitoring-agents
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-monitoring-agents
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -7,10 +7,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: prometheus-operator-crds
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-prometheus-operator-crds
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-prometheus-operator-crds
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: velero
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-velero
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-velero
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -9,10 +9,15 @@ metadata:
|
||||
spec:
|
||||
interval: 5m
|
||||
releaseName: vertical-pod-autoscaler-crds
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-vertical-pod-autoscaler-crds
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-vertical-pod-autoscaler-crds
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -36,10 +36,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: vertical-pod-autoscaler
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-vertical-pod-autoscaler
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-vertical-pod-autoscaler
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -8,10 +8,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: cozy-victoria-metrics-operator
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-victoria-metrics-operator
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-victoria-metrics-operator
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -7,10 +7,15 @@ metadata:
|
||||
cozystack.io/target-cluster-name: {{ .Release.Name }}
|
||||
spec:
|
||||
releaseName: vsnap-crd
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-kubernetes-application-kubevirt-kubernetes-volumesnapshot-crd
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-vsnap-crd
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
kubeConfig:
|
||||
secretRef:
|
||||
name: {{ .Release.Name }}-admin-kubeconfig
|
||||
|
||||
@@ -35,10 +35,15 @@ kind: HelmRelease
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-system
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-nats-application-default-nats-system
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: cozy-nats
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-system
|
||||
namespace: cozy-system
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
|
||||
@@ -13,10 +13,15 @@ metadata:
|
||||
apps.cozystack.io/application.group: apps.cozystack.io
|
||||
apps.cozystack.io/application.name: etcd
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-etcd-application-default-etcd
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: etcd
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-extra
|
||||
namespace: cozy-public
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
|
||||
@@ -12,10 +12,15 @@ metadata:
|
||||
apps.cozystack.io/application.group: apps.cozystack.io
|
||||
apps.cozystack.io/application.name: info
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-info-application-default-info
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: info
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-extra
|
||||
namespace: cozy-public
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
|
||||
@@ -13,10 +13,15 @@ metadata:
|
||||
apps.cozystack.io/application.group: apps.cozystack.io
|
||||
apps.cozystack.io/application.name: ingress
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-ingress-application-default-ingress
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: ingress
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-extra
|
||||
namespace: cozy-public
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
|
||||
@@ -13,10 +13,15 @@ metadata:
|
||||
apps.cozystack.io/application.group: apps.cozystack.io
|
||||
apps.cozystack.io/application.name: monitoring
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-monitoring-application-default-monitoring
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: monitoring
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-extra
|
||||
namespace: cozy-public
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
|
||||
@@ -13,10 +13,15 @@ metadata:
|
||||
apps.cozystack.io/application.group: apps.cozystack.io
|
||||
apps.cozystack.io/application.name: seaweedfs
|
||||
spec:
|
||||
chartRef:
|
||||
kind: ExternalArtifact
|
||||
name: cozystack-seaweedfs-application-default-seaweedfs
|
||||
namespace: cozy-system
|
||||
chart:
|
||||
spec:
|
||||
chart: seaweedfs
|
||||
reconcileStrategy: Revision
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cozystack-extra
|
||||
namespace: cozy-public
|
||||
version: '>= 0.0.0-0'
|
||||
interval: 5m
|
||||
timeout: 10m
|
||||
install:
|
||||
|
||||
@@ -69,3 +69,36 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade.
|
||||
{{- end }}
|
||||
{{- $uuid }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Node Affinity for Windows VMs
|
||||
*/}}
|
||||
{{- define "virtual-machine.nodeAffinity" -}}
|
||||
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" -}}
|
||||
{{- if $configMap -}}
|
||||
{{- $dedicatedNodesForWindowsVMs := get $configMap.data "dedicatedNodesForWindowsVMs" -}}
|
||||
{{- if eq $dedicatedNodesForWindowsVMs "true" -}}
|
||||
{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}}
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
{{- if $isWindows }}
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: In
|
||||
values:
|
||||
- "true"
|
||||
{{- else }}
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: NotIn
|
||||
values:
|
||||
- "true"
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{{- if .Values.external }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@@ -7,17 +6,24 @@ metadata:
|
||||
labels:
|
||||
apps.cozystack.io/user-service: "true"
|
||||
{{- include "virtual-machine.labels" . | nindent 4 }}
|
||||
{{- if .Values.external }}
|
||||
annotations:
|
||||
networking.cozystack.io/wholeIP: "true"
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }}
|
||||
{{- if .Values.external }}
|
||||
externalTrafficPolicy: Local
|
||||
{{- if ((include "cozy-lib.network.disableLoadBalancerNodePorts" $) | fromYaml) }}
|
||||
allocateLoadBalancerNodePorts: false
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
clusterIP: None
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "virtual-machine.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
{{- if .Values.external }}
|
||||
{{- if and (eq .Values.externalMethod "WholeIP") (not .Values.externalPorts) }}
|
||||
- port: 65535
|
||||
{{- else }}
|
||||
@@ -27,4 +33,6 @@ spec:
|
||||
targetPort: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
- port: 65535
|
||||
{{- end }}
|
||||
|
||||
@@ -124,6 +124,8 @@ spec:
|
||||
|
||||
terminationGracePeriodSeconds: 30
|
||||
|
||||
{{- include "virtual-machine.nodeAffinity" . | nindent 6 }}
|
||||
|
||||
volumes:
|
||||
- name: systemdisk
|
||||
dataVolume:
|
||||
|
||||
@@ -69,3 +69,36 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade.
|
||||
{{- end }}
|
||||
{{- $uuid }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Node Affinity for Windows VMs
|
||||
*/}}
|
||||
{{- define "virtual-machine.nodeAffinity" -}}
|
||||
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" -}}
|
||||
{{- if $configMap -}}
|
||||
{{- $dedicatedNodesForWindowsVMs := get $configMap.data "dedicatedNodesForWindowsVMs" -}}
|
||||
{{- if eq $dedicatedNodesForWindowsVMs "true" -}}
|
||||
{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}}
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
{{- if $isWindows }}
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: In
|
||||
values:
|
||||
- "true"
|
||||
{{- else }}
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: scheduling.cozystack.io/vm-windows
|
||||
operator: NotIn
|
||||
values:
|
||||
- "true"
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
{{- if .Values.external }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
@@ -7,17 +6,24 @@ metadata:
|
||||
labels:
|
||||
apps.cozystack.io/user-service: "true"
|
||||
{{- include "virtual-machine.labels" . | nindent 4 }}
|
||||
{{- if .Values.external }}
|
||||
annotations:
|
||||
networking.cozystack.io/wholeIP: "true"
|
||||
{{- end }}
|
||||
spec:
|
||||
type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }}
|
||||
{{- if .Values.external }}
|
||||
externalTrafficPolicy: Local
|
||||
{{- if ((include "cozy-lib.network.disableLoadBalancerNodePorts" $) | fromYaml) }}
|
||||
allocateLoadBalancerNodePorts: false
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
clusterIP: None
|
||||
{{- end }}
|
||||
selector:
|
||||
{{- include "virtual-machine.selectorLabels" . | nindent 4 }}
|
||||
ports:
|
||||
{{- if .Values.external }}
|
||||
{{- if and (eq .Values.externalMethod "WholeIP") (not .Values.externalPorts) }}
|
||||
- port: 65535
|
||||
{{- else }}
|
||||
@@ -27,4 +33,6 @@ spec:
|
||||
targetPort: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else }}
|
||||
- port: 65535
|
||||
{{- end }}
|
||||
|
||||
@@ -95,6 +95,9 @@ spec:
|
||||
noCloud: {}
|
||||
{{- end }}
|
||||
terminationGracePeriodSeconds: 30
|
||||
|
||||
{{- include "virtual-machine.nodeAffinity" . | nindent 6 }}
|
||||
|
||||
volumes:
|
||||
{{- range .Values.disks }}
|
||||
- name: disk-{{ .name }}
|
||||
|
||||
@@ -13,10 +13,10 @@ diff:
|
||||
cozypkg show -n $(NAMESPACE) $(NAME) --plain | kubectl diff -f-
|
||||
|
||||
update:
|
||||
timoni bundle build -f flux-aio.cue > manifests/fluxcd.yaml
|
||||
yq eval '(select(.kind == "Namespace") | .metadata.labels."pod-security.kubernetes.io/enforce") = "privileged"' -i manifests/fluxcd.yaml
|
||||
sed -i manifests/fluxcd.yaml \
|
||||
timoni bundle build -f flux-aio.cue > templates/fluxcd.yaml
|
||||
yq eval '(select(.kind == "Namespace") | .metadata.labels."pod-security.kubernetes.io/enforce") = "privileged"' -i templates/fluxcd.yaml
|
||||
sed -i templates/fluxcd.yaml \
|
||||
-e '/timoni/d' \
|
||||
-e 's|\.cluster\.local\.,||g' -e 's|\.cluster\.local\,||g' -e 's|\.cluster\.local\.||g'
|
||||
# TODO: solve dns issue with hostNetwork for installing helmreleases in tenant k8s clusters
|
||||
#-e '/hostNetwork: true/i \ dnsPolicy: ClusterFirstWithHostNet'
|
||||
-e 's|\.cluster\.local\.,||g' -e 's|\.cluster\.local\,||g' -e 's|\.cluster\.local\.||g' \
|
||||
-e '/value: .svc/a \ {{- include "cozy.kubernetes_envs" . | nindent 12 }}' \
|
||||
-e '/hostNetwork: true/i \ dnsPolicy: ClusterFirstWithHostNet'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../../internal/fluxinstall/manifests
|
||||
13
packages/core/flux-aio/templates/_helpers.tpl
Normal file
13
packages/core/flux-aio/templates/_helpers.tpl
Normal file
@@ -0,0 +1,13 @@
|
||||
{{- define "cozy.kubernetes_envs" }}
|
||||
{{- $cozyDeployment := lookup "apps/v1" "Deployment" "cozy-system" "cozystack" }}
|
||||
{{- $cozyContainers := dig "spec" "template" "spec" "containers" dict $cozyDeployment }}
|
||||
{{- range $cozyContainers }}
|
||||
{{- if eq .name "cozystack" }}
|
||||
{{- range .env }}
|
||||
{{- if has .name (list "KUBERNETES_SERVICE_HOST" "KUBERNETES_SERVICE_PORT") }}
|
||||
- {{ toJson . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -11623,6 +11623,7 @@ spec:
|
||||
value: /tmp/.sigstore
|
||||
- name: NO_PROXY
|
||||
value: .svc
|
||||
{{- include "cozy.kubernetes_envs" . | nindent 12 }}
|
||||
image: ghcr.io/fluxcd/source-controller:v1.7.3
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
@@ -11693,6 +11694,7 @@ spec:
|
||||
value: /tmp/.sigstore
|
||||
- name: NO_PROXY
|
||||
value: .svc
|
||||
{{- include "cozy.kubernetes_envs" . | nindent 12 }}
|
||||
image: ghcr.io/fluxcd/kustomize-controller:v1.7.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
@@ -11758,6 +11760,7 @@ spec:
|
||||
value: /tmp/.sigstore
|
||||
- name: NO_PROXY
|
||||
value: .svc
|
||||
{{- include "cozy.kubernetes_envs" . | nindent 12 }}
|
||||
image: ghcr.io/fluxcd/helm-controller:v1.4.3
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
@@ -11820,6 +11823,7 @@ spec:
|
||||
value: /tmp/.sigstore
|
||||
- name: NO_PROXY
|
||||
value: .svc
|
||||
{{- include "cozy.kubernetes_envs" . | nindent 12 }}
|
||||
image: ghcr.io/fluxcd/notification-controller:v1.7.4
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
@@ -11890,6 +11894,7 @@ spec:
|
||||
value: /tmp/.sigstore
|
||||
- name: NO_PROXY
|
||||
value: .svc
|
||||
{{- include "cozy.kubernetes_envs" . | nindent 12 }}
|
||||
image: ghcr.io/fluxcd/source-watcher:v2.0.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
@@ -11931,6 +11936,7 @@ spec:
|
||||
name: data
|
||||
- mountPath: /tmp
|
||||
name: tmp
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
hostNetwork: true
|
||||
priorityClassName: system-cluster-critical
|
||||
securityContext:
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -20,7 +20,18 @@ diff:
|
||||
update:
|
||||
hack/gen-profiles.sh
|
||||
|
||||
image: pre-checks image-matchbox image-talos image-operator image-packages
|
||||
image: pre-checks image-matchbox image-cozystack image-talos
|
||||
|
||||
image-cozystack:
|
||||
docker buildx build -f images/cozystack/Dockerfile ../../.. \
|
||||
--tag $(REGISTRY)/installer:$(call settag,$(TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/installer:latest \
|
||||
--cache-to type=inline \
|
||||
--metadata-file images/installer.json \
|
||||
$(BUILDX_ARGS)
|
||||
IMAGE="$(REGISTRY)/installer:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/installer.json -o json -r)" \
|
||||
yq -i '.cozystack.image = strenv(IMAGE)' values.yaml
|
||||
rm -f images/installer.json
|
||||
|
||||
image-talos:
|
||||
test -f ../../../_out/assets/installer-amd64.tar || make talos-installer
|
||||
@@ -47,31 +58,3 @@ talos-initramfs talos-kernel talos-installer talos-iso talos-nocloud talos-metal
|
||||
cat images/talos/profiles/$(subst talos-,,$@).yaml | \
|
||||
docker run --rm -i -v /dev:/dev --privileged "ghcr.io/siderolabs/imager:$(TALOS_VERSION)" --tar-to-stdout - | \
|
||||
tar -C ../../../_out/assets -xzf-
|
||||
|
||||
update-version:
|
||||
TAG="$(call settag,$(TAG))" \
|
||||
yq -i '.cozystackOperator.cozystackVersion = strenv(TAG)' values.yaml
|
||||
|
||||
image-operator:
|
||||
docker buildx build -f images/cozystack-operator/Dockerfile ../../.. \
|
||||
--tag $(REGISTRY)/cozystack-operator:$(call settag,$(TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/cozystack-operator:latest \
|
||||
--cache-to type=inline \
|
||||
--metadata-file images/cozystack-operator.json \
|
||||
$(BUILDX_ARGS)
|
||||
IMAGE="$(REGISTRY)/cozystack-operator:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/cozystack-operator.json -o json -r)" \
|
||||
yq -i '.cozystackOperator.image = strenv(IMAGE)' values.yaml
|
||||
rm -f images/cozystack-operator.json
|
||||
|
||||
|
||||
image-packages: update-version
|
||||
mkdir -p ../../../_out/assets images
|
||||
flux push artifact \
|
||||
oci://$(REGISTRY)/platform-packages:$(call settag,$(TAG)) \
|
||||
--path=../../../packages \
|
||||
--source=https://github.com/cozystack/cozystack \
|
||||
--revision="$$(git describe --tags):$$(git rev-parse HEAD)" \
|
||||
2>&1 | tee images/cozystack-packages.log
|
||||
export REPO="oci://$(REGISTRY)/platform-packages"; \
|
||||
export DIGEST=$$(awk -F@ '/artifact successfully pushed/ {print $$2}' images/cozystack-packages.log; rm -f images/cozystack-packages.log); \
|
||||
test -n "$$DIGEST" && yq -i '.cozystackOperator.platformSource = (strenv(REPO) + "@" + strenv(DIGEST))' values.yaml
|
||||
|
||||
1
packages/core/installer/definitions/.gitattributes
vendored
Normal file
1
packages/core/installer/definitions/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.yaml linguist-generated
|
||||
@@ -57,7 +57,7 @@ spec:
|
||||
properties:
|
||||
components:
|
||||
additionalProperties:
|
||||
description: PackageRelease defines overrides for a specific component
|
||||
description: PackageComponent defines overrides for a specific component
|
||||
properties:
|
||||
enabled:
|
||||
description: |-
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
kind: Package
|
||||
metadata:
|
||||
name: cozystack.cozystack-platform
|
||||
spec:
|
||||
variant: isp-full
|
||||
components:
|
||||
platform:
|
||||
values:
|
||||
publishing:
|
||||
host: "dev5.infra.aenix.org"
|
||||
apiServerEndpoint: "https://api.dev5.infra.aenix.org"
|
||||
externalIPs:
|
||||
- 10.4.0.94
|
||||
- 10.4.0.179
|
||||
- 10.4.0.26
|
||||
authentication:
|
||||
oidc:
|
||||
enabled: true
|
||||
@@ -1,23 +0,0 @@
|
||||
FROM golang:1.25-alpine as builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk add --no-cache make git
|
||||
|
||||
COPY . /src/
|
||||
WORKDIR /src
|
||||
|
||||
RUN go mod download
|
||||
|
||||
# Build cozystack-operator
|
||||
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
|
||||
-ldflags="-w -s" \
|
||||
-o /cozystack-operator \
|
||||
./cmd/cozystack-operator
|
||||
|
||||
FROM alpine:3.22
|
||||
|
||||
COPY --from=builder /cozystack-operator /usr/bin/cozystack-operator
|
||||
|
||||
ENTRYPOINT ["/usr/bin/cozystack-operator"]
|
||||
@@ -1,12 +0,0 @@
|
||||
# Exclude everything except src directory
|
||||
*
|
||||
!src/**
|
||||
!api/**
|
||||
!cmd/**
|
||||
!hack/**
|
||||
!internal/**
|
||||
!packages/**
|
||||
!pkg/**
|
||||
!scripts/**
|
||||
!go.mod
|
||||
!go.sum
|
||||
41
packages/core/installer/images/cozystack/Dockerfile
Normal file
41
packages/core/installer/images/cozystack/Dockerfile
Normal file
@@ -0,0 +1,41 @@
|
||||
FROM golang:1.24-alpine AS k8s-await-election-builder
|
||||
|
||||
ARG K8S_AWAIT_ELECTION_GITREPO=https://github.com/LINBIT/k8s-await-election
|
||||
ARG K8S_AWAIT_ELECTION_VERSION=0.4.1
|
||||
|
||||
# TARGETARCH is a docker special variable: https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk add --no-cache git make
|
||||
RUN git clone ${K8S_AWAIT_ELECTION_GITREPO} /usr/local/go/k8s-await-election/ \
|
||||
&& cd /usr/local/go/k8s-await-election \
|
||||
&& git reset --hard v${K8S_AWAIT_ELECTION_VERSION} \
|
||||
&& make \
|
||||
&& mv ./out/k8s-await-election-${TARGETARCH} /k8s-await-election
|
||||
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN apk add --no-cache make git
|
||||
RUN apk add helm --repository=https://dl-cdn.alpinelinux.org/alpine/edge/community
|
||||
|
||||
COPY . /src/
|
||||
WORKDIR /src
|
||||
|
||||
RUN go mod download
|
||||
|
||||
FROM alpine:3.22
|
||||
|
||||
RUN wget -O- https://github.com/cozystack/cozypkg/raw/refs/heads/main/hack/install.sh | sh -s -- -v 1.2.0
|
||||
|
||||
RUN apk add --no-cache make kubectl helm coreutils git jq openssl
|
||||
|
||||
COPY --from=builder /src/scripts /cozystack/scripts
|
||||
COPY --from=builder /src/packages/core /cozystack/packages/core
|
||||
COPY --from=builder /src/packages/system /cozystack/packages/system
|
||||
COPY --from=k8s-await-election-builder /k8s-await-election /usr/bin/k8s-await-election
|
||||
|
||||
WORKDIR /cozystack
|
||||
ENTRYPOINT ["/usr/bin/k8s-await-election", "/cozystack/scripts/installer.sh" ]
|
||||
@@ -0,0 +1 @@
|
||||
_out
|
||||
@@ -1,117 +0,0 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: cozy-system
|
||||
labels:
|
||||
cozystack.io/system: "true"
|
||||
pod-security.kubernetes.io/enforce: privileged
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: cozystack
|
||||
namespace: cozy-system
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: cozystack
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: cozystack
|
||||
namespace: cozy-system
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: cozystack-operator
|
||||
namespace: cozy-system
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: cozystack-operator
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: cozystack-operator
|
||||
spec:
|
||||
serviceAccountName: cozystack
|
||||
containers:
|
||||
- name: cozystack-operator
|
||||
image: "{{ .Values.cozystackOperator.image }}"
|
||||
args:
|
||||
- --leader-elect=true
|
||||
- --install-flux=true
|
||||
- --metrics-bind-address=0
|
||||
- --health-probe-bind-address=
|
||||
- --cozystack-version={{ .Values.cozystackOperator.cozystackVersion }}
|
||||
{{- if .Values.cozystackOperator.disableTelemetry }}
|
||||
- --disable-telemetry
|
||||
{{- end }}
|
||||
- --platform-source-name=cozystack-platform
|
||||
- --platform-source={{ .Values.cozystackOperator.platformSource }}
|
||||
env:
|
||||
- name: KUBERNETES_SERVICE_HOST
|
||||
value: localhost
|
||||
- name: KUBERNETES_SERVICE_PORT
|
||||
value: "7445"
|
||||
hostNetwork: true
|
||||
tolerations:
|
||||
- key: "node.kubernetes.io/not-ready"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
- key: "node.cilium.io/agent-not-ready"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
---
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
kind: PackageSource
|
||||
metadata:
|
||||
name: cozystack.cozystack-platform
|
||||
spec:
|
||||
sourceRef:
|
||||
kind: OCIRepository
|
||||
name: cozystack-packages
|
||||
namespace: cozy-system
|
||||
path: /
|
||||
variants:
|
||||
- name: default
|
||||
components:
|
||||
- install:
|
||||
namespace: cozy-system
|
||||
releaseName: cozystack-platform
|
||||
name: cozystack-platform
|
||||
path: core/platform
|
||||
valuesFiles:
|
||||
- values.yaml
|
||||
- name: isp-full
|
||||
components:
|
||||
- install:
|
||||
namespace: cozy-system
|
||||
releaseName: cozystack-platform
|
||||
name: cozystack-platform
|
||||
path: core/platform
|
||||
valuesFiles:
|
||||
- values.yaml
|
||||
- values-isp-full.yaml
|
||||
- name: isp-hosted
|
||||
components:
|
||||
- install:
|
||||
namespace: cozy-system
|
||||
releaseName: cozystack-platform
|
||||
name: cozystack-platform
|
||||
path: core/platform
|
||||
valuesFiles:
|
||||
- values.yaml
|
||||
- values-isp-hosted.yaml
|
||||
79
packages/core/installer/templates/cozystack.yaml
Normal file
79
packages/core/installer/templates/cozystack.yaml
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: cozy-system
|
||||
labels:
|
||||
cozystack.io/system: "true"
|
||||
pod-security.kubernetes.io/enforce: privileged
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: cozystack
|
||||
namespace: cozy-system
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: cozystack
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: cozystack
|
||||
namespace: cozy-system
|
||||
roleRef:
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: cozystack
|
||||
namespace: cozy-system
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: cozystack
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 0
|
||||
maxUnavailable: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: cozystack
|
||||
spec:
|
||||
hostNetwork: true
|
||||
serviceAccountName: cozystack
|
||||
containers:
|
||||
- name: cozystack
|
||||
image: "{{ .Values.cozystack.image }}"
|
||||
env:
|
||||
- name: KUBERNETES_SERVICE_HOST
|
||||
value: localhost
|
||||
- name: INSTALL_FLUX
|
||||
value: "true"
|
||||
- name: KUBERNETES_SERVICE_PORT
|
||||
value: "7445"
|
||||
- name: K8S_AWAIT_ELECTION_ENABLED
|
||||
value: "1"
|
||||
- name: K8S_AWAIT_ELECTION_NAME
|
||||
value: cozystack
|
||||
- name: K8S_AWAIT_ELECTION_LOCK_NAME
|
||||
value: cozystack
|
||||
- name: K8S_AWAIT_ELECTION_LOCK_NAMESPACE
|
||||
value: cozy-system
|
||||
- name: K8S_AWAIT_ELECTION_IDENTITY
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
tolerations:
|
||||
- key: "node.kubernetes.io/not-ready"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
- key: "node.cilium.io/agent-not-ready"
|
||||
operator: "Exists"
|
||||
effect: "NoSchedule"
|
||||
@@ -1,5 +1,5 @@
|
||||
{{/*
|
||||
{{- range $path, $_ := .Files.Glob "crds/*.yaml" }}
|
||||
{{- range $path, $_ := .Files.Glob "definitions/*.yaml" }}
|
||||
---
|
||||
{{ $.Files.Get $path }}
|
||||
{{- end }}
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
cozystack:
|
||||
image: ghcr.io/cozystack/cozystack/installer:v0.38.2@sha256:9ff92b655de6f9bea3cba4cd42dcffabd9aace6966dcfb1cc02dda2420ea4a15
|
||||
cozystackOperator:
|
||||
image: ghcr.io/cozystack/cozystack/cozystack-operator:latest@sha256:f7f6e0fd9e896b7bfa642d0bfa4378bc14e646bc5c2e86e2e09a82770ef33181
|
||||
platformSource: 'oci://ghcr.io/cozystack/cozystack/platform-packages@sha256:0576491291b33936cdf770a5c5b5692add97339c1505fc67a92df9d69dfbfdf6'
|
||||
cozystackVersion: latest
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
NAME=cozystack-platform
|
||||
NAME=platform
|
||||
NAMESPACE=cozy-system
|
||||
|
||||
include ../../../scripts/common-envs.mk
|
||||
include ../../../scripts/package.mk
|
||||
|
||||
image: image-migrations
|
||||
show:
|
||||
cozypkg show -n $(NAMESPACE) $(NAME) --plain
|
||||
|
||||
image-migrations:
|
||||
docker buildx build images/migrations \
|
||||
--tag $(REGISTRY)/platform-migrations:$(call settag,$(TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/platform-migrations:latest \
|
||||
apply:
|
||||
cozypkg show -n $(NAMESPACE) $(NAME) --plain | kubectl apply -f-
|
||||
kubectl delete helmreleases.helm.toolkit.fluxcd.io -l cozystack.io/marked-for-deletion=true -A
|
||||
|
||||
reconcile: apply
|
||||
|
||||
namespaces-show:
|
||||
cozypkg show -n $(NAMESPACE) $(NAME) --plain -s templates/namespaces.yaml
|
||||
|
||||
namespaces-apply:
|
||||
cozypkg show -n $(NAMESPACE) $(NAME) --plain -s templates/namespaces.yaml | kubectl apply -f-
|
||||
|
||||
diff:
|
||||
cozypkg show -n $(NAMESPACE) $(NAME) --plain | kubectl diff -f-
|
||||
|
||||
image: image-assets
|
||||
image-assets:
|
||||
docker buildx build -f images/cozystack-assets/Dockerfile ../../.. \
|
||||
--tag $(REGISTRY)/cozystack-assets:$(call settag,$(TAG)) \
|
||||
--cache-from type=registry,ref=$(REGISTRY)/cozystack-assets:latest \
|
||||
--cache-to type=inline \
|
||||
--metadata-file images/migrations.json \
|
||||
--metadata-file images/cozystack-assets.json \
|
||||
$(BUILDX_ARGS)
|
||||
IMAGE="$(REGISTRY)/platform-migrations:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/migrations.json -o json -r)" \
|
||||
yq -i '.migrations.image = strenv(IMAGE)' values.yaml
|
||||
rm -f images/migrations.json
|
||||
|
||||
generate:
|
||||
@echo "Generating target version from migrations..."
|
||||
@LAST_MIGRATION=$$(ls -1 images/migrations/migrations/ 2>/dev/null | grep -E '^[0-9]+$$' | sort -n | tail -1); \
|
||||
if [ -z "$$LAST_MIGRATION" ]; then \
|
||||
echo "Error: No migration files found" >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
TARGET_VERSION=$$((LAST_MIGRATION + 1)); \
|
||||
echo "Last migration: $$LAST_MIGRATION, Target version: $$TARGET_VERSION"; \
|
||||
yq -i '.migrations.targetVersion = $$TARGET_VERSION' values.yaml; \
|
||||
echo "Updated targetVersion to $$TARGET_VERSION in values.yaml"
|
||||
IMAGE="$(REGISTRY)/cozystack-assets:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/cozystack-assets.json -o json -r)" \
|
||||
yq -i '.assets.image = strenv(IMAGE)' values.yaml
|
||||
rm -f images/cozystack-assets.json
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.24-alpine AS builder
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
FROM alpine:3.22
|
||||
|
||||
RUN wget -O- https://github.com/cozystack/cozypkg/raw/refs/heads/main/hack/install.sh | sh -s -- -v 1.4.0
|
||||
|
||||
RUN apk add --no-cache kubectl helm coreutils git jq ca-certificates bash curl
|
||||
|
||||
COPY migrations /migrations
|
||||
COPY run-migrations.sh /usr/bin/run-migrations.sh
|
||||
|
||||
WORKDIR /migrations
|
||||
|
||||
ENTRYPOINT ["/usr/bin/run-migrations.sh"]
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 1 --> 2
|
||||
|
||||
# Fix mariadb-operator secrets
|
||||
if kubectl get -n cozy-mariadb-operator secret/mariadb-operator-webhook-cert; then
|
||||
kubectl annotate -n cozy-mariadb-operator secret/mariadb-operator-webhook-cert meta.helm.sh/release-namespace=cozy-mariadb-operator meta.helm.sh/release-name=mariadb-operator
|
||||
kubectl label -n cozy-mariadb-operator secret/mariadb-operator-webhook-cert app.kubernetes.io/managed-by=Helm
|
||||
fi
|
||||
|
||||
# Gratefully remove fluxcd release and keep resources
|
||||
if kubectl get hr -n cozy-fluxcd cozy-fluxcd 2>/dev/null; then
|
||||
kubectl patch hr -n cozy-fluxcd cozy-fluxcd -p '{"spec": {"suspend": true}, "metadata": {"finalizers": null}}' --type=merge
|
||||
kubectl delete hr -n cozy-fluxcd cozy-fluxcd
|
||||
fi
|
||||
kubectl delete secret -n cozy-fluxcd -l name=fluxcd
|
||||
|
||||
# Fix kubeovn crds
|
||||
kubeovn_crds=$(kubectl get crd -o name | grep '\.kubeovn\.io$')
|
||||
if [ -n "$kubeovn_crds" ]; then
|
||||
kubectl annotate $kubeovn_crds meta.helm.sh/release-namespace=cozy-kubeovn meta.helm.sh/release-name=kubeovn
|
||||
kubectl label $kubeovn_crds app.kubernetes.io/managed-by=Helm
|
||||
fi
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=2 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 10 --> 11
|
||||
|
||||
# Force reconcile hr keycloak-configure
|
||||
if kubectl get helmrelease keycloak-configure -n cozy-keycloak; then
|
||||
kubectl delete po -l app=source-controller -n cozy-fluxcd
|
||||
timestamp=$(date --rfc-3339=ns)
|
||||
kubectl annotate helmrelease keycloak-configure -n cozy-keycloak \
|
||||
reconcile.fluxcd.io/forceAt="$timestamp" \
|
||||
reconcile.fluxcd.io/requestedAt="$timestamp" \
|
||||
--overwrite
|
||||
fi
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=11 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 11 --> 12
|
||||
|
||||
# Recreate daemonset kube-rbac-proxy
|
||||
|
||||
if kubectl get daemonset kube-rbac-proxy -n cozy-monitoring; then
|
||||
kubectl delete daemonset kube-rbac-proxy --cascade=orphan -n cozy-monitoring
|
||||
fi
|
||||
|
||||
if kubectl get helmrelease monitoring-agents -n cozy-monitoring; then
|
||||
timestamp=$(date --rfc-3339=ns)
|
||||
kubectl annotate helmrelease monitoring-agents -n cozy-monitoring \
|
||||
reconcile.fluxcd.io/forceAt="$timestamp" \
|
||||
reconcile.fluxcd.io/requestedAt="$timestamp" \
|
||||
--overwrite
|
||||
fi
|
||||
|
||||
kubectl delete pods -l app.kubernetes.io/component=kube-rbac-proxy -n cozy-monitoring
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=12 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 12 --> 13
|
||||
|
||||
# Copy configuration from ingress to cozystack configmap
|
||||
if kubectl get hr -n tenant-root tenant-root > /dev/null; then
|
||||
expose_services=$(
|
||||
kubectl get hr -n tenant-root ingress -o go-template='{{ with .spec }}{{ with .values }}{{ if .dashboard }}dashboard,{{ end }}{{ if .cdiUploadProxy }}cdi-uploadproxy,{{ end }}{{ if .virtExportProxy }}vm-exportproxy,{{ end }}{{ end }}{{ end }}'
|
||||
)
|
||||
expose_services=$(echo "$expose_services" | awk '{sub(/,$/,""); print}')
|
||||
|
||||
expose_external_ips=$(
|
||||
kubectl get hr -n tenant-root ingress -o go-template='{{ with .spec }}{{ with .values }}{{ if .externalIPs }}{{ range .externalIPs }}{{ . }},{{ end }}{{ end }}{{ end }}{{ end }}'
|
||||
)
|
||||
expose_external_ips=$(echo "$expose_external_ips" | awk '{sub(/,$/,""); print}')
|
||||
|
||||
existing_expose_external_ips=$(kubectl get cm -n cozy-system cozystack -o go-template='{{ index .data "expose-external-ips" }}')
|
||||
existing_expose_services=$(kubectl get cm -n cozy-system cozystack -o go-template='{{ index .data "expose-services" }}')
|
||||
|
||||
if [ "$existing_expose_external_ips" == "<no value>" ]; then
|
||||
kubectl patch cm -n cozy-system cozystack --type merge -p="{\"data\":{\"expose-external-ips\":\"$expose_external_ips\"}}"
|
||||
fi
|
||||
|
||||
if [ "$existing_expose_services" == "<no value>" ]; then
|
||||
kubectl patch cm -n cozy-system cozystack --type merge -p="{\"data\":{\"expose-services\":\"$expose_services\"}}"
|
||||
fi
|
||||
|
||||
kubectl patch hr -n tenant-root ingress --type json -p='[{"op": "remove", "path": "/spec/values/dashboard"}]' || true
|
||||
kubectl patch hr -n tenant-root ingress --type json -p='[{"op": "remove", "path": "/spec/values/cdiUploadProxy"}]' || true
|
||||
kubectl patch hr -n tenant-root ingress --type json -p='[{"op": "remove", "path": "/spec/values/virtExportProxy"}]' || true
|
||||
kubectl patch hr -n tenant-root ingress --type json -p='[{"op": "remove", "path": "/spec/values/externalIPs"}]' || true
|
||||
kubectl patch hr -n tenant-root ingress --type merge -p='{"spec":{"chart":{"spec":{"version":"1.6.0"}}}}'
|
||||
fi
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=13 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 13 --> 14
|
||||
|
||||
# Upgrade tenants.apps to new chart version
|
||||
kubectl get tenants.apps.cozystack.io -A --no-headers --output=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name' | while read NAMESPACE NAME; do
|
||||
kubectl patch tenants.apps.cozystack.io -n "$NAMESPACE" "$NAME" --type merge -p '{"appVersion":"1.10.0"}'
|
||||
done
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=14 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 14 --> 15
|
||||
|
||||
# Delete the `capi-providers` HelmRelease in the `cozy-cluster-api` namespace if present
|
||||
if kubectl get hr -n cozy-cluster-api capi-providers >/dev/null 2>&1; then
|
||||
kubectl delete hr -n cozy-cluster-api capi-providers
|
||||
fi
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=15 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 15 --> 16
|
||||
|
||||
if kubectl get validatingwebhookconfigurations.admissionregistration.k8s.io kamaji-validating-webhook-configuration; then
|
||||
kubectl delete validatingwebhookconfigurations.admissionregistration.k8s.io kamaji-validating-webhook-configuration
|
||||
fi
|
||||
kubectl get kamajicontrolplane -A -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,VERSION:.spec.version,VERSION:.status.version --no-headers |
|
||||
while read namespace name version status; do
|
||||
if [ "$status" = "v1.32.4" ]; then
|
||||
continue
|
||||
fi
|
||||
(set -x; kubectl patch kamajicontrolplane "$name" -n "$namespace" --type merge -p '{"spec":{"version":"1.32.4"}}')
|
||||
(set -x; kubectl patch kamajicontrolplane "$name" -n "$namespace" --type merge -p '{"status":{"version":"v1.32.4"}}' --subresource status)
|
||||
(set -x; kubectl patch tcp "$name" -n "$namespace" --type merge -p '{"spec":{"kubernetes":{"version":"1.32.4"}}}')
|
||||
(set -x; kubectl patch tcp "$name" -n "$namespace" --type merge -p '{"status":{"kubernetesResources":{"version":{"version":"v1.32.4"}}}} ' --subresource status)
|
||||
done
|
||||
|
||||
# Upgrade kubernetes.apps to new chart version
|
||||
kubectl get kuberneteses.apps.cozystack.io -A --no-headers --output=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name' | while read NAMESPACE NAME; do
|
||||
kubectl patch kuberneteses.apps.cozystack.io -n "$NAMESPACE" "$NAME" --type merge -p '{"appVersion":"0.24.0"}'
|
||||
done
|
||||
|
||||
if kubectl get helmrelease kamaji -n cozy-kamaji; then
|
||||
cozypkg reconcile kamaji -n cozy-kamaji --force
|
||||
fi
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=16 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,95 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 16 --> 17
|
||||
#
|
||||
# fix-nested-resources-map.sh – prints kubectl patch commands.
|
||||
# * Replaces each resources section with {cpu,memory} merged from requests + limits
|
||||
# * Adds/Replaces .appVersion with "*"
|
||||
|
||||
set -e
|
||||
|
||||
CRDS='
|
||||
clickhouses.apps.cozystack.io
|
||||
etcds.apps.cozystack.io
|
||||
ferretdb.apps.cozystack.io
|
||||
httpcaches.apps.cozystack.io
|
||||
kafkas.apps.cozystack.io
|
||||
kuberneteses.apps.cozystack.io
|
||||
monitorings.apps.cozystack.io
|
||||
mysqls.apps.cozystack.io
|
||||
natses.apps.cozystack.io
|
||||
postgreses.apps.cozystack.io
|
||||
rabbitmqs.apps.cozystack.io
|
||||
redises.apps.cozystack.io
|
||||
seaweedfses.apps.cozystack.io
|
||||
tcpbalancers.apps.cozystack.io
|
||||
virtualmachines.apps.cozystack.io
|
||||
vminstances.apps.cozystack.io
|
||||
vpns.apps.cozystack.io
|
||||
'
|
||||
|
||||
for KIND in $CRDS; do
|
||||
kubectl get "$KIND" -A -o json | jq -r --arg kind "$KIND" '
|
||||
.items[]
|
||||
| . as $obj
|
||||
| ($obj.metadata.namespace // "") as $ns # namespace (empty string for cluster-scoped)
|
||||
| $obj.metadata.name as $name # object name
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Build an array with every JSON path ending with "resources"
|
||||
# -------------------------------------------------------------------------
|
||||
| [ $obj
|
||||
| paths
|
||||
| select(.[-1] == "resources")
|
||||
] as $rpaths
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Iterate through each resources path
|
||||
# -------------------------------------------------------------------------
|
||||
| foreach $rpaths[] as $rpath (null;
|
||||
# Current resources object
|
||||
($obj | getpath($rpath)) as $res
|
||||
|
||||
# requests + limits merged; requests override limits on key collision
|
||||
| ($res.requests? // {}) as $req
|
||||
| ($res.limits? // {}) as $lim
|
||||
| ($req + $lim) as $flat
|
||||
|
||||
# Keep cpu & memory only
|
||||
| ($flat
|
||||
| with_entries(select(.key|test("^(cpu|memory)$")))
|
||||
) as $value
|
||||
| select(($value|length) > 0) # skip if nothing to patch
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# RFC6901-encoded JSON Pointer to the resources section
|
||||
# ---------------------------------------------------------------------
|
||||
| ("/" + ($rpath
|
||||
| map(
|
||||
tostring
|
||||
| gsub("~";"~0")
|
||||
| gsub("/";"~1")
|
||||
)
|
||||
| join("/")
|
||||
)
|
||||
) as $pointer
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Compose JSON Patch: 1) add/replace appVersion, 2) replace resources
|
||||
# ---------------------------------------------------------------------
|
||||
| [
|
||||
{ op:"add", path:"/appVersion", value:"*" },
|
||||
{ op:"replace", path:$pointer, value:$value }
|
||||
] as $patch
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Print one ready-to-run kubectl patch command
|
||||
# ---------------------------------------------------------------------
|
||||
| "kubectl " +
|
||||
(if $ns != "" then "-n \($ns) " else "" end) +
|
||||
"patch \($kind) \($name) --type=json -p '\''\($patch|tojson)'\''"
|
||||
)
|
||||
' | sh -ex
|
||||
done
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=17 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 17 --> 18
|
||||
|
||||
# Upgrade kubernetes.apps to new chart version
|
||||
kubectl get kuberneteses.apps.cozystack.io -A --no-headers --output=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name' | while read NAMESPACE NAME; do
|
||||
kubectl patch kuberneteses.apps.cozystack.io -n "$NAMESPACE" "$NAME" --type merge -p '{"appVersion":"0.26.1"}'
|
||||
done
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=18 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 18 --> 19
|
||||
|
||||
# Upgrade tenants.apps to new chart version
|
||||
kubectl get tenants.apps.cozystack.io -A --no-headers --output=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name' | while read NAMESPACE NAME; do
|
||||
kubectl patch tenants.apps.cozystack.io -n "$NAMESPACE" "$NAME" --type merge -p '{"appVersion":"1.13.0"}'
|
||||
done
|
||||
|
||||
# Upgrade virtualmachines.apps to new chart version
|
||||
kubectl get virtualmachines.apps.cozystack.io -A --no-headers --output=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name' | while read NAMESPACE NAME; do
|
||||
kubectl patch virtualmachines.apps.cozystack.io -n "$NAMESPACE" "$NAME" --type merge -p '{"appVersion":"0.14.0"}'
|
||||
done
|
||||
kubectl get vminstances.apps.cozystack.io -A --no-headers --output=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name' | while read NAMESPACE NAME; do
|
||||
kubectl patch vminstances.apps.cozystack.io -n "$NAMESPACE" "$NAME" --type merge -p '{"appVersion":"0.12.0"}'
|
||||
done
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=19 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,53 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 19 --> 20
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
kubectl get helmreleases.helm.toolkit.fluxcd.io -A \
|
||||
--field-selector=metadata.name=seaweedfs -o json | jq -r '
|
||||
.items[] as $o
|
||||
| ($o.metadata.namespace // "") as $ns
|
||||
| $o.metadata.name as $name
|
||||
| $o.spec as $s
|
||||
| $o.spec.values as $v
|
||||
|
||||
| (
|
||||
($s.chart.spec.version? // "") != "0.7.0"
|
||||
or ($v.size? != null)
|
||||
or ($v.storageClass? != null)
|
||||
or ($v.replicas? != null)
|
||||
or ($v.zones? != null)
|
||||
) as $needsChange
|
||||
| select($needsChange)
|
||||
|
||||
# JSON Patch
|
||||
| [
|
||||
(if $s.chart.spec.version? then
|
||||
{op:"replace", path:"/spec/chart/spec/version", value:"0.7.0"}
|
||||
else
|
||||
{op:"add", path:"/spec/chart/spec/version", value:"0.7.0"}
|
||||
end),
|
||||
|
||||
(if ($v|type) != "object" then {op:"add", path:"/spec/values", value:{}} else empty end),
|
||||
|
||||
(if ($v.volume?|type) != "object" then {op:"add", path:"/spec/values/volume", value:{}} else empty end),
|
||||
|
||||
(if $v.size? then {op:"add", path:"/spec/values/volume/size", value:$v.size} else empty end),
|
||||
(if $v.storageClass? then {op:"add", path:"/spec/values/volume/storageClass", value:$v.storageClass} else empty end),
|
||||
(if $v.replicas? then {op:"add", path:"/spec/values/volume/replicas", value:$v.replicas} else empty end),
|
||||
(if $v.zones? then {op:"add", path:"/spec/values/volume/zones", value:$v.zones} else empty end),
|
||||
|
||||
(if $v.size? then {op:"remove", path:"/spec/values/size"} else empty end),
|
||||
(if $v.storageClass? then {op:"remove", path:"/spec/values/storageClass"} else empty end),
|
||||
(if $v.replicas? then {op:"remove", path:"/spec/values/replicas"} else empty end),
|
||||
(if $v.zones? then {op:"remove", path:"/spec/values/zones"} else empty end)
|
||||
] as $patch
|
||||
|
||||
| "kubectl " +
|
||||
(if $ns != "" then "-n \($ns) " else "" end) +
|
||||
"patch helmreleases.helm.toolkit.fluxcd.io \($name) --type=json -p '\''\($patch|tojson)'\''"
|
||||
' | sh -ex
|
||||
|
||||
# Stamp version
|
||||
kubectl create configmap -n cozy-system cozystack-version \
|
||||
--from-literal=version=20 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 2 --> 3
|
||||
|
||||
if [ -d packages/system ]; then
|
||||
kubectl apply -f packages/system/mariadb-operator/charts/mariadb-operator/crds/crds.yaml --server-side --force-conflicts
|
||||
fi
|
||||
|
||||
# Fix mariadb-operator crds
|
||||
mariadb_crds=$(kubectl get crd -o name | grep '\.k8s\.mariadb\.com$')
|
||||
if [ -n "$mariadb_crds" ]; then
|
||||
kubectl annotate $mariadb_crds meta.helm.sh/release-namespace=cozy-mariadb-operator meta.helm.sh/release-name=mariadb-operator
|
||||
kubectl label $mariadb_crds app.kubernetes.io/managed-by=Helm
|
||||
fi
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=3 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 20 --> 21
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
kubectl -n cozy-system delete helmrelease cozystack-api \
|
||||
|| kubectl -n cozy-system delete deployment cozystack-api --ignore-not-found # fallback to help migration progress if it failed first time
|
||||
while [ $( kubectl -n cozy-system get po -l app=cozystack-api --no-headers | wc -l ) != 0 ] ;
|
||||
do
|
||||
echo "Waiting for Cozystack API pods to be deleted";
|
||||
sleep 1
|
||||
done
|
||||
|
||||
kubectl -n cozy-system delete helmrelease cozystack-controller \
|
||||
|| kubectl -n cozy-system delete deployment cozystack-controller --ignore-not-found # same fallback
|
||||
kubectl delete customresourcedefinitions.apiextensions.k8s.io cozystackresourcedefinitions.cozystack.io --ignore-not-found
|
||||
while [ $( kubectl -n cozy-system get po -l app=cozystack-controller --no-headers | wc -l ) != 0 ] ;
|
||||
do
|
||||
echo "Waiting for Cozystack controller pods to be deleted";
|
||||
sleep 1
|
||||
done
|
||||
|
||||
kubectl delete helmrelease -n cozy-dashboard dashboard --ignore-not-found
|
||||
sleep 5
|
||||
if [ -d "packages/system" ]; then
|
||||
cozypkg -n cozy-system -C packages/system/cozystack-resource-definition-crd apply cozystack-resource-definition-crd --plain
|
||||
cozypkg -n cozy-system -C packages/system/cozystack-resource-definitions apply cozystack-resource-definitions --plain
|
||||
cozypkg -n cozy-system -C packages/system/cozystack-api apply cozystack-api --plain
|
||||
cozypkg -n cozy-system -C packages/system/cozystack-controller/ apply cozystack-api --plain --take-ownership
|
||||
cozypkg -n cozy-system -C packages/system/lineage-controller-webhook/ apply cozystack-api --plain --take-ownership
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
echo "Running lineage-webhook test"
|
||||
kubectl delete ns cozy-lineage-webhook-test --ignore-not-found && kubectl create ns cozy-lineage-webhook-test
|
||||
cleanup_test_ns() {
|
||||
kubectl delete ns cozy-lineage-webhook-test --ignore-not-found
|
||||
}
|
||||
trap cleanup_test_ns ERR
|
||||
timeout 60 sh -c 'until kubectl -n cozy-lineage-webhook-test create service clusterip lineage-webhook-test --clusterip="None" --dry-run=server; do sleep 1; done'
|
||||
cleanup_test_ns
|
||||
|
||||
timestamp=$(date --rfc-3339=ns || date)
|
||||
kubectl get namespace -o custom-columns=NAME:.metadata.name --no-headers |
|
||||
grep '^tenant-' |
|
||||
while read namespace ; do
|
||||
(set -x; \
|
||||
kubectl annotate \
|
||||
pods,services,pvc,secrets,ingresses.networking.k8s.io,workloadmonitors.cozystack.io \
|
||||
-n "$namespace" --all \
|
||||
migration.cozystack.io="$timestamp" --overwrite || true)
|
||||
done
|
||||
|
||||
# Stamp version
|
||||
kubectl create configmap -n cozy-system cozystack-version \
|
||||
--from-literal=version=21 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 21 --> 22
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Delete old cozystack deployment
|
||||
kubectl -n cozy-system delete deployment cozystack --ignore-not-found
|
||||
while [ $( kubectl -n cozy-system get po -l app=cozystack --no-headers 2>/dev/null | wc -l ) != 0 ] ;
|
||||
do
|
||||
echo "Waiting for old cozystack deployment pods to be deleted";
|
||||
sleep 1
|
||||
done
|
||||
|
||||
kubectl delete hr -n cozy-system cozystack-resource-definitions --ignore-not-found
|
||||
kubectl delete hr -n cozy-system cozystack-resource-definition-crd --ignore-not-found
|
||||
kubectl delete crd cozystackresourcedefinitions.cozycloud.io --ignore-not-found
|
||||
kubectl delete hr -n cozy-fluxcd fluxcd --ignore-not-found
|
||||
|
||||
# Stamp version
|
||||
kubectl create configmap -n cozy-system cozystack-version \
|
||||
--from-literal=version=22 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,161 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 22 --> 23
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "Migrating HelmReleases: adding application labels for tenant-* namespaces"
|
||||
|
||||
# Function to determine application type from HelmRelease name
|
||||
determine_app_type() {
|
||||
local name="$1"
|
||||
local app_kind=""
|
||||
local app_name=""
|
||||
|
||||
# Try to match by prefix (longest match first)
|
||||
case "$name" in
|
||||
virtual-machine-*)
|
||||
app_kind="VirtualMachine"
|
||||
app_name="${name#virtual-machine-}"
|
||||
;;
|
||||
vm-instance-*)
|
||||
app_kind="VMInstance"
|
||||
app_name="${name#vm-instance-}"
|
||||
;;
|
||||
vm-disk-*)
|
||||
app_kind="VMDisk"
|
||||
app_name="${name#vm-disk-}"
|
||||
;;
|
||||
virtualprivatecloud-*)
|
||||
app_kind="VirtualPrivateCloud"
|
||||
app_name="${name#virtualprivatecloud-}"
|
||||
;;
|
||||
http-cache-*)
|
||||
app_kind="HTTPCache"
|
||||
app_name="${name#http-cache-}"
|
||||
;;
|
||||
tcp-balancer-*)
|
||||
app_kind="TCPBalancer"
|
||||
app_name="${name#tcp-balancer-}"
|
||||
;;
|
||||
clickhouse-*)
|
||||
app_kind="ClickHouse"
|
||||
app_name="${name#clickhouse-}"
|
||||
;;
|
||||
foundationdb-*)
|
||||
app_kind="FoundationDB"
|
||||
app_name="${name#foundationdb-}"
|
||||
;;
|
||||
ferretdb-*)
|
||||
app_kind="FerretDB"
|
||||
app_name="${name#ferretdb-}"
|
||||
;;
|
||||
rabbitmq-*)
|
||||
app_kind="RabbitMQ"
|
||||
app_name="${name#rabbitmq-}"
|
||||
;;
|
||||
kubernetes-*)
|
||||
app_kind="Kubernetes"
|
||||
app_name="${name#kubernetes-}"
|
||||
;;
|
||||
bucket-*)
|
||||
app_kind="Bucket"
|
||||
app_name="${name#bucket-}"
|
||||
;;
|
||||
kafka-*)
|
||||
app_kind="Kafka"
|
||||
app_name="${name#kafka-}"
|
||||
;;
|
||||
mysql-*)
|
||||
app_kind="MySQL"
|
||||
app_name="${name#mysql-}"
|
||||
;;
|
||||
nats-*)
|
||||
app_kind="NATS"
|
||||
app_name="${name#nats-}"
|
||||
;;
|
||||
postgres-*)
|
||||
app_kind="PostgreSQL"
|
||||
app_name="${name#postgres-}"
|
||||
;;
|
||||
redis-*)
|
||||
app_kind="Redis"
|
||||
app_name="${name#redis-}"
|
||||
;;
|
||||
tenant-*)
|
||||
app_kind="Tenant"
|
||||
app_name="${name#tenant-}"
|
||||
;;
|
||||
vpn-*)
|
||||
app_kind="VPN"
|
||||
app_name="${name#vpn-}"
|
||||
;;
|
||||
bootbox)
|
||||
app_kind="BootBox"
|
||||
app_name="bootbox"
|
||||
;;
|
||||
etcd)
|
||||
app_kind="Etcd"
|
||||
app_name="etcd"
|
||||
;;
|
||||
info)
|
||||
app_kind="Info"
|
||||
app_name="info"
|
||||
;;
|
||||
ingress|ingress-*)
|
||||
app_kind="Ingress"
|
||||
if [ "$name" = "ingress" ]; then
|
||||
app_name="ingress"
|
||||
else
|
||||
app_name="${name#ingress-}"
|
||||
fi
|
||||
;;
|
||||
monitoring)
|
||||
app_kind="Monitoring"
|
||||
app_name="monitoring"
|
||||
;;
|
||||
seaweedfs)
|
||||
app_kind="SeaweedFS"
|
||||
app_name="seaweedfs"
|
||||
;;
|
||||
*)
|
||||
# Unknown type
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$app_kind|$app_name"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Process all HelmReleases in tenant-* namespaces with cozystack.io/ui=true label
|
||||
kubectl get helmreleases --all-namespaces -l cozystack.io/ui=true -o json | \
|
||||
jq -r '.items[] | select(.metadata.namespace | startswith("tenant-")) | "\(.metadata.namespace)|\(.metadata.name)"' | \
|
||||
while IFS='|' read -r namespace name; do
|
||||
echo "Processing HelmRelease $namespace/$name"
|
||||
|
||||
# Determine application type
|
||||
app_type=$(determine_app_type "$name")
|
||||
if [ $? -ne 0 ] || [ -z "$app_type" ]; then
|
||||
echo "Warning: Could not determine application type for $namespace/$name, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
app_kind=$(echo "$app_type" | cut -d'|' -f1)
|
||||
app_name=$(echo "$app_type" | cut -d'|' -f2)
|
||||
app_group="apps.cozystack.io"
|
||||
|
||||
# Build labels string
|
||||
labels="apps.cozystack.io/application.kind=$app_kind"
|
||||
labels="$labels apps.cozystack.io/application.group=$app_group"
|
||||
labels="$labels apps.cozystack.io/application.name=$app_name"
|
||||
|
||||
# Apply labels using kubectl label --overwrite
|
||||
kubectl label helmrelease -n "$namespace" "$name" --overwrite $labels
|
||||
echo "Added application labels to $namespace/$name: $labels"
|
||||
done
|
||||
|
||||
echo "Migration completed"
|
||||
|
||||
# Stamp version
|
||||
kubectl create configmap -n cozy-system cozystack-version \
|
||||
--from-literal=version=23 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 3 --> 4
|
||||
|
||||
# Fix kubeovn crds
|
||||
kubeovn_crds=$(kubectl get crd -o name | grep '\.kubeovn\.io$')
|
||||
if [ -n "$kubeovn_crds" ]; then
|
||||
kubectl annotate $kubeovn_crds meta.helm.sh/release-namespace=cozy-kubeovn meta.helm.sh/release-name=kubeovn
|
||||
kubectl label $kubeovn_crds app.kubernetes.io/managed-by=Helm
|
||||
fi
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=4 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 4 --> 5
|
||||
|
||||
# Fix tenant-kubernetes PVCs
|
||||
kubectl get secret -l kamaji.clastix.io/project=kamaji,kamaji.clastix.io/component=admin-kubeconfig -A --output=go-template='{{ range .items }}{{ printf "%s %s %s %s\n" .metadata.namespace .metadata.name (index .metadata.labels "kamaji.clastix.io/name") (index .data "super-admin.conf") }}{{ end }}' | while read NAMESPACE NAME CLUSTER CONFIGB64; do
|
||||
config=$(mktemp)
|
||||
echo "$CONFIGB64" | base64 -d > "$config"
|
||||
kubectl get pv --kubeconfig="$config" --output=go-template='{{ range .items }}{{ printf "%s\n" .metadata.name }}{{ end }}' | while read PVC; do
|
||||
(set -x; kubectl label pvc --overwrite -n "$NAMESPACE" "$PVC" "cluster.x-k8s.io/cluster-name=$CLUSTER")
|
||||
done
|
||||
rm -f "$config"
|
||||
done
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=5 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 5 --> 6
|
||||
|
||||
# Fix tenant-kubernetes PVCs
|
||||
kubectl get secret -l kamaji.clastix.io/project=kamaji,kamaji.clastix.io/component=admin-kubeconfig -A --output=go-template='{{ range .items }}{{ printf "%s %s %s %s\n" .metadata.namespace .metadata.name (index .metadata.labels "kamaji.clastix.io/name") (index .data "super-admin.conf") }}{{ end }}' | while read NAMESPACE NAME CLUSTER CONFIGB64; do
|
||||
config=$(mktemp)
|
||||
echo "$CONFIGB64" | base64 -d > "$config"
|
||||
kubectl get pv --kubeconfig="$config" --output=go-template='{{ range .items }}{{ printf "%s\n" .metadata.name }}{{ end }}' | while read PVC; do
|
||||
(set -x; kubectl label pvc --overwrite -n "$NAMESPACE" "$PVC" "cluster.x-k8s.io/cluster-name=$CLUSTER")
|
||||
(set -x; kubectl label dv --overwrite -n "$NAMESPACE" "$PVC" "cluster.x-k8s.io/cluster-name=$CLUSTER")
|
||||
done
|
||||
rm -f "$config"
|
||||
done
|
||||
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=6 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 6 --> 7
|
||||
|
||||
# Delete cert-manager crds labels and annotations
|
||||
kubectl patch hr -n cozy-cert-manager cert-manager -p '{"spec": {"suspend": true}}' --type=merge --field-manager=flux-client-side-apply
|
||||
certmanager_crds=$(kubectl get crd -o name | grep '\.cert-manager\.io$')
|
||||
if [ -n "$certmanager_crds" ]; then
|
||||
kubectl annotate $certmanager_crds meta.helm.sh/release-namespace=cozy-cert-manager meta.helm.sh/release-name=cert-manager-crds
|
||||
kubectl label $certmanager_crds app.kubernetes.io/managed-by=Helm
|
||||
fi
|
||||
|
||||
# Remove monitoring, because it is renamed to monitoring-agents
|
||||
kubectl get hr -n cozy-monitoring monitoring && kubectl delete hr -n cozy-monitoring monitoring --wait=0
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=7 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 7 --> 8
|
||||
|
||||
host=$(kubectl get hr tenant-root -n tenant-root -o yaml | grep 'host:' | awk '{print $2}')
|
||||
kubectl patch configmap -n cozy-system cozystack --type merge -p "{\"data\":{\"root-host\":\"$host\"}}"
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=8 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 8 --> 9
|
||||
|
||||
if kubectl get clusterrolebinding kubeapps-admin-group; then
|
||||
kubectl delete clusterrolebinding kubeapps-admin-group
|
||||
fi
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=9 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Migration 9 --> 10
|
||||
|
||||
# Upgrade kubernetes.apps to new chart version
|
||||
kubectl get kuberneteses.apps.cozystack.io -A --no-headers --output=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name' | while read NAMESPACE NAME; do
|
||||
kubectl patch kuberneteses.apps.cozystack.io -n "$NAMESPACE" "$NAME" --type merge -p '{"appVersion":"0.15.1"}'
|
||||
done
|
||||
|
||||
# Write version to cozystack-version config
|
||||
kubectl create configmap -n cozy-system cozystack-version --from-literal=version=10 --dry-run=client -o yaml | kubectl apply -f-
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="${NAMESPACE:-cozy-system}"
|
||||
CURRENT_VERSION="${CURRENT_VERSION:-0}"
|
||||
TARGET_VERSION="${TARGET_VERSION:-0}"
|
||||
|
||||
echo "Starting migrations from version $CURRENT_VERSION to $TARGET_VERSION"
|
||||
|
||||
# Check if ConfigMap exists
|
||||
if ! kubectl get configmap -n "$NAMESPACE" cozystack-version >/dev/null 2>&1; then
|
||||
echo "ConfigMap cozystack-version does not exist, creating it with version $TARGET_VERSION"
|
||||
kubectl create configmap -n "$NAMESPACE" cozystack-version \
|
||||
--from-literal=version="$TARGET_VERSION" \
|
||||
--dry-run=client -o yaml | kubectl apply -f-
|
||||
echo "ConfigMap created with version $TARGET_VERSION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If current version is already at target, nothing to do
|
||||
if [ "$CURRENT_VERSION" -ge "$TARGET_VERSION" ]; then
|
||||
echo "Current version $CURRENT_VERSION is already at or above target version $TARGET_VERSION"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Run migrations sequentially from current version to target version
|
||||
for i in $(seq $((CURRENT_VERSION + 1)) $TARGET_VERSION); do
|
||||
if [ -f "/migrations/$i" ]; then
|
||||
echo "Running migration $i"
|
||||
chmod +x /migrations/$i
|
||||
/migrations/$i || {
|
||||
echo "Migration $i failed"
|
||||
exit 1
|
||||
}
|
||||
echo "Migration $i completed successfully"
|
||||
else
|
||||
echo "Migration $i not found, skipping"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All migrations completed successfully"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user