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 <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
Andrei KvapilandClaude committed 2026-01-14 16:09:43 +01:00
1 parent 74d71606ab
commit 43da779eee
13 files changed
+106 -222

No files matched your search

@@ -17,6 +17,7 @@ limitations under the License.
package v1alpha1 package v1alpha1
import ( import (
helmv2 "github.com/fluxcd/helm-controller/api/v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@@ -61,24 +62,6 @@ type CozystackResourceDefinitionSpec struct {
Dashboard *CozystackResourceDefinitionDashboard `json:"dashboard,omitempty"` 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 { type CozystackResourceDefinitionApplication struct {
// Kind of the application, used for UI and API // Kind of the application, used for UI and API
Kind string `json:"kind"` Kind string `json:"kind"`
@@ -91,9 +74,8 @@ type CozystackResourceDefinitionApplication struct {
} }
type CozystackResourceDefinitionRelease struct { type CozystackResourceDefinitionRelease struct {
// Helm chart configuration // Reference to the chart source
// +optional ChartRef *helmv2.CrossNamespaceSourceReference `json:"chartRef"`
Chart CozystackResourceDefinitionChart `json:"chart,omitempty"`
// Labels for the release // Labels for the release
Labels map[string]string `json:"labels,omitempty"` Labels map[string]string `json:"labels,omitempty"`
// Prefix for the release name // Prefix for the release name
+6 -32
View File
@@ -21,6 +21,7 @@ limitations under the License.
package v1alpha1 package v1alpha1
import ( import (
"github.com/fluxcd/helm-controller/api/v2"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -118,22 +119,6 @@ func (in *CozystackResourceDefinitionApplication) DeepCopy() *CozystackResourceD
return out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CozystackResourceDefinitionDashboard) DeepCopyInto(out *CozystackResourceDefinitionDashboard) { func (in *CozystackResourceDefinitionDashboard) DeepCopyInto(out *CozystackResourceDefinitionDashboard) {
*out = *in *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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CozystackResourceDefinitionRelease) DeepCopyInto(out *CozystackResourceDefinitionRelease) { func (in *CozystackResourceDefinitionRelease) DeepCopyInto(out *CozystackResourceDefinitionRelease) {
*out = *in *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 { if in.Labels != nil {
in, out := &in.Labels, &out.Labels in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in)) *out = make(map[string]string, len(*in))
@@ -622,21 +611,6 @@ func (in Selector) DeepCopy() Selector {
return *out 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Variant) DeepCopyInto(out *Variant) { func (in *Variant) DeepCopyInto(out *Variant) {
*out = *in *out = *in
+2 -2
View File
@@ -17,7 +17,7 @@ limitations under the License.
package fuzzer package fuzzer
import ( import (
"github.com/cozystack/cozystack/pkg/apis/apps" "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1"
fuzz "github.com/google/gofuzz" fuzz "github.com/google/gofuzz"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
@@ -26,7 +26,7 @@ import (
// Funcs returns the fuzzer functions for the apps api group. // Funcs returns the fuzzer functions for the apps api group.
var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
return []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 c.FuzzNoCustom(s) // fuzz self without calling this function again
}, },
} }
-40
View File
@@ -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
}
+2 -2
View File
@@ -17,7 +17,7 @@ limitations under the License.
package fuzzer package fuzzer
import ( import (
"github.com/cozystack/cozystack/pkg/apis/core" "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
fuzz "github.com/google/gofuzz" fuzz "github.com/google/gofuzz"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer" runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
@@ -26,7 +26,7 @@ import (
// Funcs returns the fuzzer functions for the core api group. // Funcs returns the fuzzer functions for the core api group.
var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
return []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 c.FuzzNoCustom(s) // fuzz self without calling this function again
}, },
} }
-40
View File
@@ -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
}
+4 -7
View File
@@ -169,13 +169,10 @@ func (o *CozyServerOptions) Complete() error {
Release: config.ReleaseConfig{ Release: config.ReleaseConfig{
Prefix: crd.Spec.Release.Prefix, Prefix: crd.Spec.Release.Prefix,
Labels: crd.Spec.Release.Labels, Labels: crd.Spec.Release.Labels,
Chart: config.ChartConfig{ ChartRef: config.ChartRefConfig{
Name: crd.Spec.Release.Chart.Name, Kind: crd.Spec.Release.ChartRef.Kind,
SourceRef: config.SourceRefConfig{ Name: crd.Spec.Release.ChartRef.Name,
Kind: crd.Spec.Release.Chart.SourceRef.Kind, Namespace: crd.Spec.Release.ChartRef.Namespace,
Name: crd.Spec.Release.Chart.SourceRef.Name,
Namespace: crd.Spec.Release.Chart.SourceRef.Namespace,
},
}, },
}, },
} }
+5 -11
View File
@@ -38,19 +38,13 @@ type ApplicationConfig struct {
// ReleaseConfig contains the release settings. // ReleaseConfig contains the release settings.
type ReleaseConfig struct { type ReleaseConfig struct {
Prefix string `yaml:"prefix"` Prefix string `yaml:"prefix"`
Labels map[string]string `yaml:"labels"` Labels map[string]string `yaml:"labels"`
Chart ChartConfig `yaml:"chart"` ChartRef ChartRefConfig `yaml:"chartRef"`
} }
// ChartConfig contains the chart settings. // ChartRefConfig references a Flux source artifact for the Helm chart.
type ChartConfig struct { type ChartRefConfig struct {
Name string `yaml:"name"`
SourceRef SourceRefConfig `yaml:"sourceRef"`
}
// SourceRefConfig contains the reference to the chart source.
type SourceRefConfig struct {
Kind string `yaml:"kind"` Kind string `yaml:"kind"`
Name string `yaml:"name"` Name string `yaml:"name"`
Namespace string `yaml:"namespace"` Namespace string `yaml:"namespace"`
+43 -6
View File
@@ -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{ Properties: map[string]spec.Schema{
"major": { "major": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Default: "", Description: "Major is the major version of the binary version",
Type: []string{"string"}, Default: "",
Format: "", Type: []string{"string"},
Format: "",
}, },
}, },
"minor": { "minor": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Default: "", Description: "Minor is the minor version of the binary version",
Type: []string{"string"}, Default: "",
Format: "", 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": { "gitVersion": {
+5
View File
@@ -22,6 +22,11 @@ const (
HRLabel = "helm.toolkit.fluxcd.io/name" 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 { type ObjectID struct {
APIVersion string APIVersion string
Kind string Kind string
+32 -1
View File
@@ -4,8 +4,10 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"strings"
"testing" "testing"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/go-logr/logr" "github.com/go-logr/logr"
"github.com/go-logr/zapr" "github.com/go-logr/zapr"
"go.uber.org/zap" "go.uber.org/zap"
@@ -41,12 +43,41 @@ func init() {
ctx = logr.NewContext(context.Background(), l) 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) { func TestWalkingOwnershipGraph(t *testing.T) {
obj, err := dynClient.Resource(schema.GroupVersionResource{"", "v1", "pods"}).Namespace(os.Args[1]).Get(ctx, os.Args[2], metav1.GetOptions{}) obj, err := dynClient.Resource(schema.GroupVersionResource{"", "v1", "pods"}).Namespace(os.Args[1]).Get(ctx, os.Args[2], metav1.GetOptions{})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
nodes := WalkOwnershipGraph(ctx, dynClient, mapper, &stubMapper{}, obj) nodes := WalkOwnershipGraph(ctx, dynClient, mapper, &labelsMapper{}, obj)
for _, node := range nodes { for _, node := range nodes {
fmt.Printf("%#v\n", node) fmt.Printf("%#v\n", node)
} }
-49
View File
@@ -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
}
+4 -11
View File
@@ -973,17 +973,10 @@ func (r *REST) convertApplicationToHelmRelease(app *appsv1alpha1.Application) (*
UID: app.UID, UID: app.UID,
}, },
Spec: helmv2.HelmReleaseSpec{ Spec: helmv2.HelmReleaseSpec{
Chart: &helmv2.HelmChartTemplate{ ChartRef: &helmv2.CrossNamespaceSourceReference{
Spec: helmv2.HelmChartTemplateSpec{ Kind: r.releaseConfig.ChartRef.Kind,
Chart: r.releaseConfig.Chart.Name, Name: r.releaseConfig.ChartRef.Name,
Version: ">= 0.0.0-0", Namespace: r.releaseConfig.ChartRef.Namespace,
ReconcileStrategy: "Revision",
SourceRef: helmv2.CrossNamespaceObjectReference{
Kind: r.releaseConfig.Chart.SourceRef.Kind,
Name: r.releaseConfig.Chart.SourceRef.Name,
Namespace: r.releaseConfig.Chart.SourceRef.Namespace,
},
},
}, },
Interval: metav1.Duration{Duration: 5 * time.Minute}, Interval: metav1.Duration{Duration: 5 * time.Minute},
Install: &helmv2.Install{ Install: &helmv2.Install{