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 Kvapil 2026-01-14 15:41:05 +01:00
parent 74d71606ab
commit 43da779eee
No known key found for this signature in database
GPG key ID: 931CF7FEACEAF765
13 changed files with 106 additions and 222 deletions

View file

@ -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

View file

@ -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

View file

@ -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
},
}

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
}

View file

@ -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
},
}

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
}

View file

@ -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,
},
},
}

View file

@ -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"`

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{
"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": {

View file

@ -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

View file

@ -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)
}

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
}

View file

@ -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{