Feat/webhook workload monitors (#1448)

## What this PR does

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. This resubmission of the PR now
includes semantics to compare secrets to label selectors in
CozystackResourceDefinitions to determine, whether they should be marked
as user-facing or not.

### Release note

```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.
```

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
- Adds a lineage mutating webhook that auto-applies ancestry labels to
core resources and VMCluster.
- Introduces secret include/exclude selectors in resource definitions
for fine-grained tenant secret visibility.
- Deploys webhook service with TLS via cert-manager (issuers,
certificates) and updates deployment to expose webhook port.

- Chores
- Updates numerous container images to latest tags and digests across
system and app components (controller, dashboard, kubeovn, cilium,
kamaji, storage, etc.).

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Andrei Kvapil
2025-09-24 11:10:19 +02:00
committed by GitHub
43 changed files with 1062 additions and 35 deletions

View File

@@ -22,6 +22,7 @@ package v1alpha1
import (
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)
@@ -137,11 +138,49 @@ func (in *CozystackResourceDefinitionRelease) DeepCopy() *CozystackResourceDefin
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CozystackResourceDefinitionSecrets) DeepCopyInto(out *CozystackResourceDefinitionSecrets) {
*out = *in
if in.Exclude != nil {
in, out := &in.Exclude, &out.Exclude
*out = make([]*v1.LabelSelector, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
}
if in.Include != nil {
in, out := &in.Include, &out.Include
*out = make([]*v1.LabelSelector, len(*in))
for i := range *in {
if (*in)[i] != nil {
in, out := &(*in)[i], &(*out)[i]
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionSecrets.
func (in *CozystackResourceDefinitionSecrets) DeepCopy() *CozystackResourceDefinitionSecrets {
if in == nil {
return nil
}
out := new(CozystackResourceDefinitionSecrets)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CozystackResourceDefinitionSpec) DeepCopyInto(out *CozystackResourceDefinitionSpec) {
*out = *in
out.Application = in.Application
in.Release.DeepCopyInto(&out.Release)
in.Secrets.DeepCopyInto(&out.Secrets)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CozystackResourceDefinitionSpec.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/clickhouse-backup:0.13.0@sha256:3faf7a4cebf390b9053763107482de175aa0fdb88c1e77424fd81100b1c3a205
ghcr.io/cozystack/cozystack/clickhouse-backup:latest@sha256:0f8707d348e03bfa7589159a61d781d4eca238bdfff5dc09f4d3a01b31285a55

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/nginx-cache:0.7.0@sha256:50ac1581e3100bd6c477a71161cb455a341ffaf9e5e2f6086802e4e25271e8af
ghcr.io/cozystack/cozystack/nginx-cache:latest@sha256:a4ab68c6930263b80f7d0c75f9c87ff9725a835db3f23c6a655993ec4feed49c

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/cluster-autoscaler:0.29.1@sha256:2d39989846c3579dd020b9f6c77e6e314cc81aa344eaac0f6d633e723c17196d
ghcr.io/cozystack/cozystack/cluster-autoscaler:latest@sha256:89f822343654ea66efb3ac50bf72b483a52c1a11d33497fdfac5bbd0f3715c2b

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:0.29.1@sha256:5335c044313b69ee13b30ca4941687e509005e55f4ae25723861edbf2fbd6dd2
ghcr.io/cozystack/cozystack/kubevirt-cloud-provider:latest@sha256:190b7d231da1dfbded3af77777cc99b93a29a36ea69186170460f09d533fe041

View File

@@ -1 +1 @@
kklinch0/kubevirt-csi-driver:0.37.0@sha256:d334ba727f0974b085f6e44ee52a61fa0c507063875b52006665dbacfa332cbd
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:latest@sha256:35684db82ef7d5ad0cb39fa2568aa09cf60c50f3307099dbb89521aefad5baf6

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.32@sha256:e53f2394c7aa76ad10818ffb945e40006cd77406999e47e036d41b8b0bf094cc
ghcr.io/cozystack/cozystack/ubuntu-container-disk:latest@sha256:ac91fb70d6a898c8a305522020dbbd1bfa6d073a76e5d696a74307487de47dc5

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/mariadb-backup:0.10.0@sha256:a3789db9e9e065ff60cbac70771b4a8aa1460db3194307cf5ca5d4fe1b412b6b
ghcr.io/cozystack/cozystack/mariadb-backup:latest@sha256:abfc43aed08fbbeed8f090e90158108fe59ed6279db93d169c3b1b1656af0064

View File

@@ -78,7 +78,7 @@ spec:
labels:
policy.cozystack.io/allow-to-apiserver: "true"
app.kubernetes.io/name: postgres.apps.cozystack.io
app.kubernets.io/instance: {{ $.Release.Name }}
app.kubernetes.io/instance: {{ $.Release.Name }}
---
apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
@@ -91,5 +91,5 @@ spec:
type: postgres
selector:
app.kubernetes.io/name: postgres.apps.cozystack.io
app.kubernets.io/instance: {{ $.Release.Name }}
app.kubernetes.io/instance: {{ $.Release.Name }}
version: {{ $.Chart.Version }}

View File

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

View File

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

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/matchbox:v0.36.1@sha256:ecf30f70d9a4b708f68fab52ba3a2ecc0787bb2e79906d76b770bb51f8d6ad6c
ghcr.io/cozystack/cozystack/matchbox:latest@sha256:1923a1c9db94f5dd867e36f6de1db2e55512491dbd9c95ad4671e01eaef4dcf5

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/grafana:1.13.1@sha256:c63978e1ed0304e8518b31ddee56c4e8115541b997d8efbe1c0a74da57140399
ghcr.io/cozystack/cozystack/grafana:latest@sha256:cc1843297f2dadb1740ebc4dfdc3dbe5b4ac840d1da37ed6fe93549d3000196c

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v0.36.1@sha256:890c6f38d22fa8cba423d086686bd55c20b3d0c27881cf4b7a7801f1b0685112
ghcr.io/cozystack/cozystack/objectstorage-sidecar:latest@sha256:325ab962acb6b546e1fc0f597c86af25100c7c2cb570affaad04954eb6dee449

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:899ea667b3e575244d512cade23f30cc93d768b070f9c2bebcb440e443444bdb
ghcr.io/cozystack/cozystack/s3manager:latest@sha256:377bc82c8404ae1ea201d8b9a64015952edfa06d88fb2678b7bf7c45b96fb968

View File

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

View File

@@ -19,6 +19,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -41,6 +46,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -63,6 +73,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -85,6 +100,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -107,6 +127,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -129,6 +154,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -151,6 +181,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -173,6 +208,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -195,6 +235,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -217,6 +262,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -239,6 +289,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -261,6 +316,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -283,6 +343,13 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
- matchLabels:
cnpg.io/userType: superuser
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -305,6 +372,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -327,6 +399,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -349,6 +426,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -371,6 +453,11 @@ spec:
kind: HelmRepository
name: cozystack-apps
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -393,6 +480,11 @@ spec:
kind: HelmRepository
name: cozystack-extra
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -415,6 +507,11 @@ spec:
kind: HelmRepository
name: cozystack-extra
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -437,6 +534,11 @@ spec:
kind: HelmRepository
name: cozystack-extra
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -459,6 +561,11 @@ spec:
kind: HelmRepository
name: cozystack-extra
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -481,6 +588,11 @@ spec:
kind: HelmRepository
name: cozystack-extra
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]
---
apiVersion: cozystack.io/v1alpha1
kind: CozystackResourceDefinition
@@ -503,3 +615,8 @@ spec:
kind: HelmRepository
name: cozystack-extra
namespace: cozy-public
secrets:
exclude:
- matchLabels:
apps.cozystack.io/tenantresource: "false"
include: [{}]

View File

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

View File

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

View File

@@ -106,6 +106,121 @@ spec:
- chart
- prefix
type: object
secrets:
description: Secret selectors
properties:
exclude:
description: |-
Exclude contains an array of label selectors that target secrets.
If a secret matches the selector in any of the elements in the array, it is
hidden from the user, regardless of the matches in the include array.
items:
description: |-
A label selector is a label query over a set of resources. The result of matchLabels and
matchExpressions are ANDed. An empty label selector matches all objects. A null
label selector matches no objects.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: array
include:
description: |-
Include contains an array of label selectors that target secrets.
If a secret matches the selector in any of the elements in the array, and
matches none of the selectors in the exclude array that secret is marked
as a tenant secret and is visible to users.
items:
description: |-
A label selector is a label query over a set of resources. The result of matchLabels and
matchExpressions are ANDed. An empty label selector matches all objects. A null
label selector matches no objects.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: |-
A label selector requirement is a selector that contains values, a key, and an operator that
relates the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: |-
operator represents a key's relationship to a set of values.
Valid operators are In, NotIn, Exists and DoesNotExist.
type: string
values:
description: |-
values is an array of string values. If the operator is In or NotIn,
the values array must be non-empty. If the operator is Exists or DoesNotExist,
the values array must be empty. This array is replaced during a strategic
merge patch.
items:
type: string
type: array
x-kubernetes-list-type: atomic
required:
- key
- operator
type: object
type: array
x-kubernetes-list-type: atomic
matchLabels:
additionalProperties:
type: string
description: |-
matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
map is equivalent to an element of matchExpressions, whose key field is "key", the
operator is "In", and the values array contains only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: array
type: object
required:
- application
- release

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -64,4 +64,4 @@ global:
images:
kubeovn:
repository: kubeovn
tag: v1.14.5@sha256:7035b06406493c3385645319a50f2198c6bfb343d2e8c3f0707e769db1e960f7
tag: latest@sha256:0891335d3938dc604b47ae310f9bea815c352c15bea34dee111526c6aa6a48de

View File

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

View File

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

View File

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

View File

@@ -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: []

195
pkg/lineage/lineage.go Normal file
View File

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

View File

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

49
pkg/lineage/mapper.go Normal file
View File

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