Skip to content

Commit 7ecb54c

Browse files
authored
feat: add glob support for config interrupts (#98)
* feat: add glob support for config interrupts * add e2e tests and remove unecsssary temp copy * assert config pod for globbed interrupts
1 parent 9a93acd commit 7ecb54c

File tree

6 files changed

+231
-8
lines changed

6 files changed

+231
-8
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
---
18+
apiVersion: v1
19+
kind: Pod
20+
metadata:
21+
namespace: skyhook
22+
labels:
23+
skyhook.nvidia.com/name: config-skyhook
24+
skyhook.nvidia.com/package: dexter-1.2.3
25+
annotations:
26+
("skyhook.nvidia.com/package" && parse_json("skyhook.nvidia.com/package")):
27+
{
28+
"name": "dexter",
29+
"version": "1.2.3",
30+
"skyhook": "config-skyhook",
31+
"stage": "config"
32+
}
33+
spec:
34+
initContainers:
35+
- name: dexter-init
36+
- name: dexter-config
37+
args:
38+
([0]): config
39+
([1]): /root
40+
(length(@)): 3
41+
- name: dexter-configcheck
42+
args:
43+
([0]): config-check
44+
([1]): /root
45+
(length(@)): 3
46+
---
47+
apiVersion: v1
48+
kind: Node
49+
metadata:
50+
labels:
51+
skyhook.nvidia.com/test-node: skyhooke2e
52+
skyhook.nvidia.com/status_config-skyhook: complete
53+
annotations:
54+
("skyhook.nvidia.com/nodeState_config-skyhook" && parse_json("skyhook.nvidia.com/nodeState_config-skyhook")):
55+
{
56+
"dexter|1.2.3": {
57+
"name": "dexter",
58+
"version": "1.2.3",
59+
"image": "ghcr.io/nvidia/skyhook/agentless",
60+
"stage": "post-interrupt",
61+
"state": "complete"
62+
}
63+
}
64+
skyhook.nvidia.com/status_config-skyhook: complete
65+
status:
66+
(conditions[?type == 'skyhook.nvidia.com/config-skyhook/NotReady']):
67+
- reason: "Complete"
68+
status: "False"
69+
(conditions[?type == 'skyhook.nvidia.com/config-skyhook/Erroring']):
70+
- reason: "Not Erroring"
71+
status: "False"
72+
---
73+
apiVersion: skyhook.nvidia.com/v1alpha1
74+
kind: Skyhook
75+
metadata:
76+
name: config-skyhook
77+
status:
78+
status: complete
79+
nodeState:
80+
(values(@)):
81+
- dexter|1.2.3:
82+
name: dexter
83+
state: complete
84+
version: '1.2.3'
85+
stage: post-interrupt
86+
image: ghcr.io/nvidia/skyhook/agentless
87+
nodeStatus:
88+
(values(@)):
89+
- complete
90+
---
91+
kind: ConfigMap
92+
apiVersion: v1
93+
metadata:
94+
name: config-skyhook-dexter-1.2.3
95+
namespace: skyhook
96+
labels:
97+
skyhook.nvidia.com/name: config-skyhook
98+
ownerReferences:
99+
- apiVersion: skyhook.nvidia.com/v1alpha1
100+
blockOwnerDeletion: true
101+
controller: true
102+
kind: Skyhook
103+
name: config-skyhook
104+
data:
105+
game.properties: |
106+
changed via glob

k8s-tests/chainsaw/skyhook/config-skyhook/chainsaw-test.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,8 @@ spec:
6565
file: update-no-interrupt.yaml
6666
- assert:
6767
file: assert-update-no-interrupt.yaml
68+
- try:
69+
- apply:
70+
file: update-glob.yaml
71+
- assert:
72+
file: assert-update-glob.yaml
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
apiVersion: skyhook.nvidia.com/v1alpha1
18+
kind: Skyhook
19+
metadata:
20+
name: config-skyhook
21+
spec:
22+
packages:
23+
dexter:
24+
# Add a globbed configInterrupt that matches at least one key
25+
configInterrupts:
26+
"*.properties":
27+
type: service
28+
services: [rsyslog]
29+
# Change a key that should match the glob
30+
configMap:
31+
game.properties: |
32+
changed via glob

operator/api/v1alpha1/skyhook_webhook.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ package v1alpha1
2121
import (
2222
"context"
2323
"fmt"
24+
"path/filepath"
2425
"regexp"
2526
"strings"
2627

@@ -171,10 +172,30 @@ func (r *Skyhook) Validate() error {
171172
}
172173

173174
// test to make sure that the config interrupts are for valid packages
174-
for configMap := range v.ConfigInterrupts {
175-
if _, exists := v.ConfigMap[configMap]; !exists {
176-
return fmt.Errorf("error config interrupt for key that doesn't exist: %s doesn't exist as a configmap", configMap)
175+
for pattern := range v.ConfigInterrupts {
176+
// exact key present
177+
if _, exists := v.ConfigMap[pattern]; exists {
178+
continue
177179
}
180+
181+
// Only '*' is supported as a glob meta character
182+
isGlob := strings.Contains(pattern, "*")
183+
if isGlob {
184+
matchedAny := false
185+
for key := range v.ConfigMap {
186+
if ok, err := filepath.Match(pattern, key); err == nil && ok {
187+
matchedAny = true
188+
break
189+
}
190+
}
191+
if matchedAny {
192+
continue
193+
}
194+
return fmt.Errorf("error config interrupt glob %q does not match any configMap keys", pattern)
195+
}
196+
197+
// not a glob and not an exact key
198+
return fmt.Errorf("error config interrupt for key that doesn't exist: %s doesn't exist as a configmap", pattern)
178199
}
179200

180201
image, version, found := strings.Cut(v.Image, ":")

operator/api/v1alpha1/skyhook_webhook_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,51 @@ var _ = Describe("Skyhook Webhook", func() {
277277
Expect(err).To(BeNil())
278278
})
279279

280+
It("should allow glob patterns in configInterrupts that match at least one key", func() {
281+
skyhook := &Skyhook{
282+
ObjectMeta: metav1.ObjectMeta{Name: "test"},
283+
Spec: SkyhookSpec{
284+
Packages: Packages{
285+
"foo": {
286+
PackageRef: PackageRef{Name: "foo", Version: "1.0.0"},
287+
ConfigMap: map[string]string{
288+
"config.sh": "abc",
289+
"upgrade.sh": "def",
290+
},
291+
ConfigInterrupts: map[string]Interrupt{
292+
"*.sh": {Type: SERVICE, Services: []string{"kubelet"}},
293+
},
294+
},
295+
},
296+
},
297+
}
298+
299+
_, err := skyhookWebhook.ValidateCreate(ctx, skyhook)
300+
Expect(err).To(BeNil())
301+
})
302+
303+
It("should reject glob patterns in configInterrupts that match no keys", func() {
304+
skyhook := &Skyhook{
305+
ObjectMeta: metav1.ObjectMeta{Name: "test"},
306+
Spec: SkyhookSpec{
307+
Packages: Packages{
308+
"foo": {
309+
PackageRef: PackageRef{Name: "foo", Version: "1.0.0"},
310+
ConfigMap: map[string]string{
311+
"config.txt": "abc",
312+
},
313+
ConfigInterrupts: map[string]Interrupt{
314+
"*.sh": {Type: SERVICE, Services: []string{"kubelet"}},
315+
},
316+
},
317+
},
318+
},
319+
}
320+
321+
_, err := skyhookWebhook.ValidateCreate(ctx, skyhook)
322+
Expect(err).ToNot(BeNil())
323+
})
324+
280325
It("Should deny if ambiguous version match", func() {
281326
skyhook := &Skyhook{
282327
ObjectMeta: metav1.ObjectMeta{Name: "test"},

operator/internal/wrapper/skyhook.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ package wrapper
2020

2121
import (
2222
"fmt"
23+
"path/filepath"
24+
"strings"
2325

2426
"github.com/NVIDIA/skyhook/operator/api/v1alpha1"
2527
"github.com/NVIDIA/skyhook/operator/internal/version"
@@ -140,13 +142,25 @@ func (s *Skyhook) GetConfigInterrupts() map[string][]*v1alpha1.Interrupt {
140142
for _pkg := range s.Spec.Packages {
141143
_package := s.Spec.Packages[_pkg]
142144

145+
// Track duplicates to avoid adding the same interrupt multiple times per package
146+
seen := make(map[string]struct{})
147+
143148
for _, update := range s.Status.ConfigUpdates[_package.Name] {
144-
if interrupt, exists := _package.ConfigInterrupts[update]; exists {
145-
if interrupts[_package.Name] == nil {
146-
interrupts[_package.Name] = make([]*v1alpha1.Interrupt, 0)
149+
for pattern, interrupt := range _package.ConfigInterrupts {
150+
// filepath.Match treats a non-glob pattern as a literal
151+
if ok, err := filepath.Match(pattern, update); err == nil && ok {
152+
if interrupts[_package.Name] == nil {
153+
interrupts[_package.Name] = make([]*v1alpha1.Interrupt, 0)
154+
}
155+
156+
key := fmt.Sprintf("%s|%s", interrupt.Type, strings.Join(interrupt.Services, ","))
157+
if _, exists := seen[key]; exists {
158+
continue
159+
}
160+
seen[key] = struct{}{}
161+
162+
interrupts[_package.Name] = append(interrupts[_package.Name], &interrupt)
147163
}
148-
149-
interrupts[_package.Name] = append(interrupts[_package.Name], &interrupt)
150164
}
151165
}
152166
}

0 commit comments

Comments
 (0)