From 43da779eee58759e22188901c232c8618288f2ce Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Wed, 14 Jan 2026 15:41:05 +0100 Subject: [PATCH] feat(api): add chartRef to CozystackResourceDefinition Replace the chart field with chartRef for referencing Helm charts via ExternalArtifact resources. This enables the Package controller to manage chart sources centrally. Changes: - Add chartRef field to CozystackResourceDefinition spec - Remove chart field (deprecated) - Remove validation (moved to controller) - Update lineage mapper for new field structure - Regenerate openapi specs Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../cozystackresourcedefinitions_types.go | 24 ++------- api/v1alpha1/zz_generated.deepcopy.go | 38 +++----------- pkg/apis/apps/fuzzer/fuzzer.go | 4 +- pkg/apis/apps/validation/validation.go | 40 --------------- pkg/apis/core/fuzzer/fuzzer.go | 4 +- pkg/apis/core/validation/validation.go | 40 --------------- pkg/cmd/server/start.go | 11 ++--- pkg/config/config.go | 16 ++---- pkg/generated/openapi/zz_generated.openapi.go | 49 ++++++++++++++++--- pkg/lineage/lineage.go | 5 ++ pkg/lineage/lineage_test.go | 33 ++++++++++++- pkg/lineage/mapper.go | 49 ------------------- pkg/registry/apps/application/rest.go | 15 ++---- 13 files changed, 106 insertions(+), 222 deletions(-) delete mode 100644 pkg/apis/apps/validation/validation.go delete mode 100644 pkg/apis/core/validation/validation.go delete mode 100644 pkg/lineage/mapper.go diff --git a/api/v1alpha1/cozystackresourcedefinitions_types.go b/api/v1alpha1/cozystackresourcedefinitions_types.go index 2f25fb2d..04b24294 100644 --- a/api/v1alpha1/cozystackresourcedefinitions_types.go +++ b/api/v1alpha1/cozystackresourcedefinitions_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha1 import ( + helmv2 "github.com/fluxcd/helm-controller/api/v2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -61,24 +62,6 @@ type CozystackResourceDefinitionSpec struct { Dashboard *CozystackResourceDefinitionDashboard `json:"dashboard,omitempty"` } -type CozystackResourceDefinitionChart struct { - // Name of the Helm chart - Name string `json:"name"` - // Source reference for the Helm chart - SourceRef SourceRef `json:"sourceRef"` -} - -type SourceRef struct { - // Kind of the source reference - // +kubebuilder:default:="HelmRepository" - Kind string `json:"kind"` - // Name of the source reference - Name string `json:"name"` - // Namespace of the source reference - // +kubebuilder:default:="cozy-public" - Namespace string `json:"namespace"` -} - type CozystackResourceDefinitionApplication struct { // Kind of the application, used for UI and API Kind string `json:"kind"` @@ -91,9 +74,8 @@ type CozystackResourceDefinitionApplication struct { } type CozystackResourceDefinitionRelease struct { - // Helm chart configuration - // +optional - Chart CozystackResourceDefinitionChart `json:"chart,omitempty"` + // Reference to the chart source + ChartRef *helmv2.CrossNamespaceSourceReference `json:"chartRef"` // Labels for the release Labels map[string]string `json:"labels,omitempty"` // Prefix for the release name diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 7fe4f3e2..f060990f 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1alpha1 import ( + "github.com/fluxcd/helm-controller/api/v2" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -118,22 +119,6 @@ func (in *CozystackResourceDefinitionApplication) DeepCopy() *CozystackResourceD return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *CozystackResourceDefinitionChart) DeepCopyInto(out *CozystackResourceDefinitionChart) { - *out = *in - out.SourceRef = in.SourceRef -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionChart. -func (in *CozystackResourceDefinitionChart) DeepCopy() *CozystackResourceDefinitionChart { - if in == nil { - return nil - } - out := new(CozystackResourceDefinitionChart) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CozystackResourceDefinitionDashboard) DeepCopyInto(out *CozystackResourceDefinitionDashboard) { *out = *in @@ -205,7 +190,11 @@ func (in *CozystackResourceDefinitionList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CozystackResourceDefinitionRelease) DeepCopyInto(out *CozystackResourceDefinitionRelease) { *out = *in - out.Chart = in.Chart + if in.ChartRef != nil { + in, out := &in.ChartRef, &out.ChartRef + *out = new(v2.CrossNamespaceSourceReference) + **out = **in + } if in.Labels != nil { in, out := &in.Labels, &out.Labels *out = make(map[string]string, len(*in)) @@ -622,21 +611,6 @@ func (in Selector) DeepCopy() Selector { return *out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SourceRef) DeepCopyInto(out *SourceRef) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceRef. -func (in *SourceRef) DeepCopy() *SourceRef { - if in == nil { - return nil - } - out := new(SourceRef) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Variant) DeepCopyInto(out *Variant) { *out = *in diff --git a/pkg/apis/apps/fuzzer/fuzzer.go b/pkg/apis/apps/fuzzer/fuzzer.go index fd744ed6..c92c5799 100644 --- a/pkg/apis/apps/fuzzer/fuzzer.go +++ b/pkg/apis/apps/fuzzer/fuzzer.go @@ -17,7 +17,7 @@ limitations under the License. package fuzzer import ( - "github.com/cozystack/cozystack/pkg/apis/apps" + "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" fuzz "github.com/google/gofuzz" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" @@ -26,7 +26,7 @@ import ( // Funcs returns the fuzzer functions for the apps api group. var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { return []interface{}{ - func(s *apps.ApplicationSpec, c fuzz.Continue) { + func(s *v1alpha1.Application, c fuzz.Continue) { c.FuzzNoCustom(s) // fuzz self without calling this function again }, } diff --git a/pkg/apis/apps/validation/validation.go b/pkg/apis/apps/validation/validation.go deleted file mode 100644 index 84c20c54..00000000 --- a/pkg/apis/apps/validation/validation.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2024 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 validation - -import ( - "github.com/cozystack/cozystack/pkg/apis/apps" - "k8s.io/apimachinery/pkg/util/validation/field" -) - -// ValidateApplication validates a Application. -func ValidateApplication(f *apps.Application) field.ErrorList { - allErrs := field.ErrorList{} - - allErrs = append(allErrs, ValidateApplicationSpec(&f.Spec, field.NewPath("spec"))...) - - return allErrs -} - -// ValidateApplicationSpec validates a ApplicationSpec. -func ValidateApplicationSpec(s *apps.ApplicationSpec, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - - // TODO validation - - return allErrs -} diff --git a/pkg/apis/core/fuzzer/fuzzer.go b/pkg/apis/core/fuzzer/fuzzer.go index dbf3ca39..82a1b5ab 100644 --- a/pkg/apis/core/fuzzer/fuzzer.go +++ b/pkg/apis/core/fuzzer/fuzzer.go @@ -17,7 +17,7 @@ limitations under the License. package fuzzer import ( - "github.com/cozystack/cozystack/pkg/apis/core" + "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" fuzz "github.com/google/gofuzz" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" @@ -26,7 +26,7 @@ import ( // Funcs returns the fuzzer functions for the core api group. var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { return []interface{}{ - func(s *core.TenantNamespaceSpec, c fuzz.Continue) { + func(s *v1alpha1.TenantNamespace, c fuzz.Continue) { c.FuzzNoCustom(s) // fuzz self without calling this function again }, } diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go deleted file mode 100644 index 060067de..00000000 --- a/pkg/apis/core/validation/validation.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2024 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 validation - -import ( - "github.com/cozystack/cozystack/pkg/apis/core" - "k8s.io/apimachinery/pkg/util/validation/field" -) - -// ValidateTenantNamespace validates a TenantNamespace. -func ValidateTenantNamespace(f *core.TenantNamespace) field.ErrorList { - allErrs := field.ErrorList{} - - allErrs = append(allErrs, ValidateTenantNamespaceSpec(&f.Spec, field.NewPath("spec"))...) - - return allErrs -} - -// ValidateTenantNamespaceSpec validates a TenantNamespaceSpec. -func ValidateTenantNamespaceSpec(s *core.TenantNamespaceSpec, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - - // TODO validation - - return allErrs -} diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go index 5da8254e..2826cfd5 100644 --- a/pkg/cmd/server/start.go +++ b/pkg/cmd/server/start.go @@ -169,13 +169,10 @@ func (o *CozyServerOptions) Complete() error { Release: config.ReleaseConfig{ Prefix: crd.Spec.Release.Prefix, Labels: crd.Spec.Release.Labels, - Chart: config.ChartConfig{ - Name: crd.Spec.Release.Chart.Name, - SourceRef: config.SourceRefConfig{ - Kind: crd.Spec.Release.Chart.SourceRef.Kind, - Name: crd.Spec.Release.Chart.SourceRef.Name, - Namespace: crd.Spec.Release.Chart.SourceRef.Namespace, - }, + ChartRef: config.ChartRefConfig{ + Kind: crd.Spec.Release.ChartRef.Kind, + Name: crd.Spec.Release.ChartRef.Name, + Namespace: crd.Spec.Release.ChartRef.Namespace, }, }, } diff --git a/pkg/config/config.go b/pkg/config/config.go index 390918a6..1e123e2c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -38,19 +38,13 @@ type ApplicationConfig struct { // ReleaseConfig contains the release settings. type ReleaseConfig struct { - Prefix string `yaml:"prefix"` - Labels map[string]string `yaml:"labels"` - Chart ChartConfig `yaml:"chart"` + Prefix string `yaml:"prefix"` + Labels map[string]string `yaml:"labels"` + ChartRef ChartRefConfig `yaml:"chartRef"` } -// ChartConfig contains the chart settings. -type ChartConfig struct { - Name string `yaml:"name"` - SourceRef SourceRefConfig `yaml:"sourceRef"` -} - -// SourceRefConfig contains the reference to the chart source. -type SourceRefConfig struct { +// ChartRefConfig references a Flux source artifact for the Helm chart. +type ChartRefConfig struct { Kind string `yaml:"kind"` Name string `yaml:"name"` Namespace string `yaml:"namespace"` diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index 4202bbac..ba5ab71b 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -2714,6 +2714,13 @@ func schema_pkg_apis_meta_v1_DeleteOptions(ref common.ReferenceCallback) common. }, }, }, + "ignoreStoreReadErrorWithClusterBreakingPotential": { + SchemaProps: spec.SchemaProps{ + Description: "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + Type: []string{"boolean"}, + Format: "", + }, + }, }, }, }, @@ -4601,16 +4608,46 @@ func schema_k8sio_apimachinery_pkg_version_Info(ref common.ReferenceCallback) co Properties: map[string]spec.Schema{ "major": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "Major is the major version of the binary version", + Default: "", + Type: []string{"string"}, + Format: "", }, }, "minor": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "Minor is the minor version of the binary version", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "emulationMajor": { + SchemaProps: spec.SchemaProps{ + Description: "EmulationMajor is the major version of the emulation version", + Type: []string{"string"}, + Format: "", + }, + }, + "emulationMinor": { + SchemaProps: spec.SchemaProps{ + Description: "EmulationMinor is the minor version of the emulation version", + Type: []string{"string"}, + Format: "", + }, + }, + "minCompatibilityMajor": { + SchemaProps: spec.SchemaProps{ + Description: "MinCompatibilityMajor is the major version of the minimum compatibility version", + Type: []string{"string"}, + Format: "", + }, + }, + "minCompatibilityMinor": { + SchemaProps: spec.SchemaProps{ + Description: "MinCompatibilityMinor is the minor version of the minimum compatibility version", + Type: []string{"string"}, + Format: "", }, }, "gitVersion": { diff --git a/pkg/lineage/lineage.go b/pkg/lineage/lineage.go index 867e1aab..94be77b7 100644 --- a/pkg/lineage/lineage.go +++ b/pkg/lineage/lineage.go @@ -22,6 +22,11 @@ const ( HRLabel = "helm.toolkit.fluxcd.io/name" ) +// AppMapper maps HelmRelease to application metadata. +type AppMapper interface { + Map(*helmv2.HelmRelease) (apiVersion, kind, prefix string, err error) +} + type ObjectID struct { APIVersion string Kind string diff --git a/pkg/lineage/lineage_test.go b/pkg/lineage/lineage_test.go index a705f26c..7da09dfc 100644 --- a/pkg/lineage/lineage_test.go +++ b/pkg/lineage/lineage_test.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "os" + "strings" "testing" + helmv2 "github.com/fluxcd/helm-controller/api/v2" "github.com/go-logr/logr" "github.com/go-logr/zapr" "go.uber.org/zap" @@ -41,12 +43,41 @@ func init() { ctx = logr.NewContext(context.Background(), l) } +// labelsMapper implements AppMapper using HelmRelease labels. +type labelsMapper struct{} + +func (m *labelsMapper) Map(hr *helmv2.HelmRelease) (string, string, string, error) { + if hr.Labels == nil { + return "", "", "", fmt.Errorf("cannot map helm release %s/%s: labels are nil", hr.Namespace, hr.Name) + } + + appKind, ok := hr.Labels["apps.cozystack.io/application.kind"] + if !ok { + return "", "", "", fmt.Errorf("cannot map helm release %s/%s: missing application.kind label", hr.Namespace, hr.Name) + } + + appGroup, ok := hr.Labels["apps.cozystack.io/application.group"] + if !ok { + return "", "", "", fmt.Errorf("cannot map helm release %s/%s: missing application.group label", hr.Namespace, hr.Name) + } + + appName, ok := hr.Labels["apps.cozystack.io/application.name"] + if !ok { + return "", "", "", fmt.Errorf("cannot map helm release %s/%s: missing application.name label", hr.Namespace, hr.Name) + } + + apiVersion := fmt.Sprintf("%s/v1alpha1", appGroup) + prefix := strings.TrimSuffix(hr.Name, appName) + + return apiVersion, appKind, prefix, nil +} + func TestWalkingOwnershipGraph(t *testing.T) { obj, err := dynClient.Resource(schema.GroupVersionResource{"", "v1", "pods"}).Namespace(os.Args[1]).Get(ctx, os.Args[2], metav1.GetOptions{}) if err != nil { t.Fatal(err) } - nodes := WalkOwnershipGraph(ctx, dynClient, mapper, &stubMapper{}, obj) + nodes := WalkOwnershipGraph(ctx, dynClient, mapper, &labelsMapper{}, obj) for _, node := range nodes { fmt.Printf("%#v\n", node) } diff --git a/pkg/lineage/mapper.go b/pkg/lineage/mapper.go deleted file mode 100644 index c424b288..00000000 --- a/pkg/lineage/mapper.go +++ /dev/null @@ -1,49 +0,0 @@ -package lineage - -import ( - "fmt" - "strings" - - helmv2 "github.com/fluxcd/helm-controller/api/v2" -) - -type AppMapper interface { - Map(*helmv2.HelmRelease) (apiVersion, kind, prefix string, err error) -} - -type stubMapper struct{} - -var stubMapperMap = map[string]string{ - "cozystack-extra/bootbox": "apps.cozystack.io/v1alpha1/BootBox/", - "cozystack-apps/bucket": "apps.cozystack.io/v1alpha1/Bucket/bucket-", - "cozystack-apps/clickhouse": "apps.cozystack.io/v1alpha1/ClickHouse/clickhouse-", - "cozystack-extra/etcd": "apps.cozystack.io/v1alpha1/Etcd/", - "cozystack-apps/ferretdb": "apps.cozystack.io/v1alpha1/FerretDB/ferretdb-", - "cozystack-apps/http-cache": "apps.cozystack.io/v1alpha1/HTTPCache/http-cache-", - "cozystack-extra/info": "apps.cozystack.io/v1alpha1/Info/", - "cozystack-extra/ingress": "apps.cozystack.io/v1alpha1/Ingress/", - "cozystack-apps/kafka": "apps.cozystack.io/v1alpha1/Kafka/kafka-", - "cozystack-apps/kubernetes": "apps.cozystack.io/v1alpha1/Kubernetes/kubernetes-", - "cozystack-extra/monitoring": "apps.cozystack.io/v1alpha1/Monitoring/", - "cozystack-apps/mysql": "apps.cozystack.io/v1alpha1/MySQL/mysql-", - "cozystack-apps/nats": "apps.cozystack.io/v1alpha1/NATS/nats-", - "cozystack-apps/postgres": "apps.cozystack.io/v1alpha1/Postgres/postgres-", - "cozystack-apps/rabbitmq": "apps.cozystack.io/v1alpha1/RabbitMQ/rabbitmq-", - "cozystack-apps/redis": "apps.cozystack.io/v1alpha1/Redis/redis-", - "cozystack-extra/seaweedfs": "apps.cozystack.io/v1alpha1/SeaweedFS/", - "cozystack-apps/tcp-balancer": "apps.cozystack.io/v1alpha1/TCPBalancer/tcp-balancer-", - "cozystack-apps/tenant": "apps.cozystack.io/v1alpha1/Tenant/tenant-", - "cozystack-apps/virtual-machine": "apps.cozystack.io/v1alpha1/VirtualMachine/virtual-machine-", - "cozystack-apps/vm-disk": "apps.cozystack.io/v1alpha1/VMDisk/vm-disk-", - "cozystack-apps/vm-instance": "apps.cozystack.io/v1alpha1/VMInstance/vm-instance-", - "cozystack-apps/vpn": "apps.cozystack.io/v1alpha1/VPN/vpn-", -} - -func (s *stubMapper) Map(hr *helmv2.HelmRelease) (string, string, string, error) { - val, ok := stubMapperMap[hr.Spec.Chart.Spec.SourceRef.Name+"/"+hr.Spec.Chart.Spec.Chart] - if !ok { - return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app", hr.Namespace, hr.Name) - } - split := strings.Split(val, "/") - return strings.Join(split[:2], "/"), split[2], split[3], nil -} diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go index c46b5cad..6e0ae048 100644 --- a/pkg/registry/apps/application/rest.go +++ b/pkg/registry/apps/application/rest.go @@ -973,17 +973,10 @@ func (r *REST) convertApplicationToHelmRelease(app *appsv1alpha1.Application) (* UID: app.UID, }, Spec: helmv2.HelmReleaseSpec{ - Chart: &helmv2.HelmChartTemplate{ - Spec: helmv2.HelmChartTemplateSpec{ - Chart: r.releaseConfig.Chart.Name, - Version: ">= 0.0.0-0", - ReconcileStrategy: "Revision", - SourceRef: helmv2.CrossNamespaceObjectReference{ - Kind: r.releaseConfig.Chart.SourceRef.Kind, - Name: r.releaseConfig.Chart.SourceRef.Name, - Namespace: r.releaseConfig.Chart.SourceRef.Namespace, - }, - }, + ChartRef: &helmv2.CrossNamespaceSourceReference{ + Kind: r.releaseConfig.ChartRef.Kind, + Name: r.releaseConfig.ChartRef.Name, + Namespace: r.releaseConfig.ChartRef.Namespace, }, Interval: metav1.Duration{Duration: 5 * time.Minute}, Install: &helmv2.Install{