Skip to content

Commit 9522e65

Browse files
committed
add hook to drop labels
Signed-off-by: Pavel Tishkov <[email protected]>
1 parent 144c53a commit 9522e65

File tree

3 files changed

+295
-0
lines changed

3 files changed

+295
-0
lines changed

images/hooks/cmd/virtualization-module-hooks/register.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
_ "hooks/pkg/hooks/create-generic-vmclass"
2222
_ "hooks/pkg/hooks/discovery-clusterip-service-for-dvcr"
2323
_ "hooks/pkg/hooks/discovery-workload-nodes"
24+
_ "hooks/pkg/hooks/drop-helm-labels-from-generic-vmclass"
2425
_ "hooks/pkg/hooks/drop-openshift-labels"
2526
_ "hooks/pkg/hooks/generate-secret-for-dvcr"
2627
_ "hooks/pkg/hooks/migrate-delete-renamed-validation-admission-policy"
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
Copyright 2025 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package drop_helm_labels_from_generic_vmclass
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"strings"
23+
24+
"github.com/deckhouse/module-sdk/pkg"
25+
"github.com/deckhouse/module-sdk/pkg/registry"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/utils/ptr"
28+
29+
"hooks/pkg/settings"
30+
)
31+
32+
const (
33+
vmClassSnapshot = "vmclass-generic"
34+
genericVMClassName = "generic"
35+
)
36+
37+
const (
38+
helmManagedByLabel = "app.kubernetes.io/managed-by"
39+
helmHeritageLabel = "heritage"
40+
)
41+
42+
var _ = registry.RegisterFunc(configDropHelmLabels, handlerDropHelmLabels)
43+
44+
var configDropHelmLabels = &pkg.HookConfig{
45+
OnAfterHelm: &pkg.OrderedConfig{Order: 10},
46+
Kubernetes: []pkg.KubernetesConfig{
47+
{
48+
Name: vmClassSnapshot,
49+
APIVersion: "deckhouse.io/v1alpha2",
50+
Kind: "VirtualMachineClass",
51+
JqFilter: ".metadata",
52+
NameSelector: &pkg.NameSelector{
53+
MatchNames: []string{genericVMClassName},
54+
},
55+
LabelSelector: &metav1.LabelSelector{
56+
MatchLabels: map[string]string{
57+
"app": "virtualization-controller",
58+
"app.kubernetes.io/managed-by": "Helm",
59+
"heritage": "deckhouse",
60+
"module": settings.ModuleName,
61+
},
62+
},
63+
ExecuteHookOnEvents: ptr.To(false),
64+
},
65+
},
66+
67+
Queue: fmt.Sprintf("modules/%s", settings.ModuleName),
68+
}
69+
70+
type VMClassMetadata struct {
71+
Name string `json:"name"`
72+
Labels map[string]string `json:"labels"`
73+
}
74+
75+
func handlerDropHelmLabels(_ context.Context, input *pkg.HookInput) error {
76+
snaps := input.Snapshots.Get(vmClassSnapshot)
77+
if len(snaps) == 0 {
78+
return nil
79+
}
80+
81+
vmClass := &VMClassMetadata{}
82+
err := snaps[0].UnmarshalTo(vmClass)
83+
if err != nil {
84+
input.Logger.Error("failed to unmarshal VMClass", "error", err)
85+
return err
86+
}
87+
88+
if vmClass.Labels == nil {
89+
return nil
90+
}
91+
92+
// Check if VMClass has all required labels to be processed
93+
if vmClass.Labels["app"] != "virtualization-controller" ||
94+
vmClass.Labels["module"] != settings.ModuleName ||
95+
vmClass.Labels[helmManagedByLabel] != "Helm" ||
96+
vmClass.Labels[helmHeritageLabel] != "deckhouse" {
97+
input.Logger.Debug("VMClass doesn't match required labels, skipping")
98+
return nil
99+
}
100+
101+
var patches []map[string]interface{}
102+
hasLabelsToRemove := false
103+
104+
// Check and prepare patches for Helm labels
105+
if _, exists := vmClass.Labels[helmManagedByLabel]; exists {
106+
patches = append(patches, map[string]interface{}{
107+
"op": "remove",
108+
"path": fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmManagedByLabel)),
109+
"value": nil,
110+
})
111+
hasLabelsToRemove = true
112+
}
113+
114+
if _, exists := vmClass.Labels[helmHeritageLabel]; exists {
115+
patches = append(patches, map[string]interface{}{
116+
"op": "remove",
117+
"path": fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmHeritageLabel)),
118+
"value": nil,
119+
})
120+
hasLabelsToRemove = true
121+
}
122+
123+
if !hasLabelsToRemove {
124+
return nil
125+
}
126+
127+
input.Logger.Info("Removing Helm labels from generic VMClass")
128+
input.PatchCollector.PatchWithJSON(
129+
patches,
130+
"deckhouse.io/v1alpha2",
131+
"VirtualMachineClass",
132+
"",
133+
genericVMClassName,
134+
)
135+
136+
return nil
137+
}
138+
139+
func jsonPatchEscape(s string) string {
140+
return strings.NewReplacer("~", "~0", "/", "~1").Replace(s)
141+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
Copyright 2025 Flant JSC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package drop_helm_labels_from_generic_vmclass
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"testing"
23+
24+
"github.com/deckhouse/deckhouse/pkg/log"
25+
"github.com/deckhouse/module-sdk/pkg"
26+
"github.com/deckhouse/module-sdk/testing/mock"
27+
. "github.com/onsi/ginkgo/v2"
28+
. "github.com/onsi/gomega"
29+
)
30+
31+
func TestDropHelmLabelsFromGenericVMClass(t *testing.T) {
32+
RegisterFailHandler(Fail)
33+
RunSpecs(t, "Drop Helm labels from generic VMClass Suite")
34+
}
35+
36+
var _ = Describe("Drop Helm labels from generic VMClass", func() {
37+
var (
38+
snapshots *mock.SnapshotsMock
39+
patchCollector *mock.PatchCollectorMock
40+
)
41+
42+
newInput := func() *pkg.HookInput {
43+
return &pkg.HookInput{
44+
Snapshots: snapshots,
45+
PatchCollector: patchCollector,
46+
Logger: log.NewNop(),
47+
}
48+
}
49+
50+
newSnapshot := func(withManagedBy, withHeritage bool) pkg.Snapshot {
51+
return mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) (err error) {
52+
obj, ok := v.(*VMClassMetadata)
53+
Expect(ok).To(BeTrue())
54+
obj.Name = genericVMClassName
55+
obj.Labels = make(map[string]string)
56+
57+
// Required labels for VMClass to be found by the hook
58+
obj.Labels["app"] = "virtualization-controller"
59+
obj.Labels["module"] = "virtualization"
60+
61+
if withManagedBy {
62+
obj.Labels[helmManagedByLabel] = "Helm"
63+
}
64+
if withHeritage {
65+
obj.Labels[helmHeritageLabel] = "deckhouse"
66+
}
67+
68+
return nil
69+
})
70+
}
71+
72+
setSnapshots := func(snaps ...pkg.Snapshot) {
73+
snapshots.GetMock.When(vmClassSnapshot).Then(snaps)
74+
}
75+
76+
BeforeEach(func() {
77+
snapshots = mock.NewSnapshotsMock(GinkgoT())
78+
patchCollector = mock.NewPatchCollectorMock(GinkgoT())
79+
})
80+
81+
It("Should drop both Helm labels from generic VMClass with all required labels", func() {
82+
setSnapshots(newSnapshot(true, true))
83+
patchCollector.PatchWithJSONMock.Set(func(patch any, apiVersion, kind, namespace, name string, opts ...pkg.PatchCollectorOption) {
84+
Expect(apiVersion).To(Equal("deckhouse.io/v1alpha2"))
85+
Expect(kind).To(Equal("VirtualMachineClass"))
86+
Expect(namespace).To(Equal(""))
87+
Expect(name).To(Equal(genericVMClassName))
88+
Expect(opts).To(HaveLen(0))
89+
90+
jsonPatch, ok := patch.([]map[string]interface{})
91+
Expect(ok).To(BeTrue())
92+
Expect(jsonPatch).To(HaveLen(2))
93+
94+
// Check first patch (managed-by label)
95+
Expect(jsonPatch[0]["op"]).To(Equal("remove"))
96+
Expect(jsonPatch[0]["path"]).To(Equal(fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmManagedByLabel))))
97+
Expect(jsonPatch[0]["value"]).To(BeNil())
98+
99+
// Check second patch (heritage label)
100+
Expect(jsonPatch[1]["op"]).To(Equal("remove"))
101+
Expect(jsonPatch[1]["path"]).To(Equal(fmt.Sprintf("/metadata/labels/%s", jsonPatchEscape(helmHeritageLabel))))
102+
Expect(jsonPatch[1]["value"]).To(BeNil())
103+
})
104+
105+
Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed())
106+
})
107+
108+
It("Should do nothing when VMClass doesn't have all required labels", func() {
109+
// Create a snapshot with VMClass that has only some required labels
110+
partialLabelSnapshot := mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) (err error) {
111+
obj, ok := v.(*VMClassMetadata)
112+
Expect(ok).To(BeTrue())
113+
obj.Name = genericVMClassName
114+
obj.Labels = make(map[string]string)
115+
116+
// Only some required labels - VMClass won't be processed
117+
obj.Labels["app"] = "virtualization-controller"
118+
obj.Labels["module"] = "virtualization"
119+
// Missing helmManagedByLabel and helmHeritageLabel
120+
121+
return nil
122+
})
123+
124+
setSnapshots(partialLabelSnapshot)
125+
Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed())
126+
})
127+
128+
It("Should do nothing when VMClass not found", func() {
129+
setSnapshots()
130+
Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed())
131+
})
132+
133+
It("Should do nothing when VMClass exists but doesn't match label selector", func() {
134+
// Create a snapshot with VMClass that has wrong labels
135+
wrongLabelSnapshot := mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) (err error) {
136+
obj, ok := v.(*VMClassMetadata)
137+
Expect(ok).To(BeTrue())
138+
obj.Name = genericVMClassName
139+
obj.Labels = make(map[string]string)
140+
141+
// Wrong labels - VMClass won't be found by the hook
142+
obj.Labels["app"] = "wrong-app"
143+
obj.Labels["module"] = "wrong-module"
144+
obj.Labels[helmManagedByLabel] = "Helm"
145+
obj.Labels[helmHeritageLabel] = "deckhouse"
146+
147+
return nil
148+
})
149+
150+
setSnapshots(wrongLabelSnapshot)
151+
Expect(handlerDropHelmLabels(context.Background(), newInput())).To(Succeed())
152+
})
153+
})

0 commit comments

Comments
 (0)