knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/webhook/configmaps/configmaps_test.go (about)

     1  /*
     2  Copyright 2019 The Knative Authors
     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 configmaps
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"strconv"
    25  	"testing"
    26  
    27  	// Injection stuff
    28  	_ "knative.dev/pkg/client/injection/kube/client/fake"
    29  	_ "knative.dev/pkg/client/injection/kube/informers/admissionregistration/v1/validatingwebhookconfiguration/fake"
    30  	_ "knative.dev/pkg/injection/clients/namespacedkube/informers/core/v1/secret/fake"
    31  
    32  	admissionv1 "k8s.io/api/admission/v1"
    33  	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    34  	authenticationv1 "k8s.io/api/authentication/v1"
    35  	corev1 "k8s.io/api/core/v1"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	fakekubeclientset "k8s.io/client-go/kubernetes/fake"
    38  	"knative.dev/pkg/configmap"
    39  	"knative.dev/pkg/system"
    40  	"knative.dev/pkg/webhook"
    41  
    42  	_ "knative.dev/pkg/system/testing"
    43  
    44  	. "knative.dev/pkg/logging/testing"
    45  	. "knative.dev/pkg/reconciler/testing"
    46  	. "knative.dev/pkg/webhook/testing"
    47  )
    48  
    49  const (
    50  	testConfigValidationName = "configmap.webhook.knative.dev"
    51  	testConfigValidationPath = "/cm"
    52  	testConfigName           = "test-config"
    53  )
    54  
    55  var (
    56  	validations = configmap.Constructors{
    57  		testConfigName: newConfigFromConfigMap,
    58  	}
    59  	initialConfigWebhook = &admissionregistrationv1.ValidatingWebhookConfiguration{
    60  		ObjectMeta: metav1.ObjectMeta{
    61  			Name: testConfigValidationName,
    62  		},
    63  		Webhooks: []admissionregistrationv1.ValidatingWebhook{{
    64  			Name: testConfigValidationName,
    65  			ClientConfig: admissionregistrationv1.WebhookClientConfig{
    66  				Service: &admissionregistrationv1.ServiceReference{
    67  					Namespace: system.Namespace(),
    68  					Name:      "webhook",
    69  				},
    70  			},
    71  			NamespaceSelector: &metav1.LabelSelector{
    72  				MatchExpressions: []metav1.LabelSelectorRequirement{{
    73  					Key:      "pkg.knative.dev/release",
    74  					Operator: metav1.LabelSelectorOpExists,
    75  				}},
    76  			},
    77  		}},
    78  	}
    79  )
    80  
    81  func newNonRunningTestConfigValidationController(t *testing.T) (
    82  	kubeClient *fakekubeclientset.Clientset,
    83  	ac *reconciler,
    84  ) {
    85  	t.Helper()
    86  	// Create fake clients
    87  	kubeClient = fakekubeclientset.NewSimpleClientset(initialConfigWebhook)
    88  
    89  	ac = newTestConfigValidationController(t)
    90  	return kubeClient, ac
    91  }
    92  
    93  func newTestConfigValidationController(t *testing.T) *reconciler {
    94  	ctx, _ := SetupFakeContext(t)
    95  	ctx = webhook.WithOptions(ctx, webhook.Options{
    96  		SecretName: "webhook-secret",
    97  	})
    98  	return NewAdmissionController(ctx, testConfigValidationName, testConfigValidationPath,
    99  		validations).Reconciler.(*reconciler)
   100  }
   101  
   102  func TestDeleteAllowedForConfigMap(t *testing.T) {
   103  	_, ac := newNonRunningTestConfigValidationController(t)
   104  
   105  	req := &admissionv1.AdmissionRequest{
   106  		Operation: admissionv1.Delete,
   107  	}
   108  
   109  	if resp := ac.Admit(TestContextWithLogger(t), req); !resp.Allowed {
   110  		t.Fatal("Unexpected denial of delete")
   111  	}
   112  }
   113  
   114  func TestConnectAllowedForConfigMap(t *testing.T) {
   115  	_, ac := newNonRunningTestConfigValidationController(t)
   116  
   117  	req := &admissionv1.AdmissionRequest{
   118  		Operation: admissionv1.Connect,
   119  	}
   120  
   121  	resp := ac.Admit(TestContextWithLogger(t), req)
   122  	if !resp.Allowed {
   123  		t.Fatalf("Unexpected denial of connect")
   124  	}
   125  }
   126  
   127  func TestNonConfigMapKindFails(t *testing.T) {
   128  	_, ac := newNonRunningTestConfigValidationController(t)
   129  
   130  	req := &admissionv1.AdmissionRequest{
   131  		Operation: admissionv1.Create,
   132  		Kind: metav1.GroupVersionKind{
   133  			Group:   "pkg.knative.dev",
   134  			Version: "v1alpha1",
   135  			Kind:    "Garbage",
   136  		},
   137  	}
   138  
   139  	ExpectFailsWith(t, ac.Admit(TestContextWithLogger(t), req), "unhandled kind")
   140  }
   141  
   142  func TestAdmitCreateValidConfigMap(t *testing.T) {
   143  	_, ac := newNonRunningTestConfigValidationController(t)
   144  
   145  	r := createValidConfigMap()
   146  	ctx := TestContextWithLogger(t)
   147  
   148  	resp := ac.Admit(ctx, createCreateConfigMapRequest(ctx, t, r))
   149  
   150  	ExpectAllowed(t, resp)
   151  }
   152  
   153  func TestDenyInvalidCreateConfigMapWithWrongType(t *testing.T) {
   154  	_, ac := newNonRunningTestConfigValidationController(t)
   155  
   156  	r := createWrongTypeConfigMap()
   157  	ctx := TestContextWithLogger(t)
   158  
   159  	resp := ac.Admit(ctx, createCreateConfigMapRequest(ctx, t, r))
   160  
   161  	ExpectFailsWith(t, resp, "invalid syntax")
   162  }
   163  
   164  func TestDenyInvalidCreateConfigMapOutOfRange(t *testing.T) {
   165  	_, ac := newNonRunningTestConfigValidationController(t)
   166  
   167  	r := createWrongValueConfigMap()
   168  	ctx := TestContextWithLogger(t)
   169  
   170  	resp := ac.Admit(ctx, createCreateConfigMapRequest(ctx, t, r))
   171  
   172  	ExpectFailsWith(t, resp, "out of range")
   173  }
   174  
   175  func TestAdmitUpdateValidConfigMap(t *testing.T) {
   176  	_, ac := newNonRunningTestConfigValidationController(t)
   177  
   178  	r := createValidConfigMap()
   179  	ctx := TestContextWithLogger(t)
   180  
   181  	resp := ac.Admit(ctx, updateCreateConfigMapRequest(ctx, t, r))
   182  
   183  	ExpectAllowed(t, resp)
   184  }
   185  
   186  func TestDenyInvalidUpdateConfigMapWithWrongType(t *testing.T) {
   187  	_, ac := newNonRunningTestConfigValidationController(t)
   188  
   189  	r := createWrongTypeConfigMap()
   190  	ctx := TestContextWithLogger(t)
   191  
   192  	resp := ac.Admit(ctx, createCreateConfigMapRequest(ctx, t, r))
   193  
   194  	ExpectFailsWith(t, resp, "invalid syntax")
   195  }
   196  
   197  func TestDenyInvalidUpdateConfigMapOutOfRange(t *testing.T) {
   198  	_, ac := newNonRunningTestConfigValidationController(t)
   199  
   200  	r := createWrongValueConfigMap()
   201  	ctx := TestContextWithLogger(t)
   202  
   203  	resp := ac.Admit(ctx, createCreateConfigMapRequest(ctx, t, r))
   204  
   205  	ExpectFailsWith(t, resp, "out of range")
   206  }
   207  
   208  func TestAllowConfigMapExample(t *testing.T) {
   209  	_, ac := newNonRunningTestConfigValidationController(t)
   210  
   211  	r := createValidConfigMap()
   212  	// Add an _example field but add no annotation.
   213  	r.Data[configmap.ExampleKey] = "bar"
   214  	ctx := TestContextWithLogger(t)
   215  
   216  	resp := ac.Admit(ctx, createCreateConfigMapRequest(ctx, t, r))
   217  
   218  	ExpectAllowed(t, resp)
   219  }
   220  
   221  func TestAllowUnknownConfigMapExample(t *testing.T) {
   222  	_, ac := newNonRunningTestConfigValidationController(t)
   223  
   224  	r := &corev1.ConfigMap{
   225  		ObjectMeta: metav1.ObjectMeta{
   226  			Name: "some-other-config",
   227  			Annotations: map[string]string{
   228  				configmap.ExampleChecksumAnnotation: "foo",
   229  			},
   230  		},
   231  		Data: map[string]string{
   232  			configmap.ExampleKey: "bar",
   233  		},
   234  	}
   235  	ctx := TestContextWithLogger(t)
   236  
   237  	resp := ac.Admit(ctx, createCreateConfigMapRequest(ctx, t, r))
   238  
   239  	ExpectAllowed(t, resp)
   240  }
   241  
   242  func TestDenyInvalidUpdateConfigMapExample(t *testing.T) {
   243  	_, ac := newNonRunningTestConfigValidationController(t)
   244  
   245  	r := &corev1.ConfigMap{
   246  		ObjectMeta: metav1.ObjectMeta{
   247  			Name: testConfigName,
   248  			Annotations: map[string]string{
   249  				configmap.ExampleChecksumAnnotation: "foo",
   250  			},
   251  		},
   252  		Data: map[string]string{
   253  			configmap.ExampleKey: "bar",
   254  		},
   255  	}
   256  	ctx := TestContextWithLogger(t)
   257  
   258  	resp := ac.Admit(ctx, createCreateConfigMapRequest(ctx, t, r))
   259  
   260  	ExpectFailsWith(t, resp, fmt.Sprintf("a key in %q", configmap.ExampleKey))
   261  }
   262  
   263  type config struct {
   264  	value float64
   265  }
   266  
   267  func newConfigFromConfigMap(configMap *corev1.ConfigMap) (*config, error) {
   268  	data := configMap.Data
   269  	cfg := &config{}
   270  	for _, b := range []struct {
   271  		key   string
   272  		field *float64
   273  	}{{
   274  		key:   "value",
   275  		field: &cfg.value,
   276  	}} {
   277  		if raw, ok := data[b.key]; !ok {
   278  			return nil, errors.New("not found")
   279  		} else if val, err := strconv.ParseFloat(raw, 64); err != nil {
   280  			return nil, err
   281  		} else {
   282  			*b.field = val
   283  		}
   284  	}
   285  
   286  	// some sample validation on the value
   287  	if cfg.value > 2.0 || cfg.value < 0.0 {
   288  		return nil, errors.New("out of range")
   289  	}
   290  
   291  	return cfg, nil
   292  }
   293  
   294  func createValidConfigMap() *corev1.ConfigMap {
   295  	return createConfigMap("1.5")
   296  }
   297  
   298  func createWrongTypeConfigMap() *corev1.ConfigMap {
   299  	return createConfigMap("bad")
   300  }
   301  
   302  func createWrongValueConfigMap() *corev1.ConfigMap {
   303  	return createConfigMap("2.5")
   304  }
   305  
   306  func createConfigMap(value string) *corev1.ConfigMap {
   307  	return &corev1.ConfigMap{
   308  		ObjectMeta: metav1.ObjectMeta{
   309  			Namespace: system.Namespace(),
   310  			Name:      testConfigName,
   311  		},
   312  		Data: map[string]string{
   313  			"value": value,
   314  		},
   315  	}
   316  }
   317  
   318  func createCreateConfigMapRequest(ctx context.Context, t *testing.T, r *corev1.ConfigMap) *admissionv1.AdmissionRequest {
   319  	return configMapRequest(t, r, admissionv1.Create)
   320  }
   321  
   322  func updateCreateConfigMapRequest(ctx context.Context, t *testing.T, r *corev1.ConfigMap) *admissionv1.AdmissionRequest {
   323  	return configMapRequest(t, r, admissionv1.Update)
   324  }
   325  
   326  func configMapRequest(
   327  	t *testing.T,
   328  	r *corev1.ConfigMap,
   329  	o admissionv1.Operation,
   330  ) *admissionv1.AdmissionRequest {
   331  	t.Helper()
   332  	req := &admissionv1.AdmissionRequest{
   333  		Operation: o,
   334  		Kind: metav1.GroupVersionKind{
   335  			Group:   "",
   336  			Version: "v1",
   337  			Kind:    "ConfigMap",
   338  		},
   339  		UserInfo: authenticationv1.UserInfo{Username: "mattmoor"},
   340  	}
   341  	marshaled, err := json.Marshal(r)
   342  	if err != nil {
   343  		t.Fatal("Failed to marshal resource:", err)
   344  	}
   345  	req.Object.Raw = marshaled
   346  	req.Resource.Group = ""
   347  	return req
   348  }