From 562145e69bc7ce1f3a760dc6dbad4abcec01e1a3 Mon Sep 17 00:00:00 2001 From: Timofei Larkin Date: Mon, 8 Sep 2025 22:29:59 +0300 Subject: [PATCH] [cozystack-controller] Ancestor tracking webhook Many resources created as part of managed apps in cozystack (pods, secrets, etc) do not carry predictable labels that unambiguously indicate which app originally triggered their creation. Some resources are managed by controllers and other custom resources and this indirection can lead to loss of information. Other controllers sometimes simply do not allow setting labels on controlled resources and the latter do not inherit labels from the owner. This patch implements a webhook that sidesteps this problem with a universal solution. On creation of a pod/secret/PVC etc it walks through the owner references until a HelmRelease is found that can be matched with a managed app dynamically registered in the Cozystack API server. The pod is mutated with labels identifying the managed app. ```release-note [cozystack-controller] Add a mutating webhook to identify the Cozystack managed app that ultimately owns low-level resources created in the cluster and label these resources with a reference to said app. ``` Signed-off-by: Timofei Larkin --- cmd/cozystack-controller/main.go | 15 ++ internal/lineagecontrollerwebhook/config.go | 44 ++++ .../lineagecontrollerwebhook/controller.go | 55 +++++ internal/lineagecontrollerwebhook/matcher.go | 34 +++ internal/lineagecontrollerwebhook/types.go | 23 +++ internal/lineagecontrollerwebhook/webhook.go | 180 ++++++++++++++++ .../clickhouse/images/clickhouse-backup.tag | 2 +- .../apps/http-cache/images/nginx-cache.tag | 2 +- .../kubernetes/images/cluster-autoscaler.tag | 2 +- .../images/kubevirt-cloud-provider.tag | 2 +- .../kubernetes/images/kubevirt-csi-driver.tag | 2 +- .../images/ubuntu-container-disk.tag | 2 +- packages/apps/mysql/images/mariadb-backup.tag | 2 +- packages/core/installer/values.yaml | 2 +- packages/core/testing/values.yaml | 2 +- packages/extra/bootbox/images/matchbox.tag | 2 +- packages/extra/monitoring/images/grafana.tag | 2 +- .../images/objectstorage-sidecar.tag | 2 +- packages/system/bucket/images/s3manager.tag | 2 +- packages/system/cilium/values.yaml | 2 +- packages/system/cozystack-api/values.yaml | 2 +- .../templates/certmanager.yaml | 45 ++++ .../templates/deployment.yaml | 13 +- .../mutatingwebhookconfiguration.yaml | 37 ++++ .../templates/service.yaml | 15 ++ .../system/cozystack-controller/values.yaml | 4 +- .../templates/dashboard/configmap.yaml | 2 +- packages/system/dashboard/values.yaml | 8 +- packages/system/kamaji/values.yaml | 4 +- packages/system/kubeovn-plunger/values.yaml | 2 +- packages/system/kubeovn-webhook/values.yaml | 2 +- packages/system/kubeovn/values.yaml | 2 +- packages/system/kubevirt-csi-node/values.yaml | 2 +- packages/system/metallb/values.yaml | 4 +- .../objectstorage-controller/values.yaml | 2 +- packages/system/seaweedfs/values.yaml | 2 +- pkg/lineage/lineage.go | 195 ++++++++++++++++++ pkg/lineage/lineage_test.go | 53 +++++ pkg/lineage/mapper.go | 49 +++++ 39 files changed, 789 insertions(+), 33 deletions(-) create mode 100644 internal/lineagecontrollerwebhook/config.go create mode 100644 internal/lineagecontrollerwebhook/controller.go create mode 100644 internal/lineagecontrollerwebhook/matcher.go create mode 100644 internal/lineagecontrollerwebhook/types.go create mode 100644 internal/lineagecontrollerwebhook/webhook.go create mode 100644 packages/system/cozystack-controller/templates/certmanager.yaml create mode 100644 packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml create mode 100644 packages/system/cozystack-controller/templates/service.yaml create mode 100644 pkg/lineage/lineage.go create mode 100644 pkg/lineage/lineage_test.go create mode 100644 pkg/lineage/mapper.go diff --git a/cmd/cozystack-controller/main.go b/cmd/cozystack-controller/main.go index 40220b85..82f6cc15 100644 --- a/cmd/cozystack-controller/main.go +++ b/cmd/cozystack-controller/main.go @@ -38,6 +38,7 @@ import ( cozystackiov1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" "github.com/cozystack/cozystack/internal/controller" + lcw "github.com/cozystack/cozystack/internal/lineagecontrollerwebhook" "github.com/cozystack/cozystack/internal/telemetry" helmv2 "github.com/fluxcd/helm-controller/api/v2" @@ -214,6 +215,20 @@ func main() { os.Exit(1) } + // special one that's both a webhook and a reconciler + lineageControllerWebhook := &lcw.LineageControllerWebhook{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + } + if err := lineageControllerWebhook.SetupWithManagerAsController(mgr); err != nil { + setupLog.Error(err, "unable to setup controller", "controller", "LineageController") + os.Exit(1) + } + if err := lineageControllerWebhook.SetupWithManagerAsWebhook(mgr); err != nil { + setupLog.Error(err, "unable to setup webhook", "webhook", "LineageWebhook") + os.Exit(1) + } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/internal/lineagecontrollerwebhook/config.go b/internal/lineagecontrollerwebhook/config.go new file mode 100644 index 00000000..57fc4ad9 --- /dev/null +++ b/internal/lineagecontrollerwebhook/config.go @@ -0,0 +1,44 @@ +package lineagecontrollerwebhook + +import ( + "fmt" + + cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" +) + +type chartRef struct { + repo string + chart string +} + +type appRef struct { + group string + kind string +} + +type runtimeConfig struct { + chartAppMap map[chartRef]*cozyv1alpha1.CozystackResourceDefinition + appCRDMap map[appRef]*cozyv1alpha1.CozystackResourceDefinition +} + +func (l *LineageControllerWebhook) initConfig() { + l.initOnce.Do(func() { + if l.config.Load() == nil { + l.config.Store(&runtimeConfig{chartAppMap: make(map[chartRef]*cozyv1alpha1.CozystackResourceDefinition)}) + } + }) +} + +func (l *LineageControllerWebhook) Map(hr *helmv2.HelmRelease) (string, string, string, error) { + cfg, ok := l.config.Load().(*runtimeConfig) + if !ok { + return "", "", "", fmt.Errorf("failed to load chart-app mapping from config") + } + s := hr.Spec.Chart.Spec + val, ok := cfg.chartAppMap[chartRef{s.SourceRef.Name, s.Chart}] + if !ok { + return "", "", "", fmt.Errorf("cannot map helm release %s/%s to dynamic app", hr.Namespace, hr.Name) + } + return "apps.cozystack.io/v1alpha1", val.Spec.Application.Kind, val.Spec.Release.Prefix, nil +} diff --git a/internal/lineagecontrollerwebhook/controller.go b/internal/lineagecontrollerwebhook/controller.go new file mode 100644 index 00000000..7a1eb1d0 --- /dev/null +++ b/internal/lineagecontrollerwebhook/controller.go @@ -0,0 +1,55 @@ +package lineagecontrollerwebhook + +import ( + "context" + + cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// +kubebuilder:rbac:groups=cozystack.io,resources=cozystackresourcedefinitions,verbs=list;watch + +func (c *LineageControllerWebhook) SetupWithManagerAsController(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&cozyv1alpha1.CozystackResourceDefinition{}). + Complete(c) +} + +func (c *LineageControllerWebhook) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + l := log.FromContext(ctx) + crds := &cozyv1alpha1.CozystackResourceDefinitionList{} + if err := c.List(ctx, crds, &client.ListOptions{Namespace: "cozy-system"}); err != nil { + l.Error(err, "failed reading CozystackResourceDefinitions") + return ctrl.Result{}, err + } + cfg := &runtimeConfig{ + chartAppMap: make(map[chartRef]*cozyv1alpha1.CozystackResourceDefinition), + appCRDMap: make(map[appRef]*cozyv1alpha1.CozystackResourceDefinition), + } + for _, crd := range crds.Items { + chRef := chartRef{ + crd.Spec.Release.Chart.SourceRef.Name, + crd.Spec.Release.Chart.Name, + } + appRef := appRef{ + "apps.cozystack.io", + crd.Spec.Application.Kind, + } + + newRef := crd + if _, exists := cfg.chartAppMap[chRef]; exists { + l.Info("duplicate chart mapping detected; ignoring subsequent entry", "key", chRef) + } else { + cfg.chartAppMap[chRef] = &newRef + } + if _, exists := cfg.appCRDMap[appRef]; exists { + l.Info("duplicate app mapping detected; ignoring subsequent entry", "key", appRef) + } else { + cfg.appCRDMap[appRef] = &newRef + } + } + c.config.Store(cfg) + return ctrl.Result{}, nil +} diff --git a/internal/lineagecontrollerwebhook/matcher.go b/internal/lineagecontrollerwebhook/matcher.go new file mode 100644 index 00000000..ff0da8b4 --- /dev/null +++ b/internal/lineagecontrollerwebhook/matcher.go @@ -0,0 +1,34 @@ +package lineagecontrollerwebhook + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +func matchLabelsToSelector(l map[string]string, s *metav1.LabelSelector) bool { + // TODO: emit warning if error + sel, err := metav1.LabelSelectorAsSelector(s) + if err != nil { + return false + } + return sel.Matches(labels.Set(l)) +} + +func matchLabelsToSelectorArray(l map[string]string, ss []*metav1.LabelSelector) bool { + for _, s := range ss { + if matchLabelsToSelector(l, s) { + return true + } + } + return false +} + +func matchLabelsToExcludeInclude(l map[string]string, ex, in []*metav1.LabelSelector) bool { + if matchLabelsToSelectorArray(l, ex) { + return false + } + if matchLabelsToSelectorArray(l, in) { + return true + } + return false +} diff --git a/internal/lineagecontrollerwebhook/types.go b/internal/lineagecontrollerwebhook/types.go new file mode 100644 index 00000000..f423ae95 --- /dev/null +++ b/internal/lineagecontrollerwebhook/types.go @@ -0,0 +1,23 @@ +package lineagecontrollerwebhook + +import ( + "sync" + "sync/atomic" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/dynamic" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// +kubebuilder:webhook:path=/mutate-lineage,mutating=true,failurePolicy=Fail,sideEffects=None,groups="",resources=pods,secrets,services,persistentvolumeclaims,verbs=create;update,versions=v1,name=mlineage.cozystack.io,admissionReviewVersions={v1} +type LineageControllerWebhook struct { + client.Client + Scheme *runtime.Scheme + decoder admission.Decoder + dynClient dynamic.Interface + mapper meta.RESTMapper + config atomic.Value + initOnce sync.Once +} diff --git a/internal/lineagecontrollerwebhook/webhook.go b/internal/lineagecontrollerwebhook/webhook.go new file mode 100644 index 00000000..ae4d1ea1 --- /dev/null +++ b/internal/lineagecontrollerwebhook/webhook.go @@ -0,0 +1,180 @@ +package lineagecontrollerwebhook + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/cozystack/cozystack/pkg/lineage" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/discovery/cached/memory" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +var ( + NoAncestors = fmt.Errorf("no managed apps found in lineage") + AncestryAmbiguous = fmt.Errorf("object ancestry is ambiguous") +) + +// SetupWithManager registers the handler with the webhook server. +func (h *LineageControllerWebhook) SetupWithManagerAsWebhook(mgr ctrl.Manager) error { + cfg := rest.CopyConfig(mgr.GetConfig()) + + var err error + h.dynClient, err = dynamic.NewForConfig(cfg) + if err != nil { + return err + } + + discoClient, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + return err + } + + cachedDisco := memory.NewMemCacheClient(discoClient) + h.mapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDisco) + + h.initConfig() + // Register HTTP path -> handler. + mgr.GetWebhookServer().Register("/mutate-lineage", &admission.Webhook{Handler: h}) + + return nil +} + +// InjectDecoder lets controller-runtime give us a decoder for AdmissionReview requests. +func (h *LineageControllerWebhook) InjectDecoder(d admission.Decoder) error { + h.decoder = d + return nil +} + +// Handle is called for each AdmissionReview that matches the webhook config. +func (h *LineageControllerWebhook) Handle(ctx context.Context, req admission.Request) admission.Response { + logger := log.FromContext(ctx).WithValues( + "gvk", req.Kind.String(), + "namespace", req.Namespace, + "name", req.Name, + "operation", req.Operation, + ) + warn := make(admission.Warnings, 0) + + obj := &unstructured.Unstructured{} + if err := h.decodeUnstructured(req, obj); err != nil { + return admission.Errored(400, fmt.Errorf("decode object: %w", err)) + } + + labels, err := h.computeLabels(ctx, obj) + for { + if err != nil && errors.Is(err, NoAncestors) { + return admission.Allowed("object not managed by app") + } + if err != nil && errors.Is(err, AncestryAmbiguous) { + warn = append(warn, "object ancestry ambiguous, using first ancestor found") + break + } + if err != nil { + logger.Error(err, "error computing lineage labels") + return admission.Errored(500, fmt.Errorf("error computing lineage labels: %w", err)) + } + if err == nil { + break + } + } + + h.applyLabels(obj, labels) + + mutated, err := json.Marshal(obj) + if err != nil { + return admission.Errored(500, fmt.Errorf("marshal mutated pod: %w", err)) + } + logger.V(1).Info("mutated pod", "namespace", obj.GetNamespace(), "name", obj.GetName()) + return admission.PatchResponseFromRaw(req.Object.Raw, mutated).WithWarnings(warn...) +} + +func (h *LineageControllerWebhook) computeLabels(ctx context.Context, o *unstructured.Unstructured) (map[string]string, error) { + owners := lineage.WalkOwnershipGraph(ctx, h.dynClient, h.mapper, h, o) + if len(owners) == 0 { + return nil, NoAncestors + } + obj, err := owners[0].GetUnstructured(ctx, h.dynClient, h.mapper) + if err != nil { + return nil, err + } + gv, err := schema.ParseGroupVersion(obj.GetAPIVersion()) + if err != nil { + // should never happen, we got an APIVersion right from the API + return nil, fmt.Errorf("could not parse APIVersion %s to a group and version: %w", obj.GetAPIVersion(), err) + } + if len(owners) > 1 { + err = AncestryAmbiguous + } + labels := map[string]string{ + // truncate apigroup to first 63 chars + "apps.cozystack.io/application.group": func(s string) string { + if len(s) < 63 { + return s + } + s = s[:63] + for b := s[62]; !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')); s = s[:len(s)-1] { + b = s[len(s)-1] + } + return s + }(gv.Group), + "apps.cozystack.io/application.kind": obj.GetKind(), + "apps.cozystack.io/application.name": obj.GetName(), + } + if o.GetAPIVersion() != "v1" || o.GetKind() != "Secret" { + return labels, err + } + cfg := h.config.Load().(*runtimeConfig) + crd := cfg.appCRDMap[appRef{gv.Group, obj.GetKind()}] + if matchLabelsToExcludeInclude(o.GetLabels(), crd.Spec.Secrets.Exclude, crd.Spec.Secrets.Include) { + labels["internal.cozystack.io/tenantsecret"] = "" + } + return labels, err +} + +func (h *LineageControllerWebhook) applyLabels(o *unstructured.Unstructured, labels map[string]string) { + if o.GetAPIVersion() == "operator.victoriametrics.com/v1beta1" && o.GetKind() == "VMCluster" { + unstructured.SetNestedStringMap(o.Object, labels, "spec", "managedMetadata", "labels") + return + } + + existing := o.GetLabels() + if existing == nil { + existing = make(map[string]string) + } + for k, v := range labels { + existing[k] = v + } + o.SetLabels(existing) +} + +func (h *LineageControllerWebhook) decodeUnstructured(req admission.Request, out *unstructured.Unstructured) error { + if h.decoder != nil { + if err := h.decoder.Decode(req, out); err == nil { + return nil + } + if req.Kind.Group != "" || req.Kind.Kind != "" || req.Kind.Version != "" { + out.SetGroupVersionKind(schema.GroupVersionKind{ + Group: req.Kind.Group, + Version: req.Kind.Version, + Kind: req.Kind.Kind, + }) + if err := h.decoder.Decode(req, out); err == nil { + return nil + } + } + } + if len(req.Object.Raw) == 0 { + return errors.New("empty admission object") + } + return json.Unmarshal(req.Object.Raw, &out.Object) +} diff --git a/packages/apps/clickhouse/images/clickhouse-backup.tag b/packages/apps/clickhouse/images/clickhouse-backup.tag index be1dc65f..7a6ada58 100644 --- a/packages/apps/clickhouse/images/clickhouse-backup.tag +++ b/packages/apps/clickhouse/images/clickhouse-backup.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/clickhouse-backup:0.13.0@sha256:3faf7a4cebf390b9053763107482de175aa0fdb88c1e77424fd81100b1c3a205 +ghcr.io/cozystack/cozystack/clickhouse-backup:latest@sha256:0f8707d348e03bfa7589159a61d781d4eca238bdfff5dc09f4d3a01b31285a55 diff --git a/packages/apps/http-cache/images/nginx-cache.tag b/packages/apps/http-cache/images/nginx-cache.tag index 81b33d31..9ded14c2 100644 --- a/packages/apps/http-cache/images/nginx-cache.tag +++ b/packages/apps/http-cache/images/nginx-cache.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/nginx-cache:0.7.0@sha256:50ac1581e3100bd6c477a71161cb455a341ffaf9e5e2f6086802e4e25271e8af +ghcr.io/cozystack/cozystack/nginx-cache:latest@sha256:a4ab68c6930263b80f7d0c75f9c87ff9725a835db3f23c6a655993ec4feed49c diff --git a/packages/apps/kubernetes/images/cluster-autoscaler.tag b/packages/apps/kubernetes/images/cluster-autoscaler.tag index 10aa5fd7..8117b889 100644 --- a/packages/apps/kubernetes/images/cluster-autoscaler.tag +++ b/packages/apps/kubernetes/images/cluster-autoscaler.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/cluster-autoscaler:0.29.1@sha256:2d39989846c3579dd020b9f6c77e6e314cc81aa344eaac0f6d633e723c17196d +ghcr.io/cozystack/cozystack/cluster-autoscaler:latest@sha256:89f822343654ea66efb3ac50bf72b483a52c1a11d33497fdfac5bbd0f3715c2b diff --git a/packages/apps/kubernetes/images/kubevirt-cloud-provider.tag b/packages/apps/kubernetes/images/kubevirt-cloud-provider.tag index c848c0a2..06b8300e 100644 --- a/packages/apps/kubernetes/images/kubevirt-cloud-provider.tag +++ b/packages/apps/kubernetes/images/kubevirt-cloud-provider.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:0.29.1@sha256:5335c044313b69ee13b30ca4941687e509005e55f4ae25723861edbf2fbd6dd2 +ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:latest@sha256:190b7d231da1dfbded3af77777cc99b93a29a36ea69186170460f09d533fe041 diff --git a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag index ab468898..fd813a86 100644 --- a/packages/apps/kubernetes/images/kubevirt-csi-driver.tag +++ b/packages/apps/kubernetes/images/kubevirt-csi-driver.tag @@ -1 +1 @@ -kklinch0/kubevirt-csi-driver:0.37.0@sha256:d334ba727f0974b085f6e44ee52a61fa0c507063875b52006665dbacfa332cbd +ghcr.io/cozystack/cozystack/kubevirt-csi-driver:latest@sha256:35684db82ef7d5ad0cb39fa2568aa09cf60c50f3307099dbb89521aefad5baf6 diff --git a/packages/apps/kubernetes/images/ubuntu-container-disk.tag b/packages/apps/kubernetes/images/ubuntu-container-disk.tag index 50416e0a..2c9d99ea 100644 --- a/packages/apps/kubernetes/images/ubuntu-container-disk.tag +++ b/packages/apps/kubernetes/images/ubuntu-container-disk.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.32@sha256:e53f2394c7aa76ad10818ffb945e40006cd77406999e47e036d41b8b0bf094cc +ghcr.io/cozystack/cozystack/ubuntu-container-disk:latest@sha256:ac91fb70d6a898c8a305522020dbbd1bfa6d073a76e5d696a74307487de47dc5 diff --git a/packages/apps/mysql/images/mariadb-backup.tag b/packages/apps/mysql/images/mariadb-backup.tag index 1a2e7dce..aa279946 100644 --- a/packages/apps/mysql/images/mariadb-backup.tag +++ b/packages/apps/mysql/images/mariadb-backup.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/mariadb-backup:0.10.0@sha256:a3789db9e9e065ff60cbac70771b4a8aa1460db3194307cf5ca5d4fe1b412b6b +ghcr.io/cozystack/cozystack/mariadb-backup:latest@sha256:abfc43aed08fbbeed8f090e90158108fe59ed6279db93d169c3b1b1656af0064 diff --git a/packages/core/installer/values.yaml b/packages/core/installer/values.yaml index 62eb3a5a..671a1523 100644 --- a/packages/core/installer/values.yaml +++ b/packages/core/installer/values.yaml @@ -1,2 +1,2 @@ cozystack: - image: ghcr.io/cozystack/cozystack/installer:v0.36.1@sha256:1579855349bef729209e8668b96dbb03e0fc80b74bcae2c25f3ed5f3ae6d2f7f + image: ghcr.io/cozystack/cozystack/installer:latest@sha256:d11c9b065535ef8ca9513d0181419ecb8934f36c92ce254f963fc301c7a3d30c diff --git a/packages/core/testing/values.yaml b/packages/core/testing/values.yaml index f0465bae..6228c44c 100755 --- a/packages/core/testing/values.yaml +++ b/packages/core/testing/values.yaml @@ -1,2 +1,2 @@ e2e: - image: ghcr.io/cozystack/cozystack/e2e-sandbox:v0.36.1@sha256:150efd626321c9389415da5779504be4f10e70beafaeb1b7c162b08b3d50b51f + image: ghcr.io/cozystack/cozystack/e2e-sandbox:latest@sha256:1524ca94acb2ff0fa500ca6125eb980e6ac3f4105ae11deb16513ca8519bc7c6 diff --git a/packages/extra/bootbox/images/matchbox.tag b/packages/extra/bootbox/images/matchbox.tag index ef5e743d..340850ce 100644 --- a/packages/extra/bootbox/images/matchbox.tag +++ b/packages/extra/bootbox/images/matchbox.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/matchbox:v0.36.1@sha256:ecf30f70d9a4b708f68fab52ba3a2ecc0787bb2e79906d76b770bb51f8d6ad6c +ghcr.io/cozystack/cozystack/matchbox:latest@sha256:1923a1c9db94f5dd867e36f6de1db2e55512491dbd9c95ad4671e01eaef4dcf5 diff --git a/packages/extra/monitoring/images/grafana.tag b/packages/extra/monitoring/images/grafana.tag index c411a05c..fff84feb 100644 --- a/packages/extra/monitoring/images/grafana.tag +++ b/packages/extra/monitoring/images/grafana.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/grafana:1.13.1@sha256:c63978e1ed0304e8518b31ddee56c4e8115541b997d8efbe1c0a74da57140399 +ghcr.io/cozystack/cozystack/grafana:latest@sha256:cc1843297f2dadb1740ebc4dfdc3dbe5b4ac840d1da37ed6fe93549d3000196c diff --git a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag index dabec87c..3cfcc303 100644 --- a/packages/extra/seaweedfs/images/objectstorage-sidecar.tag +++ b/packages/extra/seaweedfs/images/objectstorage-sidecar.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.36.1@sha256:890c6f38d22fa8cba423d086686bd55c20b3d0c27881cf4b7a7801f1b0685112 +ghcr.io/cozystack/cozystack/objectstorage-sidecar:latest@sha256:325ab962acb6b546e1fc0f597c86af25100c7c2cb570affaad04954eb6dee449 diff --git a/packages/system/bucket/images/s3manager.tag b/packages/system/bucket/images/s3manager.tag index 896d33dc..5c20a503 100644 --- a/packages/system/bucket/images/s3manager.tag +++ b/packages/system/bucket/images/s3manager.tag @@ -1 +1 @@ -ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:899ea667b3e575244d512cade23f30cc93d768b070f9c2bebcb440e443444bdb +ghcr.io/cozystack/cozystack/s3manager:latest@sha256:377bc82c8404ae1ea201d8b9a64015952edfa06d88fb2678b7bf7c45b96fb968 diff --git a/packages/system/cilium/values.yaml b/packages/system/cilium/values.yaml index 25ed972f..db845e6f 100644 --- a/packages/system/cilium/values.yaml +++ b/packages/system/cilium/values.yaml @@ -14,7 +14,7 @@ cilium: mode: "kubernetes" image: repository: ghcr.io/cozystack/cozystack/cilium - tag: 1.17.5 + tag: latest digest: "sha256:2def2dccfc17870be6e1d63584c25b32e812f21c9cdcfa06deadd2787606654d" envoy: enabled: false diff --git a/packages/system/cozystack-api/values.yaml b/packages/system/cozystack-api/values.yaml index f38bafb2..a7f9b23d 100644 --- a/packages/system/cozystack-api/values.yaml +++ b/packages/system/cozystack-api/values.yaml @@ -1,2 +1,2 @@ cozystackAPI: - image: ghcr.io/cozystack/cozystack/cozystack-api:v0.36.1@sha256:a9ce8848b0a46e52ce47ad10fcccc4e848740f5cf1d4e6cb78fc4a196e167e1b + image: ghcr.io/cozystack/cozystack/cozystack-api:latest@sha256:be6aca4df4b769538454d6c65b7045b2bef81b2a15d70eeddbfdf55d6bd17d6c diff --git a/packages/system/cozystack-controller/templates/certmanager.yaml b/packages/system/cozystack-controller/templates/certmanager.yaml new file mode 100644 index 00000000..6d9bce7d --- /dev/null +++ b/packages/system/cozystack-controller/templates/certmanager.yaml @@ -0,0 +1,45 @@ +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: cozystack-controller-webhook-selfsigned + namespace: {{ .Release.Namespace }} +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: cozystack-controller-webhook-ca + namespace: {{ .Release.Namespace }} +spec: + secretName: cozystack-controller-webhook-ca + duration: 43800h # 5 years + commonName: cozystack-controller-webhook-ca + issuerRef: + name: cozystack-controller-webhook-selfsigned + isCA: true +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: cozystack-controller-webhook-ca + namespace: {{ .Release.Namespace }} +spec: + ca: + secretName: cozystack-controller-webhook-ca +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: cozystack-controller-webhook + namespace: {{ .Release.Namespace }} +spec: + secretName: cozystack-controller-webhook-cert + duration: 8760h + renewBefore: 720h + issuerRef: + name: cozystack-controller-webhook-ca + commonName: cozystack-controller + dnsNames: + - cozystack-controller + - cozystack-controller.{{ .Release.Namespace }}.svc diff --git a/packages/system/cozystack-controller/templates/deployment.yaml b/packages/system/cozystack-controller/templates/deployment.yaml index 33b309df..318ec3b2 100644 --- a/packages/system/cozystack-controller/templates/deployment.yaml +++ b/packages/system/cozystack-controller/templates/deployment.yaml @@ -2,7 +2,6 @@ apiVersion: apps/v1 kind: Deployment metadata: name: cozystack-controller - namespace: cozy-system labels: app: cozystack-controller spec: @@ -29,3 +28,15 @@ spec: {{- if .Values.cozystackController.disableTelemetry }} - --disable-telemetry {{- end }} + ports: + - name: webhook + containerPort: 9443 + volumeMounts: + - name: webhook-certs + mountPath: /tmp/k8s-webhook-server/serving-certs + readOnly: true + volumes: + - name: webhook-certs + secret: + secretName: cozystack-controller-webhook-cert + defaultMode: 0400 diff --git a/packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml b/packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml new file mode 100644 index 00000000..aebf8938 --- /dev/null +++ b/packages/system/cozystack-controller/templates/mutatingwebhookconfiguration.yaml @@ -0,0 +1,37 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: lineage + annotations: + cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/cozystack-controller-webhook + labels: + app: cozystack-controller +webhooks: + - name: lineage.cozystack.io + admissionReviewVersions: ["v1"] + sideEffects: None + clientConfig: + service: + name: cozystack-controller + namespace: {{ .Release.Namespace }} + path: /mutate-lineage + rules: + - operations: ["CREATE", "UPDATE"] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods","secrets", "services", "persistentvolumeclaims"] + - operations: ["CREATE", "UPDATE"] + apiGroups: ["operator.victoriametrics.com"] + apiVersions: ["v1beta1"] + resources: ["vmclusters"] + failurePolicy: Fail + namespaceSelector: + matchExpressions: + - key: cozystack.io/system + operator: NotIn + values: + - "true" + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system diff --git a/packages/system/cozystack-controller/templates/service.yaml b/packages/system/cozystack-controller/templates/service.yaml new file mode 100644 index 00000000..8cc0eb61 --- /dev/null +++ b/packages/system/cozystack-controller/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: cozystack-controller + labels: + app: cozystack-controller +spec: + type: ClusterIP + ports: + - port: 443 + targetPort: 9443 + protocol: TCP + name: webhook + selector: + app: cozystack-controller diff --git a/packages/system/cozystack-controller/values.yaml b/packages/system/cozystack-controller/values.yaml index 8c9f3595..c5e7c85f 100644 --- a/packages/system/cozystack-controller/values.yaml +++ b/packages/system/cozystack-controller/values.yaml @@ -1,5 +1,5 @@ cozystackController: - image: ghcr.io/cozystack/cozystack/cozystack-controller:v0.36.1@sha256:6a6144430bdec901b4046840a484f0d9effc570f4f7cef2e5740332b98e3077a + image: ghcr.io/cozystack/cozystack/cozystack-controller:latest@sha256:20483582c50ead02d76719d3acfe2bb5e0d5a62de7dce65ffebe4039f8f3c400 debug: false disableTelemetry: false - cozystackVersion: "v0.36.1" + cozystackVersion: "latest" diff --git a/packages/system/dashboard/charts/kubeapps/templates/dashboard/configmap.yaml b/packages/system/dashboard/charts/kubeapps/templates/dashboard/configmap.yaml index a17cfecf..b1f1891b 100644 --- a/packages/system/dashboard/charts/kubeapps/templates/dashboard/configmap.yaml +++ b/packages/system/dashboard/charts/kubeapps/templates/dashboard/configmap.yaml @@ -76,7 +76,7 @@ data: "kubeappsNamespace": {{ .Release.Namespace | quote }}, "helmGlobalNamespace": {{ include "kubeapps.helmGlobalPackagingNamespace" . | quote }}, "carvelGlobalNamespace": {{ .Values.kubeappsapis.pluginConfig.kappController.packages.v1alpha1.globalPackagingNamespace | quote }}, - "appVersion": "v0.36.1", + "appVersion": "latest", "authProxyEnabled": {{ .Values.authProxy.enabled }}, "oauthLoginURI": {{ .Values.authProxy.oauthLoginURI | quote }}, "oauthLogoutURI": {{ .Values.authProxy.oauthLogoutURI | quote }}, diff --git a/packages/system/dashboard/values.yaml b/packages/system/dashboard/values.yaml index bfd0327e..300cf5a1 100644 --- a/packages/system/dashboard/values.yaml +++ b/packages/system/dashboard/values.yaml @@ -19,8 +19,8 @@ kubeapps: image: registry: ghcr.io/cozystack/cozystack repository: dashboard - tag: v0.36.1 - digest: "sha256:54906b3d2492c8603a347a5938b6db36e5ed5c4149111cae1804ac9110361947" + tag: latest + digest: "sha256:187d4e4964c57b318a4cf538d7e6d03c2f9a73d2ea3126b9be3d5ad7d48fa7f1" frontend: image: repository: bitnamilegacy/nginx @@ -48,8 +48,8 @@ kubeapps: image: registry: ghcr.io/cozystack/cozystack repository: kubeapps-apis - tag: v0.36.1 - digest: "sha256:95cec1059acc1e307f9346bdf570cfc1ca5962bdb803ff08ccd17712d1d8f485" + tag: latest + digest: "sha256:7f6a822c80dd84ee77276183b59ffe073ececff056bddd71e686b2d542f6fde8" pluginConfig: flux: packages: diff --git a/packages/system/kamaji/values.yaml b/packages/system/kamaji/values.yaml index 1a231935..6ed1713a 100644 --- a/packages/system/kamaji/values.yaml +++ b/packages/system/kamaji/values.yaml @@ -3,7 +3,7 @@ kamaji: deploy: false image: pullPolicy: IfNotPresent - tag: v0.36.1@sha256:61bd1a046d0ad2bc90bf4507aa41073989001d13606e9b8cf8f318edcfadf2c7 + tag: latest@sha256:3ac3ed8f4b77d59745c89b215bf498b430a338c0ce39652ae0c389e36551fac9 repository: ghcr.io/cozystack/cozystack/kamaji resources: limits: @@ -13,4 +13,4 @@ kamaji: cpu: 100m memory: 100Mi extraArgs: - - --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v0.36.1@sha256:61bd1a046d0ad2bc90bf4507aa41073989001d13606e9b8cf8f318edcfadf2c7 + - --migrate-image=ghcr.io/cozystack/cozystack/kamaji:latest@sha256:3ac3ed8f4b77d59745c89b215bf498b430a338c0ce39652ae0c389e36551fac9 diff --git a/packages/system/kubeovn-plunger/values.yaml b/packages/system/kubeovn-plunger/values.yaml index 22647b56..74ef2ec8 100644 --- a/packages/system/kubeovn-plunger/values.yaml +++ b/packages/system/kubeovn-plunger/values.yaml @@ -1,4 +1,4 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v0.36.1@sha256:b38c0610e9648d21326be30a773173757897d2ba9ec438272c8ae3d738956b66 +image: ghcr.io/cozystack/cozystack/kubeovn-plunger:latest@sha256:96950f57d68ca2a911a4fa4ec0b36bfb95c87ed8dc1a27845ae4a9dc1cb959ad ovnCentralName: ovn-central diff --git a/packages/system/kubeovn-webhook/values.yaml b/packages/system/kubeovn-webhook/values.yaml index 5a8f4a0a..410abd49 100644 --- a/packages/system/kubeovn-webhook/values.yaml +++ b/packages/system/kubeovn-webhook/values.yaml @@ -1,3 +1,3 @@ portSecurity: true routes: "" -image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v0.36.1@sha256:8fa755cdb024c7bdeff390d01d6b8569d60a4b244a7371209be4c4851df5fad4 +image: ghcr.io/cozystack/cozystack/kubeovn-webhook:latest@sha256:ee87101fdcaf86c339463390c6f12ef107726746da38566bd936c5dfe5b70047 diff --git a/packages/system/kubeovn/values.yaml b/packages/system/kubeovn/values.yaml index 92319a51..145f86bc 100644 --- a/packages/system/kubeovn/values.yaml +++ b/packages/system/kubeovn/values.yaml @@ -64,4 +64,4 @@ global: images: kubeovn: repository: kubeovn - tag: v1.14.5@sha256:7035b06406493c3385645319a50f2198c6bfb343d2e8c3f0707e769db1e960f7 + tag: latest@sha256:0891335d3938dc604b47ae310f9bea815c352c15bea34dee111526c6aa6a48de diff --git a/packages/system/kubevirt-csi-node/values.yaml b/packages/system/kubevirt-csi-node/values.yaml index a979202b..cbf96787 100644 --- a/packages/system/kubevirt-csi-node/values.yaml +++ b/packages/system/kubevirt-csi-node/values.yaml @@ -1,3 +1,3 @@ storageClass: replicated csiDriver: - image: kklinch0/kubevirt-csi-driver:0.37.0@sha256:d334ba727f0974b085f6e44ee52a61fa0c507063875b52006665dbacfa332cbd + image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:latest@sha256:35684db82ef7d5ad0cb39fa2568aa09cf60c50f3307099dbb89521aefad5baf6 diff --git a/packages/system/metallb/values.yaml b/packages/system/metallb/values.yaml index d392f4c4..d7d8c136 100644 --- a/packages/system/metallb/values.yaml +++ b/packages/system/metallb/values.yaml @@ -4,8 +4,8 @@ metallb: controller: image: repository: ghcr.io/cozystack/cozystack/metallb-controller - tag: v0.15.2@sha256:0e9080234fc8eedab78ad2831fb38df375c383e901a752d72b353c8d13b9605f + tag: v0.15.2@sha256:5106869c470fcce9e1ef1e07efe5224190ad19e8006d5889230a75df4988b68d speaker: image: repository: ghcr.io/cozystack/cozystack/metallb-speaker - tag: v0.15.2@sha256:e14d4c328c3ab91a6eadfeea90da96388503492d165e7e8582f291b1872e53b2 + tag: v0.15.2@sha256:2b837031e3c693c0fa0de0ad5a9036e2aabb5e90a7a235e19b089be73af11160 diff --git a/packages/system/objectstorage-controller/values.yaml b/packages/system/objectstorage-controller/values.yaml index 14427c67..c5836ea3 100644 --- a/packages/system/objectstorage-controller/values.yaml +++ b/packages/system/objectstorage-controller/values.yaml @@ -1,3 +1,3 @@ objectstorage: controller: - image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v0.36.1@sha256:aa0000265ae58155aebefedac72d0a6acc45437b8668bb9739bf11edefec067a" + image: "ghcr.io/cozystack/cozystack/objectstorage-controller:latest@sha256:02c1ff850922afd264659532465b079502c04e9e9f54686a26a26debad7880ef" diff --git a/packages/system/seaweedfs/values.yaml b/packages/system/seaweedfs/values.yaml index fc081a90..59dabf84 100644 --- a/packages/system/seaweedfs/values.yaml +++ b/packages/system/seaweedfs/values.yaml @@ -118,7 +118,7 @@ seaweedfs: bucketClassName: "seaweedfs" region: "" sidecar: - image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.36.1@sha256:890c6f38d22fa8cba423d086686bd55c20b3d0c27881cf4b7a7801f1b0685112" + image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:latest@sha256:325ab962acb6b546e1fc0f597c86af25100c7c2cb570affaad04954eb6dee449" certificates: commonName: "SeaweedFS CA" ipAddresses: [] diff --git a/pkg/lineage/lineage.go b/pkg/lineage/lineage.go new file mode 100644 index 00000000..867e1aab --- /dev/null +++ b/pkg/lineage/lineage.go @@ -0,0 +1,195 @@ +package lineage + +import ( + "context" + "fmt" + "os" + "strings" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + HRAPIVersion = "helm.toolkit.fluxcd.io/v2" + HRKind = "HelmRelease" + HRLabel = "helm.toolkit.fluxcd.io/name" +) + +type ObjectID struct { + APIVersion string + Kind string + Namespace string + Name string +} + +func (o ObjectID) GetUnstructured(ctx context.Context, client dynamic.Interface, mapper meta.RESTMapper) (*unstructured.Unstructured, error) { + u, err := getUnstructuredObject(ctx, client, mapper, o.APIVersion, o.Kind, o.Namespace, o.Name) + if err != nil { + return nil, err + } + return u, nil +} + +func WalkOwnershipGraph( + ctx context.Context, + client dynamic.Interface, + mapper meta.RESTMapper, + appMapper AppMapper, + obj *unstructured.Unstructured, + memory ...interface{}, +) (out []ObjectID) { + + id := ObjectID{APIVersion: obj.GetAPIVersion(), Kind: obj.GetKind(), Namespace: obj.GetNamespace(), Name: obj.GetName()} + out = []ObjectID{} + l := log.FromContext(ctx) + + l.Info("processing object", "apiVersion", obj.GetAPIVersion(), "kind", obj.GetKind(), "name", obj.GetName()) + var visited map[ObjectID]bool + var ok bool + if len(memory) == 1 { + visited, ok = memory[0].(map[ObjectID]bool) + if !ok { + l.Error( + fmt.Errorf("invalid argument"), "could not parse visited map in WalkOwnershipGraph call", + "received", memory[0], "expected", "map[ObjectID]bool", + ) + return out + } + } + + if len(memory) == 0 { + visited = make(map[ObjectID]bool) + } + + if len(memory) != 0 && len(memory) != 1 { + l.Error( + fmt.Errorf("invalid argument count"), "could not parse variadic arguments to WalkOwnershipGraph", + "args passed", len(memory)+5, "expected args", "4|5", + ) + return out + } + + if visited[id] { + return out + } + + visited[id] = true + + ownerRefs := obj.GetOwnerReferences() + for _, owner := range ownerRefs { + ownerObj, err := getUnstructuredObject(ctx, client, mapper, owner.APIVersion, owner.Kind, obj.GetNamespace(), owner.Name) + if err != nil { + fmt.Fprintf(os.Stderr, "Could not fetch owner %s/%s (%s): %v\n", obj.GetNamespace(), owner.Name, owner.Kind, err) + continue + } + + out = append(out, WalkOwnershipGraph(ctx, client, mapper, appMapper, ownerObj, visited)...) + } + + // if object has owners, it couldn't be owned directly by the custom app + if len(ownerRefs) > 0 { + return + } + + // I want "if err1 != nil go to next block, if err2 != nil, go to next block, etc semantics", + // like an early return from a function, but if all checks succeed, I don't want to do the rest + // of the function, so it's a `for { if err { break } if othererr { break } if allgood { return } + for { + if obj.GetAPIVersion() != HRAPIVersion || obj.GetKind() != HRKind { + break + } + hr := helmReleaseFromUnstructured(obj) + if hr == nil { + break + } + a, k, p, err := appMapper.Map(hr) + if err != nil { + l.Error(err, "failed to map HelmRelease to app") + break + } + ownerObj, err := getUnstructuredObject(ctx, client, mapper, a, k, obj.GetNamespace(), strings.TrimPrefix(obj.GetName(), p)) + if err != nil { + l.Error(err, "couldn't get unstructured object", "APIVersion", a, "Kind", k, "Name", strings.TrimPrefix(obj.GetName(), p)) + break + } + // successfully mapped a HelmRelease to a custom app, no need to continue + out = append(out, + ObjectID{ + APIVersion: ownerObj.GetAPIVersion(), + Kind: ownerObj.GetKind(), + Namespace: ownerObj.GetNamespace(), + Name: ownerObj.GetName(), + }, + ) + return + } + + labels := obj.GetLabels() + name, ok := labels[HRLabel] + if !ok { + return + } + ownerObj, err := getUnstructuredObject(ctx, client, mapper, HRAPIVersion, HRKind, obj.GetNamespace(), name) + if err != nil { + return + } + out = append(out, WalkOwnershipGraph(ctx, client, mapper, appMapper, ownerObj, visited)...) + + return +} + +func getUnstructuredObject( + ctx context.Context, + client dynamic.Interface, + mapper meta.RESTMapper, + apiVersion, kind, namespace, name string, +) (*unstructured.Unstructured, error) { + l := log.FromContext(ctx) + gv, err := schema.ParseGroupVersion(apiVersion) + if err != nil { + l.Error( + err, "failed to parse groupversion", + "apiVersion", apiVersion, + ) + return nil, err + } + gvk := schema.GroupVersionKind{ + Group: gv.Group, + Version: gv.Version, + Kind: kind, + } + + mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + if err != nil { + l.Error(err, "Could not map GVK "+gvk.String()) + return nil, err + } + + ns := namespace + if mapping.Scope.Name() != meta.RESTScopeNameNamespace { + ns = "" + } + + ownerObj, err := client.Resource(mapping.Resource).Namespace(ns).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + return ownerObj, nil +} + +func helmReleaseFromUnstructured(obj *unstructured.Unstructured) *helmv2.HelmRelease { + if obj.GetAPIVersion() == HRAPIVersion && obj.GetKind() == HRKind { + hr := &helmv2.HelmRelease{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, hr); err == nil { + return hr + } + } + return nil +} diff --git a/pkg/lineage/lineage_test.go b/pkg/lineage/lineage_test.go new file mode 100644 index 00000000..7ae2a00c --- /dev/null +++ b/pkg/lineage/lineage_test.go @@ -0,0 +1,53 @@ +package lineage + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/go-logr/logr" + "github.com/go-logr/zapr" + "go.uber.org/zap" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/discovery/cached/memory" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/restmapper" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +var ( + dynClient dynamic.Interface + mapper meta.RESTMapper + l logr.Logger + ctx context.Context +) + +func init() { + cfg := config.GetConfigOrDie() + + dynClient, _ = dynamic.NewForConfig(cfg) + + discoClient, _ := discovery.NewDiscoveryClientForConfig(cfg) + + cachedDisco := memory.NewMemCacheClient(discoClient) + mapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDisco) + + zapLogger, _ := zap.NewDevelopment() + l = zapr.NewLogger(zapLogger) + ctx = logr.NewContext(context.Background(), l) +} + +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, obj) + for _, node := range nodes { + fmt.Printf("%#v\n", node) + } +} diff --git a/pkg/lineage/mapper.go b/pkg/lineage/mapper.go new file mode 100644 index 00000000..c424b288 --- /dev/null +++ b/pkg/lineage/mapper.go @@ -0,0 +1,49 @@ +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 +}