mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-12 23:54:01 +09:00
fix(lineage-webhook): keep an explicit tenantresource false
The webhook computes the tenantresource verdict from the ApplicationDefinition selectors on first admission and copies it over whatever label the chart rendered, so a chart cannot take a Secret the definition still selects off the tenantsecrets projection. The credentials conversion in design-proposals/user-secrets-api needs exactly that during its migration window: the converted chart renders the platform-only credentials Secret with tenantresource false while the old definition still selects the name. A rendered false is now kept; a rendered true is still replaced by the computed verdict, so the object can only narrow what the definition grants, never widen it. Assisted-by: LLM Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
This commit is contained in:
1 parent
0b284a65d5
commit
7b214464ee
2 files changed
+128
-6
No files matched your search
@@ -193,12 +193,19 @@ func (h *LineageControllerWebhook) computeLabels(ctx context.Context, obj *unstr
|
||||
crd := cfg.appCRDMap[appRef{gv.Group, owner.GetKind()}]
|
||||
resourceSelectors := h.getResourceSelectors(obj.GroupVersionKind().GroupKind(), crd)
|
||||
|
||||
labels[corev1alpha1.TenantResourceLabelKey] = func(b bool) string {
|
||||
if b {
|
||||
return corev1alpha1.TenantResourceLabelValue
|
||||
}
|
||||
return "false"
|
||||
}(matchResourceToExcludeInclude(ctx, obj.GetName(), templateLabels, obj.GetLabels(), resourceSelectors))
|
||||
selected := matchResourceToExcludeInclude(ctx, obj.GetName(), templateLabels, obj.GetLabels(), resourceSelectors)
|
||||
// The verdict can be narrowed by the object and never widened: a chart that
|
||||
// renders tenantresource "false" keeps it even where the definition selects
|
||||
// the name, while a chart-set "true" is replaced by the computed verdict.
|
||||
// This is what lets a chart take a Secret the definition still selects off
|
||||
// the tenantsecrets projection while the definition catches up.
|
||||
if obj.GetLabels()[corev1alpha1.TenantResourceLabelKey] == "false" {
|
||||
selected = false
|
||||
}
|
||||
labels[corev1alpha1.TenantResourceLabelKey] = "false"
|
||||
if selected {
|
||||
labels[corev1alpha1.TenantResourceLabelKey] = corev1alpha1.TenantResourceLabelValue
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package lineagecontrollerwebhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1"
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
)
|
||||
|
||||
// The tenantresource verdict is the definition's, but an object may narrow it
|
||||
// and never widen it: a rendered "false" survives a selector that matches the
|
||||
// name, a rendered "true" does not survive one that does not.
|
||||
func TestHandle_TenantResourceLabelNarrowsOnly(t *testing.T) {
|
||||
tenantResourcePath := "/metadata/labels/" + strings.ReplaceAll(corev1alpha1.TenantResourceLabelKey, "/", "~1")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
secret string
|
||||
rendered string
|
||||
// want is the value the label must hold once the patch is applied;
|
||||
// "" means the response must not touch the label at all.
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "a rendered false is kept where the definition selects the name",
|
||||
secret: "harbor-demo-credentials",
|
||||
rendered: "false",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "a rendered true is overwritten where the definition does not select the name",
|
||||
secret: "harbor-demo-other",
|
||||
rendered: "true",
|
||||
want: "false",
|
||||
},
|
||||
{
|
||||
name: "no rendered label gets the definition's verdict",
|
||||
secret: "harbor-demo-credentials",
|
||||
rendered: "",
|
||||
want: "true",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
w := newHandleTestWebhook(t)
|
||||
w.config.Store(&runtimeConfig{appCRDMap: map[appRef]*cozyv1alpha1.ApplicationDefinition{
|
||||
{"apps.cozystack.io", "Harbor"}: {Spec: cozyv1alpha1.ApplicationDefinitionSpec{
|
||||
Application: cozyv1alpha1.ApplicationDefinitionApplication{Kind: "Harbor"},
|
||||
Secrets: cozyv1alpha1.ApplicationDefinitionResources{
|
||||
Include: []*cozyv1alpha1.ApplicationDefinitionResourceSelector{
|
||||
{ResourceNames: []string{"{{ .kind }}-{{ .name }}-credentials"}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}})
|
||||
|
||||
labels := map[string]string{"helm.toolkit.fluxcd.io/name": "harbor-demo"}
|
||||
if tt.rendered != "" {
|
||||
labels[corev1alpha1.TenantResourceLabelKey] = tt.rendered
|
||||
}
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetAPIVersion("v1")
|
||||
obj.SetKind("Secret")
|
||||
obj.SetNamespace("tenant-demo")
|
||||
obj.SetName(tt.secret)
|
||||
obj.SetLabels(labels)
|
||||
raw, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal object: %v", err)
|
||||
}
|
||||
|
||||
resp := w.Handle(context.Background(), admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{Version: "v1", Kind: "Secret"},
|
||||
Namespace: "tenant-demo",
|
||||
Name: tt.secret,
|
||||
Operation: admissionv1.Create,
|
||||
Object: runtime.RawExtension{Raw: raw},
|
||||
}})
|
||||
if !resp.Allowed {
|
||||
t.Fatalf("expected allowed response, got %+v", resp.Result)
|
||||
}
|
||||
|
||||
var got string
|
||||
var touched bool
|
||||
for _, p := range resp.Patches {
|
||||
if p.Path != tenantResourcePath {
|
||||
continue
|
||||
}
|
||||
touched = true
|
||||
v, ok := p.Value.(string)
|
||||
if !ok {
|
||||
t.Fatalf("patch %s carries %T, want string", p.Path, p.Value)
|
||||
}
|
||||
got = v
|
||||
}
|
||||
switch {
|
||||
case tt.want == "" && touched:
|
||||
t.Errorf("expected the label untouched, got patch value %q", got)
|
||||
case tt.want != "" && !touched:
|
||||
t.Errorf("expected a patch setting the label to %q, got none", tt.want)
|
||||
case tt.want != "" && got != tt.want:
|
||||
t.Errorf("label patched to %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user