refactor a bit
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
parent
c69756de51
commit
e046206d2b
9 changed files with 394 additions and 313 deletions
|
|
@ -24,6 +24,8 @@ import (
|
|||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
"github.com/cozystack/cozystack/pkg/cozylib"
|
||||
)
|
||||
|
||||
// +kubebuilder:rbac:groups=cozystack.io,resources=cozystackresourcedefinitions,verbs=get;list;watch
|
||||
|
|
@ -358,7 +360,7 @@ func (r *CozystackResourceDefinitionReconciler) updateHelmReleaseChart(ctx conte
|
|||
var err error
|
||||
if crd.Spec.Release.Values != nil {
|
||||
logger.V(4).Info("Merging values from CRD", "name", hr.Name, "namespace", hr.Namespace, "crd", crd.Name)
|
||||
mergedValues, err = r.mergeHelmReleaseValues(crd.Spec.Release.Values, hrCopy.Spec.Values)
|
||||
mergedValues, err = cozylib.MergeValuesWithCRDPriority(crd.Spec.Release.Values, hrCopy.Spec.Values)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to merge values", "name", hr.Name, "namespace", hr.Namespace)
|
||||
return fmt.Errorf("failed to merge values: %w", err)
|
||||
|
|
@ -368,12 +370,15 @@ func (r *CozystackResourceDefinitionReconciler) updateHelmReleaseChart(ctx conte
|
|||
mergedValues = hrCopy.Spec.Values
|
||||
}
|
||||
|
||||
// Always inject namespace labels (top-level _namespace field)
|
||||
// Always inject namespace annotations (top-level _namespace field)
|
||||
// This matches the behavior in cozystack-api and NamespaceHelmReconciler
|
||||
mergedValues, err = r.injectNamespaceLabelsIntoValues(ctx, mergedValues, hrCopy.Namespace)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to inject namespace labels", "name", hr.Name, "namespace", hr.Namespace)
|
||||
// Continue even if namespace labels injection fails
|
||||
namespace := &corev1.Namespace{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: hrCopy.Namespace}, namespace); err == nil {
|
||||
mergedValues, err = cozylib.InjectNamespaceAnnotationsIntoValues(mergedValues, namespace)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to inject namespace annotations", "name", hr.Name, "namespace", hr.Namespace)
|
||||
// Continue even if namespace annotations injection fails
|
||||
}
|
||||
}
|
||||
|
||||
// Always update values to ensure _cozystack and _namespace are applied
|
||||
|
|
@ -495,47 +500,3 @@ func valuesEqual(a, b *apiextensionsv1.JSON) bool {
|
|||
return string(a.Raw) == string(b.Raw)
|
||||
}
|
||||
|
||||
// injectNamespaceLabelsIntoValues injects namespace.cozystack.io/* labels into _namespace (top-level)
|
||||
// This matches the behavior in cozystack-api and NamespaceHelmReconciler
|
||||
func (r *CozystackResourceDefinitionReconciler) injectNamespaceLabelsIntoValues(ctx context.Context, values *apiextensionsv1.JSON, namespaceName string) (*apiextensionsv1.JSON, error) {
|
||||
// Get namespace to extract namespace.cozystack.io/* labels
|
||||
namespace := &corev1.Namespace{}
|
||||
if err := r.Get(ctx, client.ObjectKey{Name: namespaceName}, namespace); err != nil {
|
||||
// If namespace not found, return values as-is
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// Extract namespace.cozystack.io/* labels
|
||||
namespaceLabels := extractNamespaceLabelsFromNamespace(namespace)
|
||||
if len(namespaceLabels) == 0 {
|
||||
// No namespace labels, return values as-is
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// Parse values
|
||||
var valuesMap map[string]interface{}
|
||||
if values != nil && len(values.Raw) > 0 {
|
||||
if err := json.Unmarshal(values.Raw, &valuesMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal values: %w", err)
|
||||
}
|
||||
} else {
|
||||
valuesMap = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Convert namespaceLabels from map[string]string to map[string]interface{}
|
||||
namespaceLabelsMap := make(map[string]interface{})
|
||||
for k, v := range namespaceLabels {
|
||||
namespaceLabelsMap[k] = v
|
||||
}
|
||||
|
||||
// Namespace labels completely overwrite existing _namespace field (top-level)
|
||||
valuesMap["_namespace"] = namespaceLabelsMap
|
||||
|
||||
// Marshal back to JSON
|
||||
mergedJSON, err := json.Marshal(valuesMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal values with namespace labels: %w", err)
|
||||
}
|
||||
|
||||
return &apiextensionsv1.JSON{Raw: mergedJSON}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
|
@ -29,6 +28,8 @@ import (
|
|||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
"github.com/cozystack/cozystack/pkg/cozylib"
|
||||
)
|
||||
|
||||
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch
|
||||
|
|
@ -49,8 +50,8 @@ func (r *NamespaceHelmReconciler) Reconcile(ctx context.Context, req ctrl.Reques
|
|||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// Extract namespace.cozystack.io/* labels
|
||||
namespaceLabels := extractNamespaceLabelsFromNamespace(namespace)
|
||||
// Extract namespace.cozystack.io/* annotations
|
||||
namespaceLabels := cozylib.ExtractNamespaceAnnotations(namespace)
|
||||
if len(namespaceLabels) == 0 {
|
||||
// No namespace labels to process, skip
|
||||
return ctrl.Result{}, nil
|
||||
|
|
@ -83,25 +84,6 @@ func (r *NamespaceHelmReconciler) Reconcile(ctx context.Context, req ctrl.Reques
|
|||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// extractNamespaceLabelsFromNamespace extracts namespace.cozystack.io/* labels from namespace
|
||||
func extractNamespaceLabelsFromNamespace(ns *corev1.Namespace) map[string]string {
|
||||
namespaceLabels := make(map[string]string)
|
||||
prefix := "namespace.cozystack.io/"
|
||||
|
||||
if ns.Labels == nil {
|
||||
return namespaceLabels
|
||||
}
|
||||
|
||||
for key, value := range ns.Labels {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
// Remove prefix and add to namespace labels
|
||||
namespaceKey := strings.TrimPrefix(key, prefix)
|
||||
namespaceLabels[namespaceKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
return namespaceLabels
|
||||
}
|
||||
|
||||
// updateHelmReleaseWithNamespaceLabels updates HelmRelease values with namespace labels
|
||||
func (r *NamespaceHelmReconciler) updateHelmReleaseWithNamespaceLabels(ctx context.Context, hr *helmv2.HelmRelease, namespaceLabels map[string]string) error {
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@ cozystackOperator:
|
|||
image: ghcr.io/cozystack/cozystack/cozystack-operator:latest@sha256:ede9a0a6b7b1ad137ef1f75c1e954115da8ebdab110e47d920ae01ac62be93ec
|
||||
disableTelemetry: false
|
||||
cozystackVersion: "latest"
|
||||
packagesDigest: sha256:e0b444176a041831f79fab0264e643171c16f8519b52854c27b1fad4b4abbf5e
|
||||
packagesDigest: sha256:da9d726066e8ee884210f01df4d59a29560ecda596cc01fba340c0f21ae36d7a
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
cozystackAPI:
|
||||
image: ghcr.io/cozystack/cozystack/cozystack-api:latest@sha256:51cbc43b73e2f2608ae451c3c14b316f6df8508e2a53a6a4d5b577f3c698b5b3
|
||||
image: ghcr.io/cozystack/cozystack/cozystack-api:latest@sha256:fa43ce97a77d4f2e4d877a1b525dabc91ec4d8a8b865c55d9d470992e9027405
|
||||
localK8sAPIEndpoint:
|
||||
enabled: true
|
||||
replicas: 2
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
cozystackController:
|
||||
image: ghcr.io/cozystack/cozystack/cozystack-controller:latest@sha256:44679c58a9a8d5431dba9b4b512ec97cb252750fd30914c5afa4addf00f50479
|
||||
image: ghcr.io/cozystack/cozystack/cozystack-controller:latest@sha256:196c769a58423f108ffca1760805f022155c63accab38c6a5bd736c6008b8b54
|
||||
debug: false
|
||||
cozystackAPIKind: "DaemonSet"
|
||||
|
|
|
|||
96
pkg/cozylib/namespace.go
Normal file
96
pkg/cozylib/namespace.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
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 cozylib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
// NamespaceAnnotationPrefix is the prefix for namespace annotations that should be copied to _namespace values
|
||||
NamespaceAnnotationPrefix = "namespace.cozystack.io/"
|
||||
)
|
||||
|
||||
// ExtractNamespaceAnnotations extracts namespace.cozystack.io/* annotations from namespace
|
||||
// and returns them as a map with the prefix removed.
|
||||
// For example, "namespace.cozystack.io/host" becomes "host" in the returned map.
|
||||
func ExtractNamespaceAnnotations(ns *corev1.Namespace) map[string]string {
|
||||
result := make(map[string]string)
|
||||
prefix := NamespaceAnnotationPrefix
|
||||
|
||||
if ns.Annotations == nil {
|
||||
return result
|
||||
}
|
||||
|
||||
for key, value := range ns.Annotations {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
// Remove prefix and add to result
|
||||
namespaceKey := strings.TrimPrefix(key, prefix)
|
||||
result[namespaceKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// InjectNamespaceAnnotationsIntoValues injects namespace.cozystack.io/* annotations into _namespace (top-level) in values.
|
||||
// This function extracts annotations from the namespace and adds them to the _namespace field in the values JSON.
|
||||
// If namespace is nil or has no matching annotations, values are returned as-is.
|
||||
func InjectNamespaceAnnotationsIntoValues(values *apiextensionsv1.JSON, ns *corev1.Namespace) (*apiextensionsv1.JSON, error) {
|
||||
if ns == nil {
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// Extract namespace.cozystack.io/* annotations
|
||||
namespaceLabels := ExtractNamespaceAnnotations(ns)
|
||||
if len(namespaceLabels) == 0 {
|
||||
// No namespace annotations, return values as-is
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// Parse values
|
||||
var valuesMap map[string]interface{}
|
||||
if values != nil && len(values.Raw) > 0 {
|
||||
if err := json.Unmarshal(values.Raw, &valuesMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal values: %w", err)
|
||||
}
|
||||
} else {
|
||||
valuesMap = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Convert namespaceLabels from map[string]string to map[string]interface{}
|
||||
namespaceLabelsMap := make(map[string]interface{})
|
||||
for k, v := range namespaceLabels {
|
||||
namespaceLabelsMap[k] = v
|
||||
}
|
||||
|
||||
// Namespace annotations completely overwrite existing _namespace field (top-level)
|
||||
valuesMap["_namespace"] = namespaceLabelsMap
|
||||
|
||||
// Marshal back to JSON
|
||||
mergedJSON, err := json.Marshal(valuesMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal values with namespace annotations: %w", err)
|
||||
}
|
||||
|
||||
return &apiextensionsv1.JSON{Raw: mergedJSON}, nil
|
||||
}
|
||||
266
pkg/cozylib/values.go
Normal file
266
pkg/cozylib/values.go
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
/*
|
||||
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 cozylib
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
)
|
||||
|
||||
// DeepMergeMaps performs a deep merge of two maps.
|
||||
// Values from override map take precedence, but nested maps are merged recursively.
|
||||
func DeepMergeMaps(base, override map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// Copy base map
|
||||
for k, v := range base {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
// Merge override map
|
||||
for k, v := range override {
|
||||
if baseVal, exists := result[k]; exists {
|
||||
// If both are maps, recursively merge
|
||||
if baseMap, ok := baseVal.(map[string]interface{}); ok {
|
||||
if overrideMap, ok := v.(map[string]interface{}); ok {
|
||||
result[k] = DeepMergeMaps(baseMap, overrideMap)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// Override takes precedence for non-map values or new keys
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MergeValues merges two JSON values with deep merge.
|
||||
// baseValues are merged first, then overrideValues (overrideValues take precedence).
|
||||
func MergeValues(baseValues, overrideValues *apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
|
||||
var baseMap, overrideMap map[string]interface{}
|
||||
|
||||
if baseValues != nil && len(baseValues.Raw) > 0 {
|
||||
if err := json.Unmarshal(baseValues.Raw, &baseMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal base values: %w", err)
|
||||
}
|
||||
} else {
|
||||
baseMap = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if overrideValues != nil && len(overrideValues.Raw) > 0 {
|
||||
if err := json.Unmarshal(overrideValues.Raw, &overrideMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal override values: %w", err)
|
||||
}
|
||||
} else {
|
||||
overrideMap = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Deep merge: baseValues first, then overrideValues (overrideValues override)
|
||||
merged := DeepMergeMaps(baseMap, overrideMap)
|
||||
|
||||
mergedJSON, err := json.Marshal(merged)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal merged values: %w", err)
|
||||
}
|
||||
|
||||
return &apiextensionsv1.JSON{Raw: mergedJSON}, nil
|
||||
}
|
||||
|
||||
// MergeValuesWithCRDPriority merges CRD values with existing values.
|
||||
// Existing values have priority (user values override defaults), but _cozystack and _namespace
|
||||
// from CRD completely overwrite existing values.
|
||||
func MergeValuesWithCRDPriority(crdValues, existingValues *apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
|
||||
// If CRD has no values, preserve existing
|
||||
if crdValues == nil || len(crdValues.Raw) == 0 {
|
||||
return existingValues, nil
|
||||
}
|
||||
|
||||
// If existing has no values, use CRD values
|
||||
if existingValues == nil || len(existingValues.Raw) == 0 {
|
||||
return crdValues, nil
|
||||
}
|
||||
|
||||
var crdMap, existingMap map[string]interface{}
|
||||
|
||||
// Parse CRD values (defaults)
|
||||
if err := json.Unmarshal(crdValues.Raw, &crdMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal CRD values: %w", err)
|
||||
}
|
||||
|
||||
// Parse existing HelmRelease values
|
||||
if err := json.Unmarshal(existingValues.Raw, &existingMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal existing values: %w", err)
|
||||
}
|
||||
|
||||
// Start with existing values as base (user values take priority)
|
||||
// Then merge CRD values on top, but _cozystack and _namespace from CRD completely overwrite
|
||||
merged := DeepMergeMaps(existingMap, crdMap)
|
||||
|
||||
// Explicitly handle "_cozystack" field: CRD values completely overwrite existing
|
||||
// This ensures _cozystack field from CRD is always used, even if user modified it
|
||||
if crdCozystack, exists := crdMap["_cozystack"]; exists {
|
||||
merged["_cozystack"] = crdCozystack
|
||||
}
|
||||
|
||||
// Explicitly handle "_namespace" field: CRD values completely overwrite existing
|
||||
// This ensures _namespace field from CRD is always used, even if user modified it
|
||||
if crdNamespace, exists := crdMap["_namespace"]; exists {
|
||||
merged["_namespace"] = crdNamespace
|
||||
}
|
||||
|
||||
mergedJSON, err := json.Marshal(merged)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal merged values: %w", err)
|
||||
}
|
||||
|
||||
return &apiextensionsv1.JSON{Raw: mergedJSON}, nil
|
||||
}
|
||||
|
||||
// RemoveUnderscoreFields recursively removes all fields starting with "_" from values.
|
||||
// This is used to hide internal fields from API responses.
|
||||
func RemoveUnderscoreFields(values *apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
|
||||
if values == nil || len(values.Raw) == 0 {
|
||||
return values, nil
|
||||
}
|
||||
|
||||
var valuesMap map[string]interface{}
|
||||
if err := json.Unmarshal(values.Raw, &valuesMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal values: %w", err)
|
||||
}
|
||||
|
||||
removeUnderscoreFieldsRecursive(valuesMap)
|
||||
|
||||
// Always return at least an empty JSON object, never nil
|
||||
if len(valuesMap) == 0 {
|
||||
return &apiextensionsv1.JSON{Raw: []byte("{}")}, nil
|
||||
}
|
||||
|
||||
cleanedJSON, err := json.Marshal(valuesMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal cleaned values: %w", err)
|
||||
}
|
||||
|
||||
return &apiextensionsv1.JSON{Raw: cleanedJSON}, nil
|
||||
}
|
||||
|
||||
// removeUnderscoreFieldsRecursive recursively removes all fields starting with "_" from a map
|
||||
func removeUnderscoreFieldsRecursive(m map[string]interface{}) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
// Collect keys to delete (we can't delete while iterating)
|
||||
keysToDelete := make([]string, 0)
|
||||
for k, v := range m {
|
||||
if strings.HasPrefix(k, "_") {
|
||||
keysToDelete = append(keysToDelete, k)
|
||||
} else if nestedMap, ok := v.(map[string]interface{}); ok {
|
||||
// Recursively process nested maps
|
||||
removeUnderscoreFieldsRecursive(nestedMap)
|
||||
} else if nestedArray, ok := v.([]interface{}); ok {
|
||||
// Process arrays that might contain maps
|
||||
for _, item := range nestedArray {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
removeUnderscoreFieldsRecursive(itemMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete collected keys
|
||||
for _, k := range keysToDelete {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveUnderscoreFieldsFromMap recursively removes all fields starting with "_" from a map.
|
||||
// This is a variant that works directly with map[string]any (used in defaulting).
|
||||
func RemoveUnderscoreFieldsFromMap(m map[string]any) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
// Collect keys to delete (we can't delete while iterating)
|
||||
keysToDelete := make([]string, 0)
|
||||
for k, v := range m {
|
||||
if strings.HasPrefix(k, "_") {
|
||||
keysToDelete = append(keysToDelete, k)
|
||||
} else if nestedMap, ok := v.(map[string]any); ok {
|
||||
// Recursively process nested maps
|
||||
RemoveUnderscoreFieldsFromMap(nestedMap)
|
||||
} else if nestedArray, ok := v.([]any); ok {
|
||||
// Process arrays that might contain maps
|
||||
for _, item := range nestedArray {
|
||||
if itemMap, ok := item.(map[string]any); ok {
|
||||
RemoveUnderscoreFieldsFromMap(itemMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete collected keys
|
||||
for _, k := range keysToDelete {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckUnderscoreFields checks if any field starting with "_" exists in user values and returns an error if it does.
|
||||
// This prevents users from setting internal fields.
|
||||
func CheckUnderscoreFields(values *apiextensionsv1.JSON) error {
|
||||
if values == nil || len(values.Raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var valuesMap map[string]interface{}
|
||||
if err := json.Unmarshal(values.Raw, &valuesMap); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal values: %w", err)
|
||||
}
|
||||
|
||||
if hasUnderscoreFields(valuesMap) {
|
||||
return fmt.Errorf("fields starting with '_' are not allowed in user values")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasUnderscoreFields recursively checks if any field starting with "_" exists in a map
|
||||
func hasUnderscoreFields(m map[string]interface{}) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
for k, v := range m {
|
||||
if strings.HasPrefix(k, "_") {
|
||||
return true
|
||||
}
|
||||
if nestedMap, ok := v.(map[string]interface{}); ok {
|
||||
if hasUnderscoreFields(nestedMap) {
|
||||
return true
|
||||
}
|
||||
} else if nestedArray, ok := v.([]interface{}); ok {
|
||||
for _, item := range nestedArray {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
if hasUnderscoreFields(itemMap) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ import (
|
|||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
|
||||
"github.com/cozystack/cozystack/pkg/config"
|
||||
"github.com/cozystack/cozystack/pkg/cozylib"
|
||||
internalapiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
|
|
@ -1052,182 +1053,6 @@ func (r *REST) getCozystackResourceDefinition(ctx context.Context) (*cozyv1alpha
|
|||
return nil, fmt.Errorf("CozystackResourceDefinition not found for kind %s", r.kindName)
|
||||
}
|
||||
|
||||
// extractNamespaceLabels extracts namespace.cozystack.io/* labels from namespace and converts them to cozystack.namespace values
|
||||
func extractNamespaceLabels(ns *corev1.Namespace) map[string]interface{} {
|
||||
namespaceValues := make(map[string]interface{})
|
||||
prefix := "namespace.cozystack.io/"
|
||||
|
||||
if ns.Labels == nil {
|
||||
return namespaceValues
|
||||
}
|
||||
|
||||
for key, value := range ns.Labels {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
// Remove prefix and add to namespace values
|
||||
namespaceKey := strings.TrimPrefix(key, prefix)
|
||||
namespaceValues[namespaceKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
return namespaceValues
|
||||
}
|
||||
|
||||
// mergeValues merges two JSON values, with userValues taking precedence
|
||||
func mergeValues(defaultValues, userValues *apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
|
||||
var defaultMap, userMap map[string]interface{}
|
||||
|
||||
if defaultValues != nil && len(defaultValues.Raw) > 0 {
|
||||
if err := json.Unmarshal(defaultValues.Raw, &defaultMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal default values: %w", err)
|
||||
}
|
||||
} else {
|
||||
defaultMap = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if userValues != nil && len(userValues.Raw) > 0 {
|
||||
if err := json.Unmarshal(userValues.Raw, &userMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal user values: %w", err)
|
||||
}
|
||||
} else {
|
||||
userMap = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Deep merge: defaultValues first, then userValues (userValues override)
|
||||
merged := deepMergeMaps(defaultMap, userMap)
|
||||
|
||||
mergedJSON, err := json.Marshal(merged)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal merged values: %w", err)
|
||||
}
|
||||
|
||||
return &apiextensionsv1.JSON{Raw: mergedJSON}, nil
|
||||
}
|
||||
|
||||
// deepMergeMaps performs a deep merge of two maps
|
||||
func deepMergeMaps(base, override map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
// Copy base map
|
||||
for k, v := range base {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
// Merge override map
|
||||
for k, v := range override {
|
||||
if baseVal, exists := result[k]; exists {
|
||||
// If both are maps, recursively merge
|
||||
if baseMap, ok := baseVal.(map[string]interface{}); ok {
|
||||
if overrideMap, ok := v.(map[string]interface{}); ok {
|
||||
result[k] = deepMergeMaps(baseMap, overrideMap)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// Override takes precedence
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// removeUnderscoreFields recursively removes all fields starting with "_" from values
|
||||
func removeUnderscoreFields(values *apiextensionsv1.JSON) (*apiextensionsv1.JSON, error) {
|
||||
if values == nil || len(values.Raw) == 0 {
|
||||
return values, nil
|
||||
}
|
||||
|
||||
var valuesMap map[string]interface{}
|
||||
if err := json.Unmarshal(values.Raw, &valuesMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal values: %w", err)
|
||||
}
|
||||
|
||||
// Recursively remove all fields starting with "_"
|
||||
removeUnderscoreFieldsRecursive(valuesMap)
|
||||
|
||||
// If map is empty, return empty JSON object instead of nil to ensure proper serialization
|
||||
if len(valuesMap) == 0 {
|
||||
return &apiextensionsv1.JSON{Raw: []byte("{}")}, nil
|
||||
}
|
||||
|
||||
cleanedJSON, err := json.Marshal(valuesMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal cleaned values: %w", err)
|
||||
}
|
||||
|
||||
return &apiextensionsv1.JSON{Raw: cleanedJSON}, nil
|
||||
}
|
||||
|
||||
// removeUnderscoreFieldsRecursive recursively removes all fields starting with "_" from a map
|
||||
func removeUnderscoreFieldsRecursive(m map[string]interface{}) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
// Collect keys to delete (we can't delete while iterating)
|
||||
keysToDelete := make([]string, 0)
|
||||
for k, v := range m {
|
||||
if strings.HasPrefix(k, "_") {
|
||||
keysToDelete = append(keysToDelete, k)
|
||||
} else if nestedMap, ok := v.(map[string]interface{}); ok {
|
||||
// Recursively process nested maps
|
||||
removeUnderscoreFieldsRecursive(nestedMap)
|
||||
} else if nestedArray, ok := v.([]interface{}); ok {
|
||||
// Process arrays that might contain maps
|
||||
for _, item := range nestedArray {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
removeUnderscoreFieldsRecursive(itemMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete collected keys
|
||||
for _, k := range keysToDelete {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
|
||||
// checkUnderscoreFields checks if any field starting with "_" exists in user values and returns an error if it does
|
||||
func checkUnderscoreFields(values *apiextensionsv1.JSON) error {
|
||||
if values == nil || len(values.Raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var valuesMap map[string]interface{}
|
||||
if err := json.Unmarshal(values.Raw, &valuesMap); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal values: %w", err)
|
||||
}
|
||||
|
||||
// Check for any field starting with "_"
|
||||
if found := findUnderscoreFields(valuesMap); found != "" {
|
||||
return fmt.Errorf("field %s is not allowed in user-specified values (fields starting with '_' are reserved)", found)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findUnderscoreFields recursively finds the first field starting with "_" and returns its key path
|
||||
func findUnderscoreFields(m map[string]interface{}) string {
|
||||
for k, v := range m {
|
||||
if strings.HasPrefix(k, "_") {
|
||||
return k
|
||||
}
|
||||
if nestedMap, ok := v.(map[string]interface{}); ok {
|
||||
if found := findUnderscoreFields(nestedMap); found != "" {
|
||||
return k + "." + found
|
||||
}
|
||||
} else if nestedArray, ok := v.([]interface{}); ok {
|
||||
for i, item := range nestedArray {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
if found := findUnderscoreFields(itemMap); found != "" {
|
||||
return fmt.Sprintf("%s[%d].%s", k, i, found)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ConvertHelmReleaseToApplication converts a HelmRelease to an Application
|
||||
func (r *REST) ConvertHelmReleaseToApplication(hr *helmv2.HelmRelease) (appsv1alpha1.Application, error) {
|
||||
klog.V(6).Infof("Converting HelmRelease to Application for resource %s", hr.GetName())
|
||||
|
|
@ -1246,7 +1071,7 @@ func (r *REST) ConvertHelmReleaseToApplication(hr *helmv2.HelmRelease) (appsv1al
|
|||
// Remove all fields starting with "_" after applying defaults to ensure they're not shown to user
|
||||
// This must be done after applySpecDefaults because defaults might add them back
|
||||
if app.Spec != nil && len(app.Spec.Raw) > 0 {
|
||||
cleanedValues, err := removeUnderscoreFields(app.Spec)
|
||||
cleanedValues, err := cozylib.RemoveUnderscoreFields(app.Spec)
|
||||
if err != nil {
|
||||
return app, fmt.Errorf("failed to remove underscore fields from values: %w", err)
|
||||
}
|
||||
|
|
@ -1288,7 +1113,7 @@ func (r *REST) ConvertApplicationToHelmRelease(app *appsv1alpha1.Application) (*
|
|||
// convertHelmReleaseToApplication implements the actual conversion logic
|
||||
func (r *REST) convertHelmReleaseToApplication(hr *helmv2.HelmRelease) (appsv1alpha1.Application, error) {
|
||||
// Remove all fields starting with "_" from values before setting spec to ensure they never appear in spec
|
||||
cleanedValues, err := removeUnderscoreFields(hr.Spec.Values)
|
||||
cleanedValues, err := cozylib.RemoveUnderscoreFields(hr.Spec.Values)
|
||||
if err != nil {
|
||||
// If removal fails, use original values (shouldn't happen, but be safe)
|
||||
cleanedValues = hr.Spec.Values
|
||||
|
|
@ -1342,7 +1167,7 @@ func (r *REST) convertApplicationToHelmRelease(app *appsv1alpha1.Application) (*
|
|||
ctx := context.Background()
|
||||
|
||||
// Check if user specified any field starting with "_" in values
|
||||
if err := checkUnderscoreFields(app.Spec); err != nil {
|
||||
if err := cozylib.CheckUnderscoreFields(app.Spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -1357,7 +1182,7 @@ func (r *REST) convertApplicationToHelmRelease(app *appsv1alpha1.Application) (*
|
|||
// Start with default values from CRD (if any)
|
||||
var mergedValues *apiextensionsv1.JSON
|
||||
if crd != nil && crd.Spec.Release.Values != nil {
|
||||
mergedValues, err = mergeValues(crd.Spec.Release.Values, app.Spec)
|
||||
mergedValues, err = cozylib.MergeValues(crd.Spec.Release.Values, app.Spec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to merge default values with user values: %w", err)
|
||||
}
|
||||
|
|
@ -1374,35 +1199,12 @@ func (r *REST) convertApplicationToHelmRelease(app *appsv1alpha1.Application) (*
|
|||
namespace = nil
|
||||
}
|
||||
|
||||
// Extract namespace labels and add to namespace (top-level)
|
||||
// Extract namespace annotations and add to _namespace (top-level)
|
||||
if namespace != nil {
|
||||
namespaceLabels := extractNamespaceLabels(namespace)
|
||||
if len(namespaceLabels) > 0 {
|
||||
// Parse merged values to add namespace labels
|
||||
var valuesMap map[string]interface{}
|
||||
if mergedValues != nil && len(mergedValues.Raw) > 0 {
|
||||
if err := json.Unmarshal(mergedValues.Raw, &valuesMap); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal merged values: %w", err)
|
||||
}
|
||||
} else {
|
||||
valuesMap = make(map[string]interface{})
|
||||
}
|
||||
|
||||
// Convert namespaceLabels to map[string]interface{}
|
||||
namespaceLabelsMap := make(map[string]interface{})
|
||||
for k, v := range namespaceLabels {
|
||||
namespaceLabelsMap[k] = v
|
||||
}
|
||||
|
||||
// Namespace labels completely overwrite existing _namespace field (top-level)
|
||||
valuesMap["_namespace"] = namespaceLabelsMap
|
||||
|
||||
// Marshal back to JSON
|
||||
mergedJSON, err := json.Marshal(valuesMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal values with namespace labels: %w", err)
|
||||
}
|
||||
mergedValues = &apiextensionsv1.JSON{Raw: mergedJSON}
|
||||
var err error
|
||||
mergedValues, err = cozylib.InjectNamespaceAnnotationsIntoValues(mergedValues, namespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to inject namespace annotations: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import (
|
|||
appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
|
||||
"github.com/cozystack/cozystack/pkg/cozylib"
|
||||
)
|
||||
|
||||
// applySpecDefaults applies default values to the Application spec based on the schema
|
||||
|
|
@ -41,7 +43,7 @@ func (r *REST) applySpecDefaults(app *appsv1alpha1.Application) error {
|
|||
m = map[string]any{}
|
||||
}
|
||||
// Remove all fields starting with "_" BEFORE applying defaults to prevent them from being processed
|
||||
removeUnderscoreFieldsFromMap(m)
|
||||
cozylib.RemoveUnderscoreFieldsFromMap(m)
|
||||
|
||||
if err := defaultLikeKubernetes(&m, r.specSchema); err != nil {
|
||||
return err
|
||||
|
|
@ -49,7 +51,7 @@ func (r *REST) applySpecDefaults(app *appsv1alpha1.Application) error {
|
|||
|
||||
// Remove all fields starting with "_" AFTER applying defaults to ensure they're never in the output
|
||||
// This is a safety measure in case defaults added them back
|
||||
removeUnderscoreFieldsFromMap(m)
|
||||
cozylib.RemoveUnderscoreFieldsFromMap(m)
|
||||
|
||||
// Always return at least an empty JSON object, never nil
|
||||
if len(m) == 0 {
|
||||
|
|
@ -65,34 +67,6 @@ func (r *REST) applySpecDefaults(app *appsv1alpha1.Application) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// removeUnderscoreFieldsFromMap recursively removes all fields starting with "_" from a map
|
||||
func removeUnderscoreFieldsFromMap(m map[string]any) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
// Collect keys to delete (we can't delete while iterating)
|
||||
keysToDelete := make([]string, 0)
|
||||
for k, v := range m {
|
||||
if strings.HasPrefix(k, "_") {
|
||||
keysToDelete = append(keysToDelete, k)
|
||||
} else if nestedMap, ok := v.(map[string]any); ok {
|
||||
// Recursively process nested maps
|
||||
removeUnderscoreFieldsFromMap(nestedMap)
|
||||
} else if nestedArray, ok := v.([]any); ok {
|
||||
// Process arrays that might contain maps
|
||||
for _, item := range nestedArray {
|
||||
if itemMap, ok := item.(map[string]any); ok {
|
||||
removeUnderscoreFieldsFromMap(itemMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete collected keys
|
||||
for _, k := range keysToDelete {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultLikeKubernetes(root *map[string]any, s *structuralschema.Structural) error {
|
||||
v := any(*root)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue