Compare commits

..

7 Commits

Author SHA1 Message Date
Timofei Larkin
7c8823a835 [platform] Cozy values secret replicator
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-06 18:10:47 +03:00
Timofei Larkin
43df3a1b70 [testing] Add aliases and autocomplete (#1803)
## What this PR does

Adds a `k=kubectl` alias and bash completion for kubectl to the
e2e-testing sandbox container to maintainers have an easier time
exec'ing into the CI container when something needs to be debugged.

### Release note

```release-note
[testing] Add k=kubectl alias and enable kubectl completion in the CI
container.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Enhanced the e2e sandbox image to enable shell bash-completion and
kubectl command completion.
* Added an alias (k) and completion wiring for kubectl to improve
interactive command use.
* These changes augment the test environment shell during image build to
provide a smoother developer/testing experience.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-05 22:48:01 +04:00
Andrei Kvapil
07b406e9bc [kubernetes] Fix endpoints for cilium-gateway (#1729)
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>

<!-- Thank you for making a contribution! Here are some tips for you:
- Start the PR title with the [label] of Cozystack component:
- For system components: [platform], [system], [linstor], [cilium],
[kube-ovn], [dashboard], [cluster-api], etc.
- For managed apps: [apps], [tenant], [kubernetes], [postgres],
[virtual-machine] etc.
- For development and maintenance: [tests], [ci], [docs], [maintenance].
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## What this PR does

Integrate fix
- https://github.com/kubevirt/cloud-provider-kubevirt/pull/379

### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- Start with the same [label] as in the PR title
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note
[kubernetes] Fix endpoints for cilium-gateway
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved handling of incomplete endpoint data by introducing fallback
detection mechanisms.
* Enhanced service discovery to gather endpoints from all available
resources when standard detection fails.
* Updated logging to provide better visibility into fallback operations
and current resource status.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-05 18:11:41 +01:00
Andrei Kvapil
5f36396ccc [platform] Replace Helm lookup with valuesFrom mechanism (#1787)
## What this PR does

Replaces Helm lookup functions with FluxCD valuesFrom mechanism for
passing configuration to HelmReleases. This provides cleaner config
propagation and eliminates the need for force reconcile controllers.

### Changes:

**Platform/Tenant charts:**
- Add Secret `cozystack-values` creation in platform chart (for
tenant-root and system namespaces)
- Add Secret `cozystack-values` creation in tenant chart (for child
namespaces)

**cozystack-api:**
- Add `valuesFrom` references to HelmRelease when creating applications
- Filter keys starting with `_` when returning Application specs
- Validate that user values don't contain `_` prefixed keys

**cozystack-controller:**
- Add validation that HelmRelease contains correct valuesFrom
configuration
- Remove `CozystackConfigReconciler` (no longer needed)
- Remove `TenantHelmReconciler` (no longer needed)

**Helm charts (40+ files):**
- Add helper templates in cozy-lib for `_cluster`/`_namespace` access
- Replace ConfigMap lookups with `.Values._cluster.*`
- Replace Namespace annotation lookups with `.Values._namespace.*`

### Architecture:

```
Secret cozystack-values (in each namespace)
├── _cluster: YAML with data from ConfigMaps (cozystack, cozystack-branding, cozystack-scheduling)
└── _namespace: YAML with namespace service references (etcd, host, ingress, monitoring, seaweedfs)

HelmRelease
└── spec.valuesFrom:
    ├── Secret/cozystack-values → _namespace → .Values._namespace
    └── Secret/cozystack-values → _cluster → .Values._cluster
```

### Release note

```release-note
[platform] Replace Helm lookup functions with FluxCD valuesFrom mechanism for configuration propagation
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Helm releases and namespaces now source centralized cluster/namespace
configuration via a new secret (cozystack-values), and many templates
read values from chart-provided _cluster/_namespace entries.

* **Bug Fixes**
* API now rejects application specs containing reserved keys prefixed
with "_" to prevent invalid configurations.

* **Refactor**
* Two background reconciler controllers were removed from startup,
simplifying controller initialization.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-01-05 17:53:33 +01:00
Andrei Kvapil
2e61810547 refactor: replace Helm lookup with valuesFrom mechanism
Replace Helm lookup functions with FluxCD valuesFrom mechanism for
reading cluster and namespace configuration.

Changes:
- Create Secret cozystack-values in each namespace with values.yaml key
  containing _cluster and _namespace configuration as nested YAML
- Configure HelmReleases to read from this Secret via valuesFrom
  (valuesKey defaults to values.yaml, so it can be omitted)
- Update cozy-lib helpers to access config via .Values._cluster
- Add default values for required _cluster keys to ensure all fields exist
- Update Go code (cozystack-api and helm reconciler) to use new format

This eliminates the need for Helm lookup functions while maintaining
the same configuration interface for charts.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-05 16:10:55 +01:00
Timofei Larkin
bf1928c96f [testing] Add aliases and autocomplete
## What this PR does

Adds a `k=kubectl` alias and bash completion for kubectl to the
e2e-testing sandbox container to maintainers have an easier time
exec'ing into the CI container when something needs to be debugged.

### Release note

```release-note
[testing] Add k=kubectl alias and enable kubectl completion in the CI
container.
```

Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-01-05 16:29:39 +03:00
Andrei Kvapil
f59665208c [kubernetes] Fix endpoints for cilium-gateway
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-01-04 10:35:16 +01:00
70 changed files with 783 additions and 650 deletions

View File

@@ -200,22 +200,6 @@ func main() {
os.Exit(1)
}
if err = (&controller.TenantHelmReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "TenantHelmReconciler")
os.Exit(1)
}
if err = (&controller.CozystackConfigReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CozystackConfigReconciler")
os.Exit(1)
}
cozyAPIKind := "DaemonSet"
if reconcileDeployment {
cozyAPIKind = "Deployment"

View File

@@ -32,12 +32,16 @@ import (
helmv2 "github.com/fluxcd/helm-controller/api/v2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcewatcherv1beta1 "github.com/fluxcd/source-watcher/api/v2/v1beta1"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log"
@@ -45,6 +49,7 @@ import (
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"github.com/cozystack/cozystack/internal/cozyvaluesreplicator"
"github.com/cozystack/cozystack/internal/fluxinstall"
"github.com/cozystack/cozystack/internal/operator"
// +kubebuilder:scaffold:imports
@@ -73,6 +78,9 @@ func main() {
var enableHTTP2 bool
var installFlux bool
var cozystackVersion string
var cozyValuesSecretName string
var cozyValuesSecretNamespace string
var cozyValuesNamespaceSelector string
var platformSourceURL string
var platformSourceName string
var platformSourceRef string
@@ -92,6 +100,9 @@ func main() {
flag.StringVar(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
flag.StringVar(&cozyValuesSecretName, "cozy-values-secret-name", "cozystack-values", "The name of the secret containing cluster-wide configuration values.")
flag.StringVar(&cozyValuesSecretNamespace, "cozy-values-secret-namespace", "cozy-system", "The namespace of the secret containing cluster-wide configuration values.")
flag.StringVar(&cozyValuesNamespaceSelector, "cozy-values-namespace-selector", "cozystack.io/system=true", "The label selector for namespaces where the cluster-wide configuration values must be replicated.")
opts := zap.Options{
Development: true,
@@ -110,10 +121,29 @@ func main() {
os.Exit(1)
}
targetNSSelector, err := labels.Parse(cozyValuesNamespaceSelector)
if err != nil {
setupLog.Error(err, "could not parse namespace label selector")
os.Exit(1)
}
// Start the controller manager
setupLog.Info("Starting controller manager")
mgr, err := ctrl.NewManager(config, ctrl.Options{
Scheme: scheme,
Cache: cache.Options{
ByObject: map[client.Object]cache.ByObject{
// Cache only Secrets named <secretName> (in any namespace)
&corev1.Secret{}: {
Field: fields.OneTermEqualSelector("metadata.name", cozyValuesSecretName),
},
// Cache only Namespaces that match a label selector
&corev1.Namespace{}: {
Label: targetNSSelector,
},
},
},
Metrics: metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
@@ -169,6 +199,16 @@ func main() {
}
}
if err := (&cozyvaluesreplicator.SecretReplicatorReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
SourceNamespace: cozyValuesSecretNamespace,
SecretName: cozyValuesSecretName,
TargetNamespaceSelector: targetNSSelector,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CozyValuesReplicator")
os.Exit(1)
}
// Setup PackageSource reconciler
if err := (&operator.PackageSourceReconciler{
Client: mgr.GetClient(),

View File

@@ -97,7 +97,34 @@ func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleasesForCRD(ctx
return nil
}
// updateHelmReleaseChart updates the chart in HelmRelease based on CozystackResourceDefinition
// expectedValuesFrom returns the expected valuesFrom configuration for HelmReleases
func expectedValuesFrom() []helmv2.ValuesReference {
return []helmv2.ValuesReference{
{
Kind: "Secret",
Name: "cozystack-values",
},
}
}
// valuesFromEqual compares two ValuesReference slices
func valuesFromEqual(a, b []helmv2.ValuesReference) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i].Kind != b[i].Kind ||
a[i].Name != b[i].Name ||
a[i].ValuesKey != b[i].ValuesKey ||
a[i].TargetPath != b[i].TargetPath ||
a[i].Optional != b[i].Optional {
return false
}
}
return true
}
// updateHelmReleaseChart updates the chart and valuesFrom in HelmRelease based on CozystackResourceDefinition
func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleaseChart(ctx context.Context, hr *helmv2.HelmRelease, crd *cozyv1alpha1.CozystackResourceDefinition) error {
logger := log.FromContext(ctx)
hrCopy := hr.DeepCopy()
@@ -154,6 +181,14 @@ func (r *CozystackResourceDefinitionHelmReconciler) updateHelmReleaseChart(ctx c
}
}
// Check and update valuesFrom configuration
expected := expectedValuesFrom()
if !valuesFromEqual(hrCopy.Spec.ValuesFrom, expected) {
logger.V(4).Info("Updating HelmRelease valuesFrom", "name", hr.Name, "namespace", hr.Namespace)
hrCopy.Spec.ValuesFrom = expected
updated = true
}
if updated {
logger.V(4).Info("Updating HelmRelease chart", "name", hr.Name, "namespace", hr.Namespace)
if err := r.Update(ctx, hrCopy); err != nil {

View File

@@ -1,140 +0,0 @@
package controller
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"time"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
type CozystackConfigReconciler struct {
client.Client
Scheme *runtime.Scheme
}
var configMapNames = []string{"cozystack", "cozystack-branding", "cozystack-scheduling"}
const configMapNamespace = "cozy-system"
const digestAnnotation = "cozystack.io/cozy-config-digest"
const forceReconcileKey = "reconcile.fluxcd.io/forceAt"
const requestedAt = "reconcile.fluxcd.io/requestedAt"
func (r *CozystackConfigReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
time.Sleep(2 * time.Second)
digest, err := r.computeDigest(ctx)
if err != nil {
log.Error(err, "failed to compute config digest")
return ctrl.Result{}, nil
}
var helmList helmv2.HelmReleaseList
if err := r.List(ctx, &helmList); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to list HelmReleases: %w", err)
}
now := time.Now().Format(time.RFC3339Nano)
updated := 0
for _, hr := range helmList.Items {
isSystemApp := hr.Labels["cozystack.io/system-app"] == "true"
isTenantRoot := hr.Namespace == "tenant-root" && hr.Name == "tenant-root"
if !isSystemApp && !isTenantRoot {
continue
}
patchTarget := hr.DeepCopy()
if hr.Annotations == nil {
hr.Annotations = map[string]string{}
}
if hr.Annotations[digestAnnotation] == digest {
continue
}
patchTarget.Annotations[digestAnnotation] = digest
patchTarget.Annotations[forceReconcileKey] = now
patchTarget.Annotations[requestedAt] = now
patch := client.MergeFrom(hr.DeepCopy())
if err := r.Patch(ctx, patchTarget, patch); err != nil {
log.Error(err, "failed to patch HelmRelease", "name", hr.Name, "namespace", hr.Namespace)
continue
}
updated++
log.Info("patched HelmRelease with new config digest", "name", hr.Name, "namespace", hr.Namespace)
}
log.Info("finished reconciliation", "updatedHelmReleases", updated)
return ctrl.Result{}, nil
}
func (r *CozystackConfigReconciler) computeDigest(ctx context.Context) (string, error) {
hash := sha256.New()
for _, name := range configMapNames {
var cm corev1.ConfigMap
err := r.Get(ctx, client.ObjectKey{Namespace: configMapNamespace, Name: name}, &cm)
if err != nil {
if kerrors.IsNotFound(err) {
continue // ignore missing
}
return "", err
}
// Sort keys for consistent hashing
var keys []string
for k := range cm.Data {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v := cm.Data[k]
fmt.Fprintf(hash, "%s:%s=%s\n", name, k, v)
}
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func (r *CozystackConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
WithEventFilter(predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
cm, ok := e.ObjectNew.(*corev1.ConfigMap)
return ok && cm.Namespace == configMapNamespace && contains(configMapNames, cm.Name)
},
CreateFunc: func(e event.CreateEvent) bool {
cm, ok := e.Object.(*corev1.ConfigMap)
return ok && cm.Namespace == configMapNamespace && contains(configMapNames, cm.Name)
},
DeleteFunc: func(e event.DeleteEvent) bool {
cm, ok := e.Object.(*corev1.ConfigMap)
return ok && cm.Namespace == configMapNamespace && contains(configMapNames, cm.Name)
},
}).
For(&corev1.ConfigMap{}).
Complete(r)
}
func contains(slice []string, val string) bool {
for _, s := range slice {
if s == val {
return true
}
}
return false
}

View File

@@ -1,159 +0,0 @@
package controller
import (
"context"
"fmt"
"strings"
"time"
e "errors"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
"gopkg.in/yaml.v2"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)
type TenantHelmReconciler struct {
client.Client
Scheme *runtime.Scheme
}
func (r *TenantHelmReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
time.Sleep(2 * time.Second)
hr := &helmv2.HelmRelease{}
if err := r.Get(ctx, req.NamespacedName, hr); err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{}, nil
}
logger.Error(err, "unable to fetch HelmRelease")
return ctrl.Result{}, err
}
if !strings.HasPrefix(hr.Name, "tenant-") {
return ctrl.Result{}, nil
}
if len(hr.Status.Conditions) == 0 || hr.Status.Conditions[0].Type != "Ready" {
return ctrl.Result{}, nil
}
if len(hr.Status.History) == 0 {
logger.Info("no history in HelmRelease status", "name", hr.Name)
return ctrl.Result{}, nil
}
if hr.Status.History[0].Status != "deployed" {
return ctrl.Result{}, nil
}
newDigest := hr.Status.History[0].Digest
var hrList helmv2.HelmReleaseList
childNamespace := getChildNamespace(hr.Namespace, hr.Name)
if childNamespace == "tenant-root" && hr.Name == "tenant-root" {
if hr.Spec.Values == nil {
logger.Error(e.New("hr.Spec.Values is nil"), "cant annotate tenant-root ns")
return ctrl.Result{}, nil
}
err := annotateTenantRootNs(*hr.Spec.Values, r.Client)
if err != nil {
logger.Error(err, "cant annotate tenant-root ns")
return ctrl.Result{}, nil
}
logger.Info("namespace 'tenant-root' annotated")
}
if err := r.List(ctx, &hrList, client.InNamespace(childNamespace)); err != nil {
logger.Error(err, "unable to list HelmReleases in namespace", "namespace", hr.Name)
return ctrl.Result{}, err
}
for _, item := range hrList.Items {
if item.Name == hr.Name {
continue
}
oldDigest := item.GetAnnotations()["cozystack.io/tenant-config-digest"]
if oldDigest == newDigest {
continue
}
patchTarget := item.DeepCopy()
if patchTarget.Annotations == nil {
patchTarget.Annotations = map[string]string{}
}
ts := time.Now().Format(time.RFC3339Nano)
patchTarget.Annotations["cozystack.io/tenant-config-digest"] = newDigest
patchTarget.Annotations["reconcile.fluxcd.io/forceAt"] = ts
patchTarget.Annotations["reconcile.fluxcd.io/requestedAt"] = ts
patch := client.MergeFrom(item.DeepCopy())
if err := r.Patch(ctx, patchTarget, patch); err != nil {
logger.Error(err, "failed to patch HelmRelease", "name", patchTarget.Name)
continue
}
logger.Info("patched HelmRelease with new digest", "name", patchTarget.Name, "digest", newDigest, "version", hr.Status.History[0].Version)
}
return ctrl.Result{}, nil
}
func (r *TenantHelmReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&helmv2.HelmRelease{}).
Complete(r)
}
func getChildNamespace(currentNamespace, hrName string) string {
tenantName := strings.TrimPrefix(hrName, "tenant-")
switch {
case currentNamespace == "tenant-root" && hrName == "tenant-root":
// 1) root tenant inside root namespace
return "tenant-root"
case currentNamespace == "tenant-root":
// 2) any other tenant in root namespace
return fmt.Sprintf("tenant-%s", tenantName)
default:
// 3) tenant in a dedicated namespace
return fmt.Sprintf("%s-%s", currentNamespace, tenantName)
}
}
func annotateTenantRootNs(values apiextensionsv1.JSON, c client.Client) error {
var data map[string]interface{}
if err := yaml.Unmarshal(values.Raw, &data); err != nil {
return fmt.Errorf("failed to parse HelmRelease values: %w", err)
}
host, ok := data["host"].(string)
if !ok || host == "" {
return fmt.Errorf("host field not found or not a string")
}
var ns corev1.Namespace
if err := c.Get(context.TODO(), client.ObjectKey{Name: "tenant-root"}, &ns); err != nil {
return fmt.Errorf("failed to get namespace tenant-root: %w", err)
}
if ns.Annotations == nil {
ns.Annotations = map[string]string{}
}
ns.Annotations["namespace.cozystack.io/host"] = host
if err := c.Update(context.TODO(), &ns); err != nil {
return fmt.Errorf("failed to update namespace: %w", err)
}
return nil
}

View File

@@ -0,0 +1,160 @@
package cozyvaluesreplicator
import (
"context"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// Reconciler fields this setup relies on.
type SecretReplicatorReconciler struct {
client.Client
Scheme *runtime.Scheme
// Source of truth:
SourceNamespace string
SecretName string
// Namespaces to replicate into:
// (e.g. labels.SelectorFromSet(labels.Set{"tenant":"true"}), or metav1.LabelSelectorAsSelector(...))
TargetNamespaceSelector labels.Selector
}
func (r *SecretReplicatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
// 1) Primary watch for requirement (b):
// Reconcile any Secret named r.SecretName in any namespace (includes source too).
// This keeps Secrets in cache and causes “copy changed -> reconcile it” to happen.
secretNameOnly := predicate.NewPredicateFuncs(func(obj client.Object) bool {
return obj.GetName() == r.SecretName
})
// 2) Secondary watch for requirement (c):
// When the *source* Secret changes, fan-out reconcile requests to every matching namespace.
onlySourceSecret := predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool { return isSourceSecret(e.Object, r) },
UpdateFunc: func(e event.UpdateEvent) bool { return isSourceSecret(e.ObjectNew, r) },
DeleteFunc: func(e event.DeleteEvent) bool { return isSourceSecret(e.Object, r) },
GenericFunc: func(e event.GenericEvent) bool {
return isSourceSecret(e.Object, r)
},
}
// Fan-out mapper for source Secret events -> one request per matching target namespace.
fanOutOnSourceSecret := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, _ client.Object) []reconcile.Request {
// List namespaces *from the cache* (because we also watch Namespaces below).
var nsList corev1.NamespaceList
if err := r.List(ctx, &nsList); err != nil {
// If list fails, best-effort: return nothing; reconcile will be retried by next event.
return nil
}
reqs := make([]reconcile.Request, 0, len(nsList.Items))
for i := range nsList.Items {
ns := &nsList.Items[i]
if ns.Name == r.SourceNamespace {
continue
}
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)) {
continue
}
reqs = append(reqs, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: ns.Name,
Name: r.SecretName,
},
})
}
return reqs
})
// 3) Namespace watch for requirement (a):
// When a namespace is created/updated to match selector, enqueue reconcile for the Secret copy in that namespace.
enqueueOnNamespaceMatch := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
ns, ok := obj.(*corev1.Namespace)
if !ok {
return nil
}
if ns.Name == r.SourceNamespace {
return nil
}
if r.TargetNamespaceSelector != nil && !r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)) {
return nil
}
return []reconcile.Request{{
NamespacedName: types.NamespacedName{
Namespace: ns.Name,
Name: r.SecretName,
},
}}
})
// Only trigger from namespace events where the label match may be (or become) true.
// (You can keep this simple; its fine if it fires on any update—your Reconcile should be idempotent.)
namespaceMayMatter := predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool {
ns, ok := e.Object.(*corev1.Namespace)
return ok && (r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(ns.Labels)))
},
UpdateFunc: func(e event.UpdateEvent) bool {
oldNS, okOld := e.ObjectOld.(*corev1.Namespace)
newNS, okNew := e.ObjectNew.(*corev1.Namespace)
if !okOld || !okNew {
return false
}
// Fire if it matches now OR matched before (covers transitions both ways; reconcile can decide what to do).
oldMatch := r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(oldNS.Labels))
newMatch := r.TargetNamespaceSelector == nil || r.TargetNamespaceSelector.Matches(labels.Set(newNS.Labels))
return oldMatch || newMatch
},
DeleteFunc: func(event.DeleteEvent) bool { return false }, // nothing to do on namespace delete
GenericFunc: func(event.GenericEvent) bool { return false },
}
return ctrl.NewControllerManagedBy(mgr).
// (b) Watch all Secrets with the chosen name; this also ensures Secret objects are cached.
For(&corev1.Secret{}, builder.WithPredicates(secretNameOnly)).
// (c) Add a second watch on Secret, but only for the source secret, and fan-out to all namespaces.
Watches(
&corev1.Secret{},
fanOutOnSourceSecret,
builder.WithPredicates(onlySourceSecret),
).
// (a) Watch Namespaces so theyre cached and so “namespace appears / starts matching” enqueues reconcile.
Watches(
&corev1.Namespace{},
enqueueOnNamespaceMatch,
builder.WithPredicates(namespaceMayMatter),
).
Complete(r)
}
func isSourceSecret(obj client.Object, r *SecretReplicatorReconciler) bool {
if obj == nil {
return false
}
return obj.GetNamespace() == r.SourceNamespace && obj.GetName() == r.SecretName
}
func (r *SecretReplicatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
if req.Name != r.SecretName || req.Namespace == r.SourceNamespace {
return ctrl.Result{}, nil
}
originalSecret := &corev1.Secret{}
r.Get(ctx, types.NamespacedName{Namespace: r.SourceNamespace, Name: r.SecretName}, originalSecret)
replicatedSecret := originalSecret.DeepCopy()
replicatedSecret.Namespace = req.Namespace
r.Update(ctx, replicatedSecret)
return ctrl.Result{}, nil
}

View File

@@ -1,5 +1,4 @@
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $seaweedfs := index $myNS.metadata.annotations "namespace.cozystack.io/seaweedfs" }}
{{- $seaweedfs := .Values._namespace.seaweedfs }}
apiVersion: objectstorage.k8s.io/v1alpha1
kind: BucketClaim
metadata:

View File

@@ -21,5 +21,8 @@ spec:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values
values:
bucketName: {{ .Release.Name }}

View File

@@ -1,5 +1,4 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }}
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
{{- if .Values.clickhouseKeeper.enabled }}
apiVersion: "clickhouse-keeper.altinity.com/v1"

View File

@@ -1,5 +1,4 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }}
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }}
{{- $passwords := dict }}
{{- $users := .Values.users }}

View File

@@ -50,9 +50,8 @@ spec:
postgresUID: 999
postgresGID: 999
enableSuperuserAccess: true
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
{{- if $configMap }}
{{- $rawConstraints := get $configMap.data "globalAppTopologySpreadConstraints" }}
{{- if .Values._cluster.scheduling }}
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
{{- if $rawConstraints }}
{{- $rawConstraints | fromYaml | toYaml | nindent 2 }}
labelSelector:

View File

@@ -1,5 +1,4 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" | default (dict "data" (dict)) }}
{{- $clusterDomain := index $cozyConfig.data "cluster-domain" | default "cozy.local" }}
{{- $clusterDomain := index .Values._cluster "cluster-domain" | default "cozy.local" }}
---
apiVersion: apps.foundationdb.org/v1beta2
kind: FoundationDBCluster

View File

@@ -1,5 +1,5 @@
diff --git a/pkg/controller/kubevirteps/kubevirteps_controller.go b/pkg/controller/kubevirteps/kubevirteps_controller.go
index 53388eb8e..28644236f 100644
index 53388eb8e..873060251 100644
--- a/pkg/controller/kubevirteps/kubevirteps_controller.go
+++ b/pkg/controller/kubevirteps/kubevirteps_controller.go
@@ -12,7 +12,6 @@ import (
@@ -10,12 +10,17 @@ index 53388eb8e..28644236f 100644
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -669,35 +668,50 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
@@ -666,38 +665,62 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
// for extracting the nodes it does not matter what type of address we are dealing with
// all nodes with an endpoint for a corresponding slice will be selected.
nodeSet := sets.Set[string]{}
+ hasEndpointsWithoutNodeName := false
for _, slice := range tenantSlices {
for _, endpoint := range slice.Endpoints {
// find all unique nodes that correspond to an endpoint in a tenant slice
+ if endpoint.NodeName == nil {
+ klog.Warningf("Skipping endpoint without NodeName in slice %s/%s", slice.Namespace, slice.Name)
+ hasEndpointsWithoutNodeName = true
+ continue
+ }
nodeSet.Insert(*endpoint.NodeName)
@@ -23,6 +28,13 @@ index 53388eb8e..28644236f 100644
}
- klog.Infof("Desired nodes for service %s in namespace %s: %v", service.Name, service.Namespace, sets.List(nodeSet))
+ // Fallback: if no endpoints with NodeName were found, but there are endpoints without NodeName,
+ // distribute traffic to all VMIs (similar to ExternalTrafficPolicy=Cluster behavior)
+ if nodeSet.Len() == 0 && hasEndpointsWithoutNodeName {
+ klog.Infof("No endpoints with NodeName found for service %s/%s, falling back to all VMIs", service.Namespace, service.Name)
+ return c.getAllVMIEndpoints()
+ }
+
+ klog.Infof("Desired nodes for service %s/%s: %v", service.Namespace, service.Name, sets.List(nodeSet))
for _, node := range sets.List(nodeSet) {
@@ -68,7 +80,7 @@ index 53388eb8e..28644236f 100644
desiredEndpoints = append(desiredEndpoints, &discovery.Endpoint{
Addresses: []string{i.IP},
Conditions: discovery.EndpointConditions{
@@ -705,9 +719,9 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
@@ -705,9 +728,9 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
Serving: &serving,
Terminating: &terminating,
},
@@ -80,6 +92,71 @@ index 53388eb8e..28644236f 100644
}
}
}
@@ -716,6 +739,64 @@ func (c *Controller) getDesiredEndpoints(service *v1.Service, tenantSlices []*di
return desiredEndpoints
}
+// getAllVMIEndpoints returns endpoints for all VMIs in the infra namespace.
+// This is used as a fallback when tenant endpoints don't have NodeName specified,
+// similar to ExternalTrafficPolicy=Cluster behavior where traffic is distributed to all nodes.
+func (c *Controller) getAllVMIEndpoints() []*discovery.Endpoint {
+ var endpoints []*discovery.Endpoint
+
+ // List all VMIs in the infra namespace
+ vmiList, err := c.infraDynamic.
+ Resource(kubevirtv1.VirtualMachineInstanceGroupVersionKind.GroupVersion().WithResource("virtualmachineinstances")).
+ Namespace(c.infraNamespace).
+ List(context.TODO(), metav1.ListOptions{})
+ if err != nil {
+ klog.Errorf("Failed to list VMIs in namespace %q: %v", c.infraNamespace, err)
+ return endpoints
+ }
+
+ for _, obj := range vmiList.Items {
+ vmi := &kubevirtv1.VirtualMachineInstance{}
+ err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, vmi)
+ if err != nil {
+ klog.Errorf("Failed to convert Unstructured to VirtualMachineInstance: %v", err)
+ continue
+ }
+
+ if vmi.Status.NodeName == "" {
+ klog.Warningf("Skipping VMI %s/%s: NodeName is empty", vmi.Namespace, vmi.Name)
+ continue
+ }
+ nodeNamePtr := &vmi.Status.NodeName
+
+ ready := vmi.Status.Phase == kubevirtv1.Running
+ serving := vmi.Status.Phase == kubevirtv1.Running
+ terminating := vmi.Status.Phase == kubevirtv1.Failed || vmi.Status.Phase == kubevirtv1.Succeeded
+
+ for _, i := range vmi.Status.Interfaces {
+ if i.Name == "default" {
+ if i.IP == "" {
+ klog.Warningf("VMI %s/%s interface %q has no IP, skipping", vmi.Namespace, vmi.Name, i.Name)
+ continue
+ }
+ endpoints = append(endpoints, &discovery.Endpoint{
+ Addresses: []string{i.IP},
+ Conditions: discovery.EndpointConditions{
+ Ready: &ready,
+ Serving: &serving,
+ Terminating: &terminating,
+ },
+ NodeName: nodeNamePtr,
+ })
+ break
+ }
+ }
+ }
+
+ klog.Infof("Fallback: created %d endpoints from all VMIs in namespace %s", len(endpoints), c.infraNamespace)
+ return endpoints
+}
+
func (c *Controller) ensureEndpointSliceLabels(slice *discovery.EndpointSlice, svc *v1.Service) (map[string]string, bool) {
labels := make(map[string]string)
labelsChanged := false
diff --git a/pkg/controller/kubevirteps/kubevirteps_controller_test.go b/pkg/controller/kubevirteps/kubevirteps_controller_test.go
index 1c97035b4..d205d0bed 100644
--- a/pkg/controller/kubevirteps/kubevirteps_controller_test.go

View File

@@ -1,7 +1,6 @@
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $etcd := index $myNS.metadata.annotations "namespace.cozystack.io/etcd" }}
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
{{- $etcd := .Values._namespace.etcd }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
{{- $kubevirtmachinetemplateNames := list }}
{{- define "kubevirtmachinetemplate" -}}
spec:
@@ -31,9 +30,8 @@ spec:
{{- end }}
cluster.x-k8s.io/deployment-name: {{ $.Release.Name }}-{{ .groupName }}
spec:
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
{{- if $configMap }}
{{- $rawConstraints := get $configMap.data "globalAppTopologySpreadConstraints" }}
{{- if .Values._cluster.scheduling }}
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
{{- if $rawConstraints }}
{{- $rawConstraints | fromYaml | toYaml | nindent 10 }}
labelSelector:

View File

@@ -1,5 +1,4 @@
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $targetTenant := index $myNS.metadata.annotations "namespace.cozystack.io/monitoring" }}
{{- $targetTenant := .Values._namespace.monitoring }}
{{- if .Values.addons.monitoringAgents.enabled }}
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease

View File

@@ -1,8 +1,6 @@
{{- define "cozystack.defaultVPAValues" -}}
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }}
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $targetTenant := index $myNS.metadata.annotations "namespace.cozystack.io/monitoring" }}
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
{{- $targetTenant := .Values._namespace.monitoring }}
vpaForVPA: false
vertical-pod-autoscaler:
recommender:

View File

@@ -1,5 +1,4 @@
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
{{- $ingress := .Values._namespace.ingress }}
{{- if and (eq .Values.addons.ingressNginx.exposeMethod "Proxied") .Values.addons.ingressNginx.hosts }}
---
apiVersion: networking.k8s.io/v1

View File

@@ -1,5 +1,4 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }}
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-credentials" .Release.Name) }}
{{- $passwords := dict }}
@@ -53,6 +52,9 @@ spec:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values
values:
nats:
container:

View File

@@ -46,9 +46,8 @@ spec:
imageName: ghcr.io/cloudnative-pg/postgresql:{{ include "postgres.versionMap" $ | trimPrefix "v" }}
enableSuperuserAccess: true
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
{{- if $configMap }}
{{- $rawConstraints := get $configMap.data "globalAppTopologySpreadConstraints" }}
{{- if .Values._cluster.scheduling }}
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
{{- if $rawConstraints }}
{{- $rawConstraints | fromYaml | toYaml | nindent 2 }}
labelSelector:

View File

@@ -31,4 +31,7 @@ spec:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values
{{- end }}

View File

@@ -30,3 +30,6 @@ spec:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values

View File

@@ -31,4 +31,7 @@ spec:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values
{{- end }}

View File

@@ -1,5 +1,4 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $oidcEnabled := index $cozyConfig.data "oidc-enabled" }}
{{- $oidcEnabled := index .Values._cluster "oidc-enabled" }}
{{- if eq $oidcEnabled "true" }}
{{- if .Capabilities.APIVersions.Has "v1.edp.epam.com/v1" }}
apiVersion: v1.edp.epam.com/v1

View File

@@ -31,4 +31,7 @@ spec:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values
{{- end }}

View File

@@ -1,46 +1,63 @@
{{- define "cozystack.namespace-anotations" }}
{{- $context := index . 0 }}
{{- $existingNS := index . 1 }}
{{- range $x := list "etcd" "monitoring" "ingress" "seaweedfs" }}
{{- if (index $context.Values $x) }}
namespace.cozystack.io/{{ $x }}: "{{ include "tenant.name" $context }}"
{{- else }}
namespace.cozystack.io/{{ $x }}: "{{ index $existingNS.metadata.annotations (printf "namespace.cozystack.io/%s" $x) | required (printf "namespace %s has no namespace.cozystack.io/%s annotation" $context.Release.Namespace $x) }}"
{{- end }}
{{- end }}
{{- end }}
{{/* Lookup for namespace uid (needed for ownerReferences) */}}
{{- $existingNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- if not $existingNS }}
{{- fail (printf "error lookup existing namespace: %s" .Release.Namespace) }}
{{- end }}
{{- if ne (include "tenant.name" .) "tenant-root" }}
{{/* Compute namespace values once for use in both Secret and labels */}}
{{- $tenantName := include "tenant.name" . }}
{{- $parentNamespace := .Values._namespace | default dict }}
{{- $parentHost := $parentNamespace.host | default "" }}
{{/* Compute host */}}
{{- $computedHost := "" }}
{{- if .Values.host }}
{{- $computedHost = .Values.host }}
{{- else if $parentHost }}
{{- $computedHost = printf "%s.%s" (splitList "-" $tenantName | last) $parentHost }}
{{- end }}
{{/* Compute service references */}}
{{- $etcd := $parentNamespace.etcd | default "" }}
{{- if .Values.etcd }}
{{- $etcd = $tenantName }}
{{- end }}
{{- $ingress := $parentNamespace.ingress | default "" }}
{{- if .Values.ingress }}
{{- $ingress = $tenantName }}
{{- end }}
{{- $monitoring := $parentNamespace.monitoring | default "" }}
{{- if .Values.monitoring }}
{{- $monitoring = $tenantName }}
{{- end }}
{{- $seaweedfs := $parentNamespace.seaweedfs | default "" }}
{{- if .Values.seaweedfs }}
{{- $seaweedfs = $tenantName }}
{{- end }}
---
apiVersion: v1
kind: Namespace
metadata:
name: {{ include "tenant.name" . }}
name: {{ $tenantName }}
{{- if hasPrefix "tenant-" .Release.Namespace }}
annotations:
{{- if .Values.host }}
namespace.cozystack.io/host: "{{ .Values.host }}"
{{- else }}
{{ $parentHost := index $existingNS.metadata.annotations "namespace.cozystack.io/host" | required (printf "namespace %s has no namespace.cozystack.io/host annotation" .Release.Namespace) }}
namespace.cozystack.io/host: "{{ splitList "-" (include "tenant.name" .) | last }}.{{ $parentHost }}"
{{- end }}
{{- include "cozystack.namespace-anotations" (list . $existingNS) | nindent 4 }}
labels:
tenant.cozystack.io/{{ include "tenant.name" $ }}: ""
{{- if hasPrefix "tenant-" .Release.Namespace }}
tenant.cozystack.io/{{ $tenantName }}: ""
{{- $parts := splitList "-" .Release.Namespace }}
{{- range $i, $v := $parts }}
{{- if ne $i 0 }}
tenant.cozystack.io/{{ join "-" (slice $parts 0 (add $i 1)) }}: ""
{{- end }}
{{- end }}
{{- end }}
{{- include "cozystack.namespace-anotations" (list $ $existingNS) | nindent 4 }}
{{/* Labels for network policies */}}
namespace.cozystack.io/etcd: {{ $etcd | quote }}
namespace.cozystack.io/ingress: {{ $ingress | quote }}
namespace.cozystack.io/monitoring: {{ $monitoring | quote }}
namespace.cozystack.io/seaweedfs: {{ $seaweedfs | quote }}
namespace.cozystack.io/host: {{ $computedHost | quote }}
alpha.kubevirt.io/auto-memory-limits-ratio: "1.0"
ownerReferences:
- apiVersion: v1
@@ -50,4 +67,23 @@ metadata:
name: {{ .Release.Namespace }}
uid: {{ $existingNS.metadata.uid }}
{{- end }}
---
apiVersion: v1
kind: Secret
metadata:
name: cozystack-values
namespace: {{ $tenantName }}
labels:
reconcile.fluxcd.io/watch: Enabled
type: Opaque
stringData:
values.yaml: |
_cluster:
{{- .Values._cluster | toYaml | nindent 6 }}
_namespace:
etcd: {{ $etcd | quote }}
ingress: {{ $ingress | quote }}
monitoring: {{ $monitoring | quote }}
seaweedfs: {{ $seaweedfs | quote }}
host: {{ $computedHost | quote }}
{{- end }}

View File

@@ -31,4 +31,7 @@ spec:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values
{{- end }}

View File

@@ -74,9 +74,8 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade.
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 .Values._cluster.scheduling -}}
{{- $dedicatedNodesForWindowsVMs := get .Values._cluster.scheduling "dedicatedNodesForWindowsVMs" -}}
{{- if eq $dedicatedNodesForWindowsVMs "true" -}}
{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}}
affinity:

View File

@@ -74,9 +74,8 @@ Generate a stable UUID for cloud-init re-initialization upon upgrade.
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 .Values._cluster.scheduling -}}
{{- $dedicatedNodesForWindowsVMs := get .Values._cluster.scheduling "dedicatedNodesForWindowsVMs" -}}
{{- if eq $dedicatedNodesForWindowsVMs "true" -}}
{{- $isWindows := hasPrefix "windows" (toString .Values.instanceProfile) -}}
affinity:

View File

@@ -1,5 +1,4 @@
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
{{- $host := .Values._namespace.host }}
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (printf "%s-vpn" .Release.Name) }}
{{- $accessKeys := list }}
{{- $passwords := dict }}

View File

@@ -1,6 +1,22 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $cozystackBranding := lookup "v1" "ConfigMap" "cozy-system" "cozystack-branding" }}
{{- $cozystackScheduling := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
{{- $kubeRootCa := lookup "v1" "ConfigMap" "kube-system" "kube-root-ca.crt" }}
{{- $bundleName := index $cozyConfig.data "bundle-name" }}
{{- $bundle := tpl (.Files.Get (printf "bundles/%s.yaml" $bundleName)) . | fromYaml }}
{{/* Default values for _cluster config to ensure all required keys exist */}}
{{- $clusterDefaults := dict
"root-host" ""
"bundle-name" ""
"clusterissuer" "http01"
"oidc-enabled" "false"
"expose-services" ""
"expose-ingress" "tenant-root"
"expose-external-ips" ""
"cluster-domain" "cozy.local"
"api-server-endpoint" ""
}}
{{- $clusterConfig := mergeOverwrite $clusterDefaults ($cozyConfig.data | default dict) }}
{{- $host := "example.org" }}
{{- $host := "example.org" }}
{{- if $cozyConfig.data }}
@@ -22,6 +38,8 @@ kind: Namespace
metadata:
annotations:
helm.sh/resource-policy: keep
labels:
tenant.cozystack.io/tenant-root: ""
namespace.cozystack.io/etcd: tenant-root
namespace.cozystack.io/monitoring: tenant-root
namespace.cozystack.io/ingress: tenant-root
@@ -29,6 +47,36 @@ metadata:
namespace.cozystack.io/host: "{{ $host }}"
name: tenant-root
---
apiVersion: v1
kind: Secret
metadata:
name: cozystack-values
namespace: tenant-root
labels:
reconcile.fluxcd.io/watch: Enabled
type: Opaque
stringData:
values.yaml: |
_cluster:
{{- $clusterConfig | toYaml | nindent 6 }}
{{- with $cozystackBranding.data }}
branding:
{{- . | toYaml | nindent 8 }}
{{- end }}
{{- with $cozystackScheduling.data }}
scheduling:
{{- . | toYaml | nindent 8 }}
{{- end }}
{{- with $kubeRootCa.data }}
kube-root-ca: {{ index . "ca.crt" | b64enc | quote }}
{{- end }}
_namespace:
etcd: tenant-root
monitoring: tenant-root
ingress: tenant-root
seaweedfs: tenant-root
host: {{ $host | quote }}
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
@@ -56,6 +104,9 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
valuesFrom:
- kind: Secret
name: cozystack-values
values:
host: "{{ $host }}"
dependsOn:

View File

@@ -83,6 +83,9 @@ spec:
values:
{{- toYaml . | nindent 4}}
{{- end }}
valuesFrom:
- kind: Secret
name: cozystack-values
{{- with $x.dependsOn }}
dependsOn:

View File

@@ -1,5 +1,20 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $cozystackBranding := lookup "v1" "ConfigMap" "cozy-system" "cozystack-branding" }}
{{- $cozystackScheduling := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
{{- $bundleName := index $cozyConfig.data "bundle-name" }}
{{/* Default values for _cluster config to ensure all required keys exist */}}
{{- $clusterDefaults := dict
"root-host" ""
"bundle-name" ""
"clusterissuer" "http01"
"oidc-enabled" "false"
"expose-services" ""
"expose-ingress" "tenant-root"
"expose-external-ips" ""
"cluster-domain" "cozy.local"
"api-server-endpoint" ""
}}
{{- $clusterConfig := mergeOverwrite $clusterDefaults ($cozyConfig.data | default dict) }}
{{- $bundle := tpl (.Files.Get (printf "bundles/%s.yaml" $bundleName)) . | fromYaml }}
{{- $disabledComponents := splitList "," ((index $cozyConfig.data "bundle-disable") | default "") }}
{{- $enabledComponents := splitList "," ((index $cozyConfig.data "bundle-enable") | default "") }}
@@ -37,4 +52,25 @@ metadata:
pod-security.kubernetes.io/enforce: privileged
{{- end }}
name: {{ $namespace }}
---
apiVersion: v1
kind: Secret
metadata:
name: cozystack-values
namespace: {{ $namespace }}
labels:
reconcile.fluxcd.io/watch: Enabled
type: Opaque
stringData:
values.yaml: |
_cluster:
{{- $clusterConfig | toYaml | nindent 6 }}
{{- with $cozystackBranding.data }}
branding:
{{- . | toYaml | nindent 8 }}
{{- end }}
{{- with $cozystackScheduling.data }}
scheduling:
{{- . | toYaml | nindent 8 }}
{{- end }}
{{- end }}

0
packages/core/testing/Chart.yaml Executable file → Normal file
View File

0
packages/core/testing/Makefile Executable file → Normal file
View File

10
packages/core/testing/images/e2e-sandbox/Dockerfile Executable file → Normal file
View File

@@ -9,7 +9,7 @@ ARG TARGETOS
ARG TARGETARCH
RUN apt update -q
RUN apt install -yq --no-install-recommends psmisc genisoimage ca-certificates qemu-kvm qemu-utils iproute2 iptables wget xz-utils netcat curl jq make git
RUN apt install -yq --no-install-recommends psmisc genisoimage ca-certificates qemu-kvm qemu-utils iproute2 iptables wget xz-utils netcat curl jq make git bash-completion
RUN curl -sSL "https://github.com/siderolabs/talos/releases/download/v${TALOSCTL_VERSION}/talosctl-${TARGETOS}-${TARGETARCH}" -o /usr/local/bin/talosctl \
&& chmod +x /usr/local/bin/talosctl
RUN curl -sSL "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/${TARGETOS}/${TARGETARCH}/kubectl" -o /usr/local/bin/kubectl \
@@ -21,5 +21,13 @@ RUN curl -sSL "https://fluxcd.io/install.sh" | bash
RUN curl -sSL "https://github.com/cozystack/cozyhr/raw/refs/heads/main/hack/install.sh" | sh -s -- -v "${COZYHR_VERSION}"
RUN curl https://dl.min.io/client/mc/release/${TARGETOS}-${TARGETARCH}/mc --create-dirs -o /usr/local/bin/mc \
&& chmod +x /usr/local/bin/mc
RUN <<'EOF'
cat <<'EOT' >> /etc/bash.bashrc
. /etc/bash_completion
. <(kubectl completion bash)
alias k=kubectl
complete -F __start_kubectl k
EOT
EOF
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

0
packages/core/testing/values.yaml Executable file → Normal file
View File

View File

@@ -1,9 +1,6 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $issuerType := (index $cozyConfig.data "clusterissuer") | default "http01" }}
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:

View File

@@ -1,9 +1,4 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $issuerType := (index $cozyConfig.data "clusterissuer") | default "http01" }}
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
{{- $host := .Values._namespace.host }}
{{ range $m := .Values.machines }}
---

View File

@@ -49,10 +49,9 @@ spec:
{{- with .Values.resources }}
resources: {{- include "cozy-lib.resources.sanitize" (list . $) | nindent 10 }}
{{- end }}
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
{{- $rawConstraints := "" }}
{{- if $configMap }}
{{- $rawConstraints = get $configMap.data "globalAppTopologySpreadConstraints" }}
{{- if .Values._cluster.scheduling }}
{{- $rawConstraints = get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
{{- end }}
{{- if $rawConstraints }}
{{- $rawConstraints | fromYaml | toYaml | nindent 6 }}

View File

@@ -1,5 +1,4 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $oidcEnabled := index $cozyConfig.data "oidc-enabled" }}
{{- $oidcEnabled := index .Values._cluster "oidc-enabled" }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:

View File

@@ -1,23 +1,14 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $host := index $cozyConfig.data "root-host" }}
{{- $host := .Values._namespace.host | default (index .Values._cluster "root-host") }}
{{- $k8sClientSecret := lookup "v1" "Secret" "cozy-keycloak" "k8s-client" }}
{{- if $k8sClientSecret }}
{{- $apiServerEndpoint := index $cozyConfig.data "api-server-endpoint" }}
{{- $managementKubeconfigEndpoint := default "" (get $cozyConfig.data "management-kubeconfig-endpoint") }}
{{- $apiServerEndpoint := index .Values._cluster "api-server-endpoint" }}
{{- $managementKubeconfigEndpoint := default "" (index .Values._cluster "management-kubeconfig-endpoint") }}
{{- if and $managementKubeconfigEndpoint (ne $managementKubeconfigEndpoint "") }}
{{- $apiServerEndpoint = $managementKubeconfigEndpoint }}
{{- end }}
{{- $k8sClient := index $k8sClientSecret.data "client-secret-key" | b64dec }}
{{- $rootSaConfigMap := lookup "v1" "ConfigMap" "kube-system" "kube-root-ca.crt" }}
{{- $k8sCa := index $rootSaConfigMap.data "ca.crt" | b64enc }}
{{- if .Capabilities.APIVersions.Has "helm.toolkit.fluxcd.io/v2" }}
{{- $tenantRoot := lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" "tenant-root" "tenant-root" }}
{{- if and $tenantRoot $tenantRoot.spec $tenantRoot.spec.values $tenantRoot.spec.values.host }}
{{- $host = $tenantRoot.spec.values.host }}
{{- end }}
{{- end }}
{{- $k8sCa := index .Values._cluster "kube-root-ca" }}
---
apiVersion: v1
kind: Secret

View File

@@ -1,6 +1,5 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $exposeIngress := index $cozyConfig.data "expose-ingress" | default "tenant-root" }}
{{- $exposeExternalIPs := (index $cozyConfig.data "expose-external-ips") | default "" | nospace }}
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
{{- $exposeExternalIPs := (index .Values._cluster "expose-external-ips") | default "" | nospace }}
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
@@ -24,6 +23,9 @@ spec:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values
values:
ingress-nginx:
fullnameOverride: {{ trimPrefix "tenant-" .Release.Namespace }}-ingress

View File

@@ -5,9 +5,8 @@ metadata:
name: alerta-db
spec:
instances: 2
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
{{- if $configMap }}
{{- $rawConstraints := get $configMap.data "globalAppTopologySpreadConstraints" }}
{{- if .Values._cluster.scheduling }}
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
{{- if $rawConstraints }}
{{- $rawConstraints | fromYaml | toYaml | nindent 2 }}
labelSelector:

View File

@@ -1,9 +1,6 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $issuerType := (index $cozyConfig.data "clusterissuer") | default "http01" }}
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
{{- $apiKey := randAlphaNum 32 }}
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace "alerta" }}

View File

@@ -6,9 +6,8 @@ spec:
instances: 2
storage:
size: {{ .Values.grafana.db.size }}
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
{{- if $configMap }}
{{- $rawConstraints := get $configMap.data "globalAppTopologySpreadConstraints" }}
{{- if .Values._cluster.scheduling }}
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
{{- if $rawConstraints }}
{{- $rawConstraints | fromYaml | toYaml | nindent 2 }}
labelSelector:

View File

@@ -1,9 +1,6 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $issuerType := (index $cozyConfig.data "clusterissuer") | default "http01" }}
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
---
apiVersion: grafana.integreatly.org/v1beta1
kind: Grafana

View File

@@ -1,7 +1,5 @@
{{- if eq .Values.topology "Client" }}
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
{{- $host := .Values._namespace.host }}
---
apiVersion: apps/v1
kind: Deployment

View File

@@ -1,9 +1,5 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $issuerType := (index $cozyConfig.data "clusterissuer") | default "http01" }}
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
{{- if and (not (eq .Values.topology "Client")) (.Values.filer.grpcHost) }}
---
apiVersion: networking.k8s.io/v1

View File

@@ -34,9 +34,8 @@
{{- end }}
{{- if not (eq .Values.topology "Client") }}
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
{{- $ingress := .Values._namespace.ingress }}
{{- $host := .Values._namespace.host }}
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
@@ -60,6 +59,9 @@ spec:
force: true
remediation:
retries: -1
valuesFrom:
- kind: Secret
name: cozystack-values
values:
global:
serviceAccountName: "{{ .Release.Namespace }}-seaweedfs"

View File

@@ -1,7 +1,130 @@
{{/*
Cluster-wide configuration helpers.
These helpers read from .Values._cluster which is populated via valuesFrom from Secret cozystack-values.
*/}}
{{/*
Get the root host for the cluster.
Usage: {{ include "cozy-lib.root-host" . }}
*/}}
{{- define "cozy-lib.root-host" -}}
{{- (index .Values._cluster "root-host") | default "" }}
{{- end }}
{{/*
Get the bundle name for the cluster.
Usage: {{ include "cozy-lib.bundle-name" . }}
*/}}
{{- define "cozy-lib.bundle-name" -}}
{{- (index .Values._cluster "bundle-name") | default "" }}
{{- end }}
{{/*
Get the images registry.
Usage: {{ include "cozy-lib.images-registry" . }}
*/}}
{{- define "cozy-lib.images-registry" -}}
{{- (index .Values._cluster "images-registry") | default "" }}
{{- end }}
{{/*
Get the ipv4 cluster CIDR.
Usage: {{ include "cozy-lib.ipv4-cluster-cidr" . }}
*/}}
{{- define "cozy-lib.ipv4-cluster-cidr" -}}
{{- (index .Values._cluster "ipv4-cluster-cidr") | default "" }}
{{- end }}
{{/*
Get the ipv4 service CIDR.
Usage: {{ include "cozy-lib.ipv4-service-cidr" . }}
*/}}
{{- define "cozy-lib.ipv4-service-cidr" -}}
{{- (index .Values._cluster "ipv4-service-cidr") | default "" }}
{{- end }}
{{/*
Get the ipv4 join CIDR.
Usage: {{ include "cozy-lib.ipv4-join-cidr" . }}
*/}}
{{- define "cozy-lib.ipv4-join-cidr" -}}
{{- (index .Values._cluster "ipv4-join-cidr") | default "" }}
{{- end }}
{{/*
Get scheduling configuration.
Usage: {{ include "cozy-lib.scheduling" . }}
Returns: YAML string of scheduling configuration
*/}}
{{- define "cozy-lib.scheduling" -}}
{{- if .Values._cluster.scheduling }}
{{- .Values._cluster.scheduling | toYaml }}
{{- end }}
{{- end }}
{{/*
Get branding configuration.
Usage: {{ include "cozy-lib.branding" . }}
Returns: YAML string of branding configuration
*/}}
{{- define "cozy-lib.branding" -}}
{{- if .Values._cluster.branding }}
{{- .Values._cluster.branding | toYaml }}
{{- end }}
{{- end }}
{{/*
Namespace-specific configuration helpers.
These helpers read from .Values._namespace which is populated via valuesFrom from Secret cozystack-values.
*/}}
{{/*
Get the host for this namespace.
Usage: {{ include "cozy-lib.ns-host" . }}
*/}}
{{- define "cozy-lib.ns-host" -}}
{{- .Values._namespace.host | default "" }}
{{- end }}
{{/*
Get the etcd namespace reference.
Usage: {{ include "cozy-lib.ns-etcd" . }}
*/}}
{{- define "cozy-lib.ns-etcd" -}}
{{- .Values._namespace.etcd | default "" }}
{{- end }}
{{/*
Get the ingress namespace reference.
Usage: {{ include "cozy-lib.ns-ingress" . }}
*/}}
{{- define "cozy-lib.ns-ingress" -}}
{{- .Values._namespace.ingress | default "" }}
{{- end }}
{{/*
Get the monitoring namespace reference.
Usage: {{ include "cozy-lib.ns-monitoring" . }}
*/}}
{{- define "cozy-lib.ns-monitoring" -}}
{{- .Values._namespace.monitoring | default "" }}
{{- end }}
{{/*
Get the seaweedfs namespace reference.
Usage: {{ include "cozy-lib.ns-seaweedfs" . }}
*/}}
{{- define "cozy-lib.ns-seaweedfs" -}}
{{- .Values._namespace.seaweedfs | default "" }}
{{- end }}
{{/*
Legacy helper - kept for backward compatibility during migration.
Loads config into context. Deprecated: use direct .Values._cluster access instead.
*/}}
{{- define "cozy-lib.loadCozyConfig" }}
{{- include "cozy-lib.checkInput" . }}
{{- if not (hasKey (index . 1) "cozyConfig") }}
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $_ := set (index . 1) "cozyConfig" $cozyConfig }}
{{- $_ := set (index . 1) "cozyConfig" (dict "data" ((index . 1).Values._cluster | default dict)) }}
{{- end }}
{{- end }}

View File

@@ -1,8 +1,6 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $myNS := lookup "v1" "Namespace" "" .Release.Namespace }}
{{- $host := index $myNS.metadata.annotations "namespace.cozystack.io/host" }}
{{- $ingress := index $myNS.metadata.annotations "namespace.cozystack.io/ingress" }}
{{- $issuerType := (index $cozyConfig.data "clusterissuer") | default "http01" }}
{{- $host := .Values._namespace.host }}
{{- $ingress := .Values._namespace.ingress }}
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
apiVersion: networking.k8s.io/v1
kind: Ingress

View File

@@ -1,5 +1,4 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $issuerType := (index $cozyConfig.data "clusterissuer") | default "http01" }}
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
apiVersion: cert-manager.io/v1
kind: ClusterIssuer

View File

@@ -1,7 +1,6 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $host := index $cozyConfig.data "root-host" }}
{{- $exposeServices := splitList "," ((index $cozyConfig.data "expose-services") | default "") }}
{{- $exposeIngress := index $cozyConfig.data "expose-ingress" | default "tenant-root" }}
{{- $host := index .Values._cluster "root-host" }}
{{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }}
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
{{- if and (has "api" $exposeServices) }}
apiVersion: networking.k8s.io/v1

View File

@@ -1,4 +0,0 @@
apiVersion: v2
name: cozy-cozystack-values
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process

View File

@@ -1,65 +0,0 @@
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: cozystack-values
namespace: tenant-root
labels:
cozystack.io/repository: system
cozystack.io/system-app: "true"
spec:
interval: 5m
releaseName: cozystack-values
install:
remediation:
retries: -1
upgrade:
remediation:
retries: -1
chart:
spec:
chart: cozy-cozystack-values
reconcileStrategy: Revision
sourceRef:
kind: HelmRepository
name: cozystack-system
namespace: cozy-system
version: '>= 0.0.0-0'
valuesFrom:
# Cluster configuration from cozystack ConfigMap
# The ConfigMap data keys (root-host, bundle-name, etc.) will be mapped to _cluster
- kind: ConfigMap
name: cozystack
namespace: cozy-system
targetPath: _cluster
# Branding configuration from cozystack-branding ConfigMap
# All keys from the ConfigMap data will be nested under _cluster.branding
- kind: ConfigMap
name: cozystack-branding
namespace: cozy-system
targetPath: _cluster.branding
optional: true
# Scheduling configuration from cozystack-scheduling ConfigMap
# All keys from the ConfigMap data will be nested under _cluster.scheduling
- kind: ConfigMap
name: cozystack-scheduling
namespace: cozy-system
targetPath: _cluster.scheduling
optional: true
# Kube root CA from kube-root-ca.crt ConfigMap
# Extract the ca.crt key and place it at _cluster.kubeRootCa
- kind: ConfigMap
name: kube-root-ca.crt
namespace: kube-system
targetPath: _cluster.kubeRootCa
valuesKey: ca.crt
optional: true
values:
_namespace:
etcd: tenant-root
monitoring: tenant-root
ingress: tenant-root
seaweedfs: tenant-root
# host will be determined from _cluster.root-host or tenantRootHost
# Default to example.org if neither is set
host: "example.org"

View File

@@ -1,54 +0,0 @@
{{- /* Default values for _cluster config to ensure all required keys exist */}}
{{- $clusterDefaults := dict
"root-host" ""
"bundle-name" ""
"clusterissuer" "http01"
"oidc-enabled" "false"
"expose-services" ""
"expose-ingress" "tenant-root"
"expose-external-ips" ""
"cluster-domain" "cozy.local"
"api-server-endpoint" ""
}}
{{- $clusterConfig := mergeOverwrite $clusterDefaults (.Values._cluster | default dict) }}
{{- $host := .Values._namespace.host | default "example.org" }}
{{- if .Values._cluster }}
{{- if index .Values._cluster "root-host" }}
{{- $host = index .Values._cluster "root-host" }}
{{- end }}
{{- end }}
{{- /* Check if tenant-root HelmRelease host value is available */}}
{{- if .Values.tenantRootHost }}
{{- $host = .Values.tenantRootHost }}
{{- end }}
---
apiVersion: v1
kind: Secret
metadata:
name: cozystack-values
namespace: {{ .Values._namespace.etcd | default "tenant-root" }}
labels:
reconcile.fluxcd.io/watch: Enabled
type: Opaque
stringData:
values.yaml: |
_cluster:
{{- $clusterConfig | toYaml | nindent 6 }}
{{- with .Values._cluster.branding }}
branding:
{{- . | toYaml | nindent 8 }}
{{- end }}
{{- with .Values._cluster.scheduling }}
scheduling:
{{- . | toYaml | nindent 8 }}
{{- end }}
{{- with .Values._cluster.kubeRootCa }}
kube-root-ca: {{ . | b64enc | quote }}
{{- end }}
_namespace:
etcd: {{ .Values._namespace.etcd | default "tenant-root" | quote }}
monitoring: {{ .Values._namespace.monitoring | default "tenant-root" | quote }}
ingress: {{ .Values._namespace.ingress | default "tenant-root" | quote }}
seaweedfs: {{ .Values._namespace.seaweedfs | default "tenant-root" | quote }}
host: {{ $host | quote }}

View File

@@ -1,18 +0,0 @@
# Default values for cozystack-values chart
# These values will be populated via valuesFrom in the HelmRelease
# Cluster configuration from cozystack ConfigMap
# The ConfigMap data keys will be mapped directly to _cluster
_cluster: {}
# Namespace configuration
_namespace:
etcd: tenant-root
monitoring: tenant-root
ingress: tenant-root
seaweedfs: tenant-root
host: "example.org"
# Host value from tenant-root HelmRelease (if available)
tenantRootHost: ""

View File

@@ -1,4 +1,4 @@
{{- $brandingConfig:= lookup "v1" "ConfigMap" "cozy-system" "cozystack-branding" }}
{{- $brandingConfig := .Values._cluster.branding | default dict }}
{{- $tenantText := "v0.38.2" }}
{{- $footerText := "Cozystack" }}
@@ -16,9 +16,9 @@ metadata:
app.kubernetes.io/instance: incloud-web
app.kubernetes.io/name: web
data:
CUSTOM_TENANT_TEXT: {{ $brandingConfig | dig "data" "tenantText" $tenantText | quote }}
FOOTER_TEXT: {{ $brandingConfig | dig "data" "footerText" $footerText | quote }}
TITLE_TEXT: {{ $brandingConfig | dig "data" "titleText" $titleText | quote }}
LOGO_TEXT: {{ $brandingConfig | dig "data" "logoText" $logoText | quote }}
CUSTOM_LOGO_SVG: {{ $brandingConfig | dig "data" "logoSvg" $logoSvg | quote }}
ICON_SVG: {{ $brandingConfig | dig "data" "iconSvg" $iconSvg | quote }}
CUSTOM_TENANT_TEXT: {{ $brandingConfig.tenantText | default $tenantText | quote }}
FOOTER_TEXT: {{ $brandingConfig.footerText | default $footerText | quote }}
TITLE_TEXT: {{ $brandingConfig.titleText | default $titleText | quote }}
LOGO_TEXT: {{ $brandingConfig.logoText | default $logoText | quote }}
CUSTOM_LOGO_SVG: {{ $brandingConfig.logoSvg | default $logoSvg | quote }}
ICON_SVG: {{ $brandingConfig.iconSvg | default $iconSvg | quote }}

View File

@@ -1,6 +1,5 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $host := index $cozyConfig.data "root-host" }}
{{- $oidcEnabled := index $cozyConfig.data "oidc-enabled" }}
{{- $host := index .Values._cluster "root-host" }}
{{- $oidcEnabled := index .Values._cluster "oidc-enabled" }}
apiVersion: apps/v1
kind: Deployment

View File

@@ -1,8 +1,7 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $issuerType := (index $cozyConfig.data "clusterissuer") | default "http01" }}
{{- $host := index $cozyConfig.data "root-host" }}
{{- $exposeServices := splitList "," ((index $cozyConfig.data "expose-services") | default "") }}
{{- $exposeIngress := index $cozyConfig.data "expose-ingress" | default "tenant-root" }}
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $host := index .Values._cluster "root-host" }}
{{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }}
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
{{- if and (has "dashboard" $exposeServices) }}
apiVersion: networking.k8s.io/v1

View File

@@ -1,6 +1,5 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $host := index $cozyConfig.data "root-host" }}
{{- $extraRedirectUris := splitList "," ((index $cozyConfig.data "extra-keycloak-redirect-uri-for-dashboard") | default "") }}
{{- $host := index .Values._cluster "root-host" }}
{{- $extraRedirectUris := splitList "," ((index .Values._cluster "extra-keycloak-redirect-uri-for-dashboard") | default "") }}
{{- $existingK8sSecret := lookup "v1" "Secret" .Release.Namespace "k8s-client" }}
{{- $existingDashboardSecret := lookup "v1" "Secret" .Release.Namespace "dashboard-client" }}

View File

@@ -1,5 +1,4 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $host := index $cozyConfig.data "root-host" }}
{{- $host := index .Values._cluster "root-host" }}
apiVersion: v1
data:

View File

@@ -1,13 +1,11 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $host := index $cozyConfig.data "root-host" }}
{{- $extraRedirectUris := splitList "," ((index $cozyConfig.data "extra-keycloak-redirect-uri-for-dashboard") | default "") }}
{{- $rootSaConfigMap := lookup "v1" "ConfigMap" "kube-system" "kube-root-ca.crt" }}
{{- $k8sCa := index $rootSaConfigMap.data "ca.crt" | b64enc }}
{{- $host := index .Values._cluster "root-host" }}
{{- $extraRedirectUris := splitList "," ((index .Values._cluster "extra-keycloak-redirect-uri-for-dashboard") | default "") }}
{{- $k8sCa := index .Values._cluster "kube-root-ca" }}
{{- $existingK8sSecret := lookup "v1" "Secret" .Release.Namespace "k8s-client" }}
{{- $existingKubeappsSecret := lookup "v1" "Secret" .Release.Namespace "kubeapps-client" }}
{{- $existingAuthConfig := lookup "v1" "Secret" "cozy-dashboard" "kubeapps-auth-config" }}
{{- $cozystackBranding:= lookup "v1" "ConfigMap" "cozy-system" "cozystack-branding" }}
{{- $brandingConfig := .Values._cluster.branding | default dict }}
{{ $k8sClient := "" }}
{{- if $existingK8sSecret }}
@@ -17,8 +15,8 @@
{{- end }}
{{ $branding := "" }}
{{- if $cozystackBranding }}
{{- $branding = index $cozystackBranding.data "branding" }}
{{- if $brandingConfig }}
{{- $branding = $brandingConfig.branding }}
{{- end }}
---

View File

@@ -6,9 +6,8 @@ spec:
instances: 2
storage:
size: 20Gi
{{- $configMap := lookup "v1" "ConfigMap" "cozy-system" "cozystack-scheduling" }}
{{- if $configMap }}
{{- $rawConstraints := get $configMap.data "globalAppTopologySpreadConstraints" }}
{{- if .Values._cluster.scheduling }}
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
{{- if $rawConstraints }}
{{- $rawConstraints | fromYaml | toYaml | nindent 2 }}
labelSelector:

View File

@@ -1,7 +1,6 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $host := index $cozyConfig.data "root-host" }}
{{- $issuerType := (index $cozyConfig.data "clusterissuer") | default "http01" }}
{{- $exposeIngress := index $cozyConfig.data "expose-ingress" | default "tenant-root" }}
{{- $host := index .Values._cluster "root-host" }}
{{- $issuerType := (index .Values._cluster "clusterissuer") | default "http01" }}
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
apiVersion: networking.k8s.io/v1
kind: Ingress

View File

@@ -1,6 +1,5 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $host := index $cozyConfig.data "root-host" }}
{{- $clusterDomain := (index $cozyConfig.data "cluster-domain") | default "cozy.local" }}
{{- $host := index .Values._cluster "root-host" }}
{{- $clusterDomain := (index .Values._cluster "cluster-domain") | default "cozy.local" }}
{{- $existingPassword := lookup "v1" "Secret" "cozy-keycloak" (printf "%s-credentials" .Release.Name) }}
{{- $password := randAlphaNum 16 -}}

View File

@@ -1,7 +1,6 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $host := index $cozyConfig.data "root-host" }}
{{- $exposeServices := splitList "," ((index $cozyConfig.data "expose-services") | default "") }}
{{- $exposeIngress := index $cozyConfig.data "expose-ingress" | default "tenant-root" }}
{{- $host := index .Values._cluster "root-host" }}
{{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }}
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
{{- if and (has "cdi-uploadproxy" $exposeServices) }}

View File

@@ -1,7 +1,6 @@
{{- $cozyConfig := lookup "v1" "ConfigMap" "cozy-system" "cozystack" }}
{{- $host := index $cozyConfig.data "root-host" }}
{{- $exposeServices := splitList "," ((index $cozyConfig.data "expose-services") | default "") }}
{{- $exposeIngress := index $cozyConfig.data "expose-ingress" | default "tenant-root" }}
{{- $host := index .Values._cluster "root-host" }}
{{- $exposeServices := splitList "," ((index .Values._cluster "expose-services") | default "") }}
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
{{- if and (has "vm-exportproxy" $exposeServices) }}
apiVersion: networking.k8s.io/v1

View File

@@ -3,3 +3,6 @@ quota:
cpu: "20"
storage: "5Gi"
foobar: "3"
_cluster: {}
_namespace: {}

View File

@@ -148,6 +148,11 @@ func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation
return nil, fmt.Errorf("expected *appsv1alpha1.Application object, got %T", obj)
}
// Validate that values don't contain reserved keys (starting with "_")
if err := validateNoInternalKeys(app.Spec); err != nil {
return nil, apierrors.NewBadRequest(err.Error())
}
// Convert Application to HelmRelease
helmRelease, err := r.ConvertApplicationToHelmRelease(app)
if err != nil {
@@ -442,6 +447,11 @@ func (r *REST) Update(ctx context.Context, name string, objInfo rest.UpdatedObje
return nil, false, fmt.Errorf("expected *appsv1alpha1.Application object, got %T", newObj)
}
// Validate that values don't contain reserved keys (starting with "_")
if err := validateNoInternalKeys(app.Spec); err != nil {
return nil, false, apierrors.NewBadRequest(err.Error())
}
// Convert Application to HelmRelease
helmRelease, err := r.ConvertApplicationToHelmRelease(app)
if err != nil {
@@ -851,8 +861,49 @@ func (r *REST) ConvertApplicationToHelmRelease(app *appsv1alpha1.Application) (*
return r.convertApplicationToHelmRelease(app)
}
// filterInternalKeys removes keys starting with "_" from the JSON values
func filterInternalKeys(values *apiextv1.JSON) *apiextv1.JSON {
if values == nil || len(values.Raw) == 0 {
return values
}
var data map[string]interface{}
if err := json.Unmarshal(values.Raw, &data); err != nil {
return values
}
for key := range data {
if strings.HasPrefix(key, "_") {
delete(data, key)
}
}
filtered, err := json.Marshal(data)
if err != nil {
return values
}
return &apiextv1.JSON{Raw: filtered}
}
// validateNoInternalKeys checks that values don't contain keys starting with "_"
func validateNoInternalKeys(values *apiextv1.JSON) error {
if values == nil || len(values.Raw) == 0 {
return nil
}
var data map[string]interface{}
if err := json.Unmarshal(values.Raw, &data); err != nil {
return err
}
for key := range data {
if strings.HasPrefix(key, "_") {
return fmt.Errorf("values key %q is reserved (keys starting with '_' are not allowed)", key)
}
}
return nil
}
// convertHelmReleaseToApplication implements the actual conversion logic
func (r *REST) convertHelmReleaseToApplication(hr *helmv2.HelmRelease) (appsv1alpha1.Application, error) {
// Filter out internal keys (starting with "_") from spec
filteredSpec := filterInternalKeys(hr.Spec.Values)
app := appsv1alpha1.Application{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps.cozystack.io/v1alpha1",
@@ -868,7 +919,7 @@ func (r *REST) convertHelmReleaseToApplication(hr *helmv2.HelmRelease) (appsv1al
Labels: filterPrefixedMap(hr.Labels, LabelPrefix),
Annotations: filterPrefixedMap(hr.Annotations, AnnotationPrefix),
},
Spec: hr.Spec.Values,
Spec: filteredSpec,
Status: appsv1alpha1.ApplicationStatus{
Version: hr.Status.LastAttemptedRevision,
},
@@ -935,6 +986,12 @@ func (r *REST) convertApplicationToHelmRelease(app *appsv1alpha1.Application) (*
Retries: -1,
},
},
ValuesFrom: []helmv2.ValuesReference{
{
Kind: "Secret",
Name: "cozystack-values",
},
},
Values: app.Spec,
},
}