Skip to content

Commit 4ca6ded

Browse files
authored
feat: Add ValidatingAdmissionPolicy for managedresource (#1179)
2 parents a315f95 + 74eac8c commit 4ca6ded

File tree

10 files changed

+694
-62
lines changed

10 files changed

+694
-62
lines changed

cmd/crdinstaller/main.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"flag"
1212
"fmt"
1313

14+
admv1 "k8s.io/api/admissionregistration/v1"
1415
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1516
"k8s.io/apimachinery/pkg/runtime"
1617
"k8s.io/klog/v2"
@@ -21,9 +22,7 @@ import (
2122
"go.goms.io/fleet/cmd/crdinstaller/utils"
2223
)
2324

24-
var (
25-
mode = flag.String("mode", "", "Mode to run in: 'hub' or 'member' (required)")
26-
)
25+
var mode = flag.String("mode", "", "Mode to run in: 'hub' or 'member' (required)")
2726

2827
func main() {
2928
klog.InitFlags(nil)
@@ -55,10 +54,12 @@ func main() {
5554
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
5655
klog.Fatalf("Failed to add apiextensions scheme: %v", err)
5756
}
57+
if err := admv1.AddToScheme(scheme); err != nil {
58+
klog.Fatalf("Failed to add admissionregistration scheme: %v", err)
59+
}
5860
client, err := client.New(config, client.Options{
5961
Scheme: scheme,
6062
})
61-
6263
if err != nil {
6364
klog.Fatalf("Failed to create Kubernetes client: %v", err)
6465
}
@@ -70,6 +71,13 @@ func main() {
7071
}
7172

7273
klog.Infof("Successfully installed %s CRDs", *mode)
74+
75+
if err := utils.InstallManagedResourceVAP(ctx, client, *mode); err != nil {
76+
klog.Warningf("Failed to install managed resource ValidatingAdmissionPolicy: %v", err)
77+
return
78+
}
79+
80+
klog.Infof("Successfully installed %s managed resource ValidatingAdmissionPolicy", *mode)
7381
}
7482

7583
// installCRDs installs the CRDs from the specified directory based on the mode.

cmd/crdinstaller/utils/util.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@ import (
1515
"strings"
1616

1717
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
18+
"k8s.io/apimachinery/pkg/api/meta"
1819
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1920
"k8s.io/apimachinery/pkg/runtime"
2021
"k8s.io/apimachinery/pkg/runtime/serializer"
2122
"k8s.io/klog/v2"
2223
"sigs.k8s.io/controller-runtime/pkg/client"
2324
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
25+
26+
"go.goms.io/fleet/pkg/webhook/managedresource"
2427
)
2528

2629
const (
@@ -51,7 +54,7 @@ func InstallCRD(ctx context.Context, client client.Client, crd *apiextensionsv1.
5154
},
5255
}
5356

54-
createOrUpdateRes, err := controllerutil.CreateOrUpdate(ctx, client, &existingCRD, func() error {
57+
mutFn := func() error {
5558
// Copy spec from our decoded CRD to the object we're creating/updating.
5659
existingCRD.Spec = crd.Spec
5760

@@ -65,14 +68,24 @@ func InstallCRD(ctx context.Context, client client.Client, crd *apiextensionsv1.
6568
// needed for clean up of CRD by kube-addon-manager.
6669
existingCRD.Labels[AzureManagedLabelKey] = FleetLabelValue
6770
return nil
68-
})
71+
}
72+
err := install(ctx, client, &existingCRD, mutFn)
73+
if err == nil {
74+
klog.Infof("Successfully created/updated CRD %s", crd.Name)
75+
}
76+
return err
77+
}
6978

79+
func install(ctx context.Context, client client.Client, obj client.Object, mut func() error) error {
80+
if mut == nil {
81+
mut = func() error { return nil }
82+
}
83+
84+
result, err := controllerutil.CreateOrUpdate(ctx, client, obj, mut)
7085
if err != nil {
71-
klog.ErrorS(err, "Failed to create or update CRD", "name", crd.Name, "operation", createOrUpdateRes)
86+
klog.ErrorS(err, "Failed to create or update", "kind", obj.GetObjectKind().GroupVersionKind().Kind, "name", obj.GetName(), "operation", result)
7287
return err
7388
}
74-
75-
klog.Infof("Successfully created/updated CRD %s", crd.Name)
7689
return nil
7790
}
7891

@@ -161,3 +174,19 @@ func GetCRDFromPath(crdPath string, scheme *runtime.Scheme) (*apiextensionsv1.Cu
161174

162175
return crd, nil
163176
}
177+
178+
func InstallManagedResourceVAP(ctx context.Context, c client.Client, mode string) error {
179+
vap := managedresource.GetValidatingAdmissionPolicy(mode == "hub")
180+
vapBinding := managedresource.GetValidatingAdmissionPolicyBinding()
181+
for _, ob := range []client.Object{vap, vapBinding} {
182+
if err := install(ctx, c, ob, nil); err != nil {
183+
if meta.IsNoMatchError(err) {
184+
klog.Infof("Cluster does not support %s resource, skipping installation", ob.GetObjectKind().GroupVersionKind().Kind)
185+
return nil
186+
}
187+
return err
188+
}
189+
}
190+
klog.Infof("Successfully installed managed resource ValidatingAdmissionPolicy")
191+
return nil
192+
}

cmd/crdinstaller/utils/util_test.go

Lines changed: 227 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,24 @@ Licensed under the MIT license.
77
package utils
88

99
import (
10+
"context"
1011
"os"
1112
"testing"
1213

1314
"github.com/google/go-cmp/cmp"
1415
"github.com/google/go-cmp/cmp/cmpopts"
16+
admv1 "k8s.io/api/admissionregistration/v1"
1517
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
18+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1619
"k8s.io/apimachinery/pkg/runtime"
20+
"k8s.io/apimachinery/pkg/types"
21+
"sigs.k8s.io/controller-runtime/pkg/client"
22+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1723
)
1824

19-
var (
20-
lessFunc = func(s1, s2 string) bool {
21-
return s1 < s2
22-
}
23-
)
25+
var lessFunc = func(s1, s2 string) bool {
26+
return s1 < s2
27+
}
2428

2529
// Test using the actual config/crd/bases directory.
2630
func TestCollectCRDFileNamesWithActualPath(t *testing.T) {
@@ -114,3 +118,221 @@ func runTest(t *testing.T, crdPath string) {
114118
})
115119
}
116120
}
121+
122+
func TestInstallCRD(t *testing.T) {
123+
scheme := runtime.NewScheme()
124+
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
125+
t.Fatalf("Failed to add apiextensions scheme: %v", err)
126+
}
127+
128+
testCRD := &apiextensionsv1.CustomResourceDefinition{
129+
ObjectMeta: metav1.ObjectMeta{
130+
Name: "test.example.com",
131+
},
132+
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
133+
Group: "example.com",
134+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
135+
{
136+
Name: "v1",
137+
Served: true,
138+
Storage: true,
139+
Schema: &apiextensionsv1.CustomResourceValidation{
140+
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
141+
Type: "object",
142+
},
143+
},
144+
},
145+
},
146+
Scope: apiextensionsv1.NamespaceScoped,
147+
Names: apiextensionsv1.CustomResourceDefinitionNames{
148+
Plural: "tests",
149+
Singular: "test",
150+
Kind: "Test",
151+
},
152+
},
153+
}
154+
155+
tests := []struct {
156+
name string
157+
crd *apiextensionsv1.CustomResourceDefinition
158+
wantError bool
159+
}{
160+
{
161+
name: "successful CRD installation",
162+
crd: testCRD,
163+
wantError: false,
164+
},
165+
}
166+
167+
for _, tt := range tests {
168+
t.Run(tt.name, func(t *testing.T) {
169+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
170+
err := InstallCRD(context.Background(), fakeClient, tt.crd)
171+
172+
if tt.wantError {
173+
if err == nil {
174+
t.Errorf("InstallCRD() expected error but got none")
175+
}
176+
return
177+
}
178+
179+
if err != nil {
180+
t.Errorf("InstallCRD() unexpected error: %v", err)
181+
return
182+
}
183+
184+
var installedCRD apiextensionsv1.CustomResourceDefinition
185+
err = fakeClient.Get(context.Background(), types.NamespacedName{Name: tt.crd.Name}, &installedCRD)
186+
if err != nil {
187+
t.Errorf("Failed to get installed CRD: %v", err)
188+
return
189+
}
190+
191+
if installedCRD.Labels[CRDInstallerLabelKey] != "true" {
192+
t.Errorf("Expected CRD label %s to be 'true', got %q", CRDInstallerLabelKey, installedCRD.Labels[CRDInstallerLabelKey])
193+
}
194+
195+
if installedCRD.Labels[AzureManagedLabelKey] != FleetLabelValue {
196+
t.Errorf("Expected CRD label %s to be %q, got %q", AzureManagedLabelKey, FleetLabelValue, installedCRD.Labels[AzureManagedLabelKey])
197+
}
198+
199+
if diff := cmp.Diff(tt.crd.Spec, installedCRD.Spec); diff != "" {
200+
t.Errorf("CRD spec mismatch (-want +got):\n%s", diff)
201+
}
202+
})
203+
}
204+
}
205+
206+
func TestInstall(t *testing.T) {
207+
scheme := runtime.NewScheme()
208+
if err := apiextensionsv1.AddToScheme(scheme); err != nil {
209+
t.Fatalf("Failed to add apiextensions scheme: %v", err)
210+
}
211+
212+
testCRD := &apiextensionsv1.CustomResourceDefinition{
213+
ObjectMeta: metav1.ObjectMeta{
214+
Name: "test.example.com",
215+
},
216+
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
217+
Group: "example.com",
218+
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
219+
{
220+
Name: "v1",
221+
Served: true,
222+
Storage: true,
223+
Schema: &apiextensionsv1.CustomResourceValidation{
224+
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
225+
Type: "object",
226+
},
227+
},
228+
},
229+
},
230+
Scope: apiextensionsv1.NamespaceScoped,
231+
Names: apiextensionsv1.CustomResourceDefinitionNames{
232+
Plural: "tests",
233+
Singular: "test",
234+
Kind: "Test",
235+
},
236+
},
237+
}
238+
239+
tests := []struct {
240+
name string
241+
obj client.Object
242+
mutFunc func() error
243+
wantError bool
244+
}{
245+
{
246+
name: "successful install with mutation",
247+
obj: testCRD,
248+
mutFunc: func() error {
249+
if testCRD.Labels == nil {
250+
testCRD.Labels = make(map[string]string)
251+
}
252+
testCRD.Labels["test"] = "value"
253+
return nil
254+
},
255+
wantError: false,
256+
},
257+
{
258+
name: "successful install without mutation",
259+
obj: &apiextensionsv1.CustomResourceDefinition{
260+
ObjectMeta: metav1.ObjectMeta{
261+
Name: "test2.example.com",
262+
},
263+
Spec: testCRD.Spec,
264+
},
265+
mutFunc: func() error {
266+
return nil
267+
},
268+
wantError: false,
269+
},
270+
}
271+
272+
for _, tt := range tests {
273+
t.Run(tt.name, func(t *testing.T) {
274+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
275+
err := install(context.Background(), fakeClient, tt.obj, tt.mutFunc)
276+
277+
if tt.wantError {
278+
if err == nil {
279+
t.Errorf("install() expected error but got none")
280+
}
281+
return
282+
}
283+
284+
if err != nil {
285+
t.Errorf("install() unexpected error: %v", err)
286+
return
287+
}
288+
289+
var installed apiextensionsv1.CustomResourceDefinition
290+
err = fakeClient.Get(context.Background(), types.NamespacedName{Name: tt.obj.GetName()}, &installed)
291+
if err != nil {
292+
t.Errorf("Failed to get installed object: %v", err)
293+
return
294+
}
295+
296+
if tt.mutFunc != nil && tt.obj.GetName() == "test.example.com" {
297+
if installed.Labels["test"] != "value" {
298+
t.Errorf("Expected label 'test' to be 'value', got %q", installed.Labels["test"])
299+
}
300+
}
301+
})
302+
}
303+
}
304+
305+
func TestInstallManagedResourceVAP(t *testing.T) {
306+
tests := []struct {
307+
name string
308+
mode string
309+
}{
310+
{
311+
name: "hub mode",
312+
mode: "hub",
313+
},
314+
{
315+
name: "member mode",
316+
mode: "member",
317+
},
318+
}
319+
320+
for _, tt := range tests {
321+
t.Run(tt.name, func(t *testing.T) {
322+
scheme := runtime.NewScheme()
323+
if err := admv1.AddToScheme(scheme); err != nil {
324+
t.Fatalf("Failed to add admissionregistration scheme: %v", err)
325+
}
326+
327+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
328+
err := InstallManagedResourceVAP(context.Background(), fakeClient, tt.mode)
329+
330+
// The function should complete without errors
331+
// The actual installation behavior depends on the RESTMapper implementation
332+
// which is difficult to test reliably with the fake client
333+
if err != nil {
334+
t.Errorf("InstallManagedResourceVAP() unexpected error: %v", err)
335+
}
336+
})
337+
}
338+
}

docker/crd-installer.Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ RUN go mod download
1111

1212
# Copy the go source
1313
COPY cmd/crdinstaller/ cmd/crdinstaller/
14+
COPY pkg pkg
1415

1516
ARG TARGETARCH
1617

0 commit comments

Comments
 (0)