...

Source file src/sigs.k8s.io/cli-utils/pkg/apply/mutator/apply_time_mutator_test.go

Documentation: sigs.k8s.io/cli-utils/pkg/apply/mutator

     1  // Copyright 2021 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package mutator
     5  
     6  import (
     7  	"context"
     8  	"testing"
     9  
    10  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    11  	"k8s.io/apimachinery/pkg/api/meta/testrestmapper"
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    14  	"k8s.io/apimachinery/pkg/runtime/schema"
    15  	"k8s.io/client-go/dynamic"
    16  	"k8s.io/client-go/dynamic/fake"
    17  	"k8s.io/kubectl/pkg/scheme"
    18  	"sigs.k8s.io/cli-utils/pkg/apply/cache"
    19  	ktestutil "sigs.k8s.io/cli-utils/pkg/kstatus/polling/testutil"
    20  	"sigs.k8s.io/cli-utils/pkg/object"
    21  
    22  	// Using gopkg.in/yaml.v3 instead of sigs.k8s.io/yaml on purpose.
    23  	// yaml.v3 correctly parses ints:
    24  	// https://github.com/kubernetes-sigs/yaml/issues/45
    25  	"gopkg.in/yaml.v3"
    26  
    27  	"github.com/stretchr/testify/require"
    28  )
    29  
    30  var expectedReason = "object contained annotation: config.kubernetes.io/apply-time-mutation"
    31  
    32  var pod1y = `
    33  apiVersion: v1
    34  kind: Pod
    35  metadata:
    36    name: pod-name
    37    namespace: pod-namespace
    38    annotations:
    39      config.kubernetes.io/apply-time-mutation: |
    40        - sourceRef:
    41            group: networking.k8s.io
    42            kind: Ingress
    43            name: ingress1-name
    44            namespace: ingress-namespace
    45          sourcePath: $.spec.rules[0].http.paths[0].backend.service.port.number
    46          targetPath: $.spec.containers[0].env[0].value
    47          token: ${service-port}
    48  spec:
    49    containers:
    50    - name: app
    51      image: example:1.0
    52      ports:
    53      - containerPort: 80
    54      env:
    55      - name: SERVICE_PORT
    56        value: ${service-port}
    57  `
    58  
    59  var ingress1y = `
    60  apiVersion: networking.k8s.io/v1
    61  kind: Ingress
    62  metadata:
    63    name: ingress1-name
    64    namespace: ingress-namespace
    65    annotations:
    66      nginx.ingress.kubernetes.io/rewrite-target: /
    67  spec:
    68    rules:
    69    - http:
    70        paths:
    71        - path: /old
    72          pathType: Prefix
    73          backend:
    74            service:
    75              name: old
    76              port:
    77                number: 80
    78  `
    79  
    80  var pod2y = `
    81  apiVersion: v1
    82  kind: Pod
    83  metadata:
    84    name: pod-name
    85    namespace: pod-namespace
    86    annotations:
    87      config.kubernetes.io/apply-time-mutation: |
    88        - sourceRef:
    89            group: networking.k8s.io
    90            kind: Ingress
    91            name: ingress1-name
    92            namespace: ingress-namespace
    93          sourcePath: $.spec.rules[?(@.http)].http.paths[?(@.path=="/old")].backend.service.port.number
    94          targetPath: $.spec.containers[?(@.name=="app")].env[?(@.name=="SERVICE_PORT")].value
    95          token: ${service-port}
    96        - sourceRef:
    97            group: networking.k8s.io
    98            kind: Ingress
    99            name: ingress1-name
   100            namespace: ingress-namespace
   101          sourcePath: $.spec.rules[?(@.http)].http.paths[?(@.path=="/old")].backend.service.name
   102          targetPath: $.spec.containers[?(@.name=="app")].env[?(@.name=="SERVICE_NAME")].value
   103  spec:
   104    containers:
   105    - name: app
   106      image: example:1.0
   107      ports:
   108      - containerPort: 80
   109      env:
   110      - name: SERVICE_PORT
   111        value: ${service-port}
   112      - name: SERVICE_NAME
   113        value: "" # field must exist to be mutated
   114  `
   115  
   116  var pod3y = `
   117  apiVersion: v1
   118  kind: Pod
   119  metadata:
   120    name: pod-name
   121    namespace: pod-namespace
   122    annotations:
   123      config.kubernetes.io/apply-time-mutation: |
   124        - sourceRef:
   125            kind: ConfigMap
   126            name: map1-name
   127            namespace: map-namespace
   128          sourcePath: $.data.image
   129          targetPath: $.spec.containers[?(@.name=="app")].image
   130          token: ${app-image}
   131        - sourceRef:
   132            kind: ConfigMap
   133            name: map1-name
   134            namespace: map-namespace
   135          sourcePath: $.data.version
   136          targetPath: $.spec.containers[?(@.name=="app")].image
   137          token: ${app-version}
   138        - sourceRef:
   139            group: networking.k8s.io
   140            kind: Ingress
   141            name: ingress1-name
   142            namespace: ingress-namespace
   143          sourcePath: $.spec.rules[?(@.http)].http.paths[?(@.path=="/old")].backend.service.port.number
   144          targetPath: $.spec.containers[?(@.name=="app")].env[?(@.name=="SERVICE_PORT")].value
   145          token: ${service-port}
   146  spec:
   147    containers:
   148    - name: app
   149      image: ${app-image}:${app-version}
   150      ports:
   151      - containerPort: 80
   152      env:
   153      - name: SERVICE_PORT
   154        value: ${service-port}
   155  `
   156  
   157  var configmap1y = `
   158  apiVersion: v1
   159  kind: ConfigMap
   160  metadata:
   161    name: map1-name
   162    namespace: map-namespace
   163  data:
   164    image: traefik/whoami
   165    version: "1.0"
   166  `
   167  
   168  var configmap2y = `
   169  apiVersion: v1
   170  kind: ConfigMap
   171  metadata:
   172    name: map2-name
   173    namespace: map-namespace
   174    annotations:
   175      config.kubernetes.io/apply-time-mutation: |
   176        - sourceRef:
   177            kind: ConfigMap
   178            name: map1-name
   179            namespace: map-namespace
   180          sourcePath: $.data
   181          targetPath: $.data.json
   182          token: ${map-data-json}
   183  data:
   184    json: "[{\"π\":3.14},${map-data-json}]"
   185  `
   186  
   187  // invalid
   188  var configmap3y = `
   189  apiVersion: v1
   190  kind: ConfigMap
   191  metadata:
   192    name: map3-name
   193    namespace: map-namespace
   194    annotations:
   195      config.kubernetes.io/apply-time-mutation: "not a valid substitution list"
   196  data: {}
   197  `
   198  
   199  // self-reference
   200  var configmap4y = `
   201  apiVersion: v1
   202  kind: ConfigMap
   203  metadata:
   204    name: map4-name
   205    namespace: map-namespace
   206    annotations:
   207      config.kubernetes.io/apply-time-mutation: |
   208        - sourceRef:
   209            kind: ConfigMap
   210            name: map4-name
   211            namespace: map-namespace
   212          sourcePath: $.data
   213          targetPath: $.data
   214  data:
   215    movie: inception
   216    slogan: we need to go deeper
   217  `
   218  
   219  var ingress2y = `
   220  apiVersion: networking.k8s.io/v1
   221  kind: Ingress
   222  metadata:
   223    name: ingress2-name
   224    namespace: ingress-namespace
   225    annotations:
   226      nginx.ingress.kubernetes.io/rewrite-target: /
   227      config.kubernetes.io/apply-time-mutation: |
   228        - sourceRef:
   229            apiVersion: networking.k8s.io/v1
   230            kind: Ingress
   231            name: ingress1-name
   232            namespace: ingress-namespace
   233          sourcePath: $.spec.rules[0].http.paths[?(@.path=="/old")]
   234          targetPath: $.spec.rules[0].http.paths[(@.length-1)]
   235  spec:
   236    rules:
   237    - http:
   238        paths:
   239        - path: /new
   240          pathType: Prefix
   241          backend:
   242            service:
   243              name: new
   244              port:
   245                number: 80
   246        - {} # field must exist to be mutated
   247  `
   248  
   249  var joinedPathsYaml = `
   250  - path: /new
   251    pathType: Prefix
   252    backend:
   253      service:
   254        name: new
   255        port:
   256          number: 80
   257  - path: /old
   258    pathType: Prefix
   259    backend:
   260      service:
   261        name: old
   262        port:
   263          number: 80
   264  `
   265  
   266  var service1y = `
   267  apiVersion: v1
   268  kind: Service
   269  metadata:
   270    name: service1-name
   271    namespace: service1-namespace
   272    annotations:
   273      config.kubernetes.io/apply-time-mutation: |
   274        - sourceRef:
   275            group: apps
   276            kind: Deployment
   277            name: deployment1-name
   278            namespace: deployment1-namespace
   279          sourcePath: $.spec.template.spec.containers[?(@.name=="tcp-handler")].ports[0].containerPort
   280          targetPath: $.spec.ports[?(@.protocol=="TCP" && @.port==80)].targetPort
   281        - sourceRef:
   282            group: apps
   283            kind: Deployment
   284            name: deployment1-name
   285            namespace: deployment1-namespace
   286          sourcePath: $.spec.template.spec.containers[?(@.name=="udp-handler")].ports[0].containerPort
   287          targetPath: $.spec.ports[?(@.protocol=="UDP" && @.port==80)].targetPort
   288  spec:
   289    selector:
   290      app: MyApp
   291    ports:
   292      - protocol: TCP
   293        port: 80
   294        targetPort: 0 # field must exist to be mutated
   295      - protocol: TCP
   296        port: 443
   297        targetPort: 443
   298      - protocol: UDP
   299        port: 80
   300        targetPort: 0 # field must exist to be mutated
   301  `
   302  
   303  var deployment1y = `
   304  apiVersion: apps/v1
   305  kind: Deployment
   306  metadata:
   307    name: deployment1-name
   308    namespace: deployment1-namespace
   309  spec:
   310    selector:
   311      matchLabels:
   312        app: example
   313    replicas: 2
   314    template:
   315      metadata:
   316        labels:
   317          app: example
   318      spec:
   319        containers:
   320        - name: tcp-handler
   321          image: example-tcp
   322          ports:
   323          - containerPort: 8080
   324        - name: udp-handler
   325          image: example-udp
   326          ports:
   327          - containerPort: 8081
   328  `
   329  
   330  var clusterrole1y = `
   331  apiVersion: rbac.authorization.k8s.io/v1
   332  kind: ClusterRole
   333  metadata:
   334    name: example-role
   335    labels:
   336      domain: example.com
   337  rules:
   338  - apiGroups: [""]
   339    resources: ["pods"]
   340    verbs: ["get", "watch", "list"]
   341  `
   342  
   343  var clusterrolebinding1y = `
   344  apiVersion: rbac.authorization.k8s.io/v1
   345  kind: ClusterRoleBinding
   346  metadata:
   347    name: read-secrets
   348    annotations:
   349      config.kubernetes.io/apply-time-mutation: |
   350        - sourceRef:
   351            apiVersion: rbac.authorization.k8s.io/v1
   352            kind: ClusterRole
   353            name: example-role
   354          sourcePath: $.metadata.labels.domain
   355          targetPath: $.subjects[0].name
   356          token: ${domain}
   357  subjects:
   358  - kind: User
   359    name: "bob@${domain}"
   360    apiGroup: rbac.authorization.k8s.io
   361  roleRef:
   362    kind: ClusterRole
   363    name: secret-reader
   364    apiGroup: rbac.authorization.k8s.io
   365  `
   366  
   367  type nestedFieldValue struct {
   368  	Field []interface{}
   369  	Value interface{}
   370  }
   371  
   372  func TestMutate(t *testing.T) {
   373  	pod1 := ktestutil.YamlToUnstructured(t, pod1y)
   374  	ingress1 := ktestutil.YamlToUnstructured(t, ingress1y)
   375  	pod2 := ktestutil.YamlToUnstructured(t, pod2y)
   376  	pod3 := ktestutil.YamlToUnstructured(t, pod3y)
   377  	configmap1 := ktestutil.YamlToUnstructured(t, configmap1y)
   378  	configmap2 := ktestutil.YamlToUnstructured(t, configmap2y)
   379  	configmap3 := ktestutil.YamlToUnstructured(t, configmap3y)
   380  	configmap4 := ktestutil.YamlToUnstructured(t, configmap4y)
   381  	ingress2 := ktestutil.YamlToUnstructured(t, ingress2y)
   382  	service1 := ktestutil.YamlToUnstructured(t, service1y)
   383  	deployment1 := ktestutil.YamlToUnstructured(t, deployment1y)
   384  	clusterrole1 := ktestutil.YamlToUnstructured(t, clusterrole1y)
   385  	clusterrolebinding1 := ktestutil.YamlToUnstructured(t, clusterrolebinding1y)
   386  
   387  	joinedPaths := make([]interface{}, 0)
   388  	err := yaml.Unmarshal([]byte(joinedPathsYaml), &joinedPaths)
   389  	if err != nil {
   390  		t.Fatalf("error parsing yaml: %v", err)
   391  	}
   392  
   393  	tests := map[string]struct {
   394  		target   *unstructured.Unstructured
   395  		sources  []*unstructured.Unstructured
   396  		cache    cache.ResourceCache
   397  		mutated  bool
   398  		reason   string
   399  		errMsg   string
   400  		expected []nestedFieldValue
   401  	}{
   402  		"no annotation": {
   403  			target:  configmap1,
   404  			mutated: false,
   405  			reason:  "",
   406  		},
   407  		"invalid annotation": {
   408  			target:  configmap3,
   409  			mutated: false,
   410  			reason:  "",
   411  			// exact error message isn't very important. Feel free to update if the error text changes.
   412  			errMsg: `failed to read annotation in object (v1/namespaces/map-namespace/ConfigMap/map3-name): ` +
   413  				`invalid "config.kubernetes.io/apply-time-mutation" annotation: ` +
   414  				`error unmarshaling JSON: ` +
   415  				`while decoding JSON: ` +
   416  				`json: cannot unmarshal string into Go value of type mutation.ApplyTimeMutation`,
   417  		},
   418  		"invalid self-reference": {
   419  			target:  configmap4,
   420  			mutated: false,
   421  			reason:  "",
   422  			// exact error message isn't very important. Feel free to update if the error text changes.
   423  			errMsg: `invalid self-reference (/namespaces/map-namespace/ConfigMap/map4-name)`,
   424  		},
   425  		"missing source": {
   426  			target:  pod1,
   427  			mutated: false,
   428  			reason:  "",
   429  			// exact error message isn't very important. Feel free to update if the error text changes.
   430  			errMsg: `failed to get source object (networking.k8s.io/namespaces/ingress-namespace/Ingress/ingress1-name): ` +
   431  				`object not found: ` +
   432  				`ingresses.networking.k8s.io "ingress1-name" not found`,
   433  		},
   434  		"pod env var string from ingress port int": {
   435  			target:  pod1,
   436  			sources: []*unstructured.Unstructured{ingress1},
   437  			mutated: true,
   438  			reason:  expectedReason,
   439  			expected: []nestedFieldValue{
   440  				{
   441  					Field: []interface{}{"spec", "containers", 0, "env", 0, "value"},
   442  					Value: "80", // must be string, not int
   443  				},
   444  			},
   445  		},
   446  		"two subs, one source, no token, missing target field, field selector": {
   447  			target:  pod2,
   448  			sources: []*unstructured.Unstructured{ingress1, ingress1}, // twice, because not cached
   449  			mutated: true,
   450  			reason:  expectedReason,
   451  			expected: []nestedFieldValue{
   452  				{
   453  					Field: []interface{}{"spec", "containers", 0, "env", 0, "value"},
   454  					Value: "80", // must be string, not int
   455  				},
   456  				{
   457  					Field: []interface{}{"spec", "containers", 0, "env", 1, "value"},
   458  					Value: "old",
   459  				},
   460  			},
   461  		},
   462  		"two subs, one source, no token, missing target field, field selector (cached)": {
   463  			target:  pod2,
   464  			sources: []*unstructured.Unstructured{ingress1}, // only once, because cached
   465  			cache:   cache.NewResourceCacheMap(),
   466  			mutated: true,
   467  			reason:  expectedReason,
   468  			expected: []nestedFieldValue{
   469  				{
   470  					Field: []interface{}{"spec", "containers", 0, "env", 0, "value"},
   471  					Value: "80", // must be string, not int
   472  				},
   473  				{
   474  					Field: []interface{}{"spec", "containers", 0, "env", 1, "value"},
   475  					Value: "old",
   476  				},
   477  			},
   478  		},
   479  		"three subs, two sources, two tokens in the same target field, float string": {
   480  			target:  pod3,
   481  			sources: []*unstructured.Unstructured{configmap1, configmap1, ingress1}, // repeats, because not cached
   482  			mutated: true,
   483  			reason:  expectedReason,
   484  			expected: []nestedFieldValue{
   485  				{
   486  					Field: []interface{}{"spec", "containers", 0, "env", 0, "value"},
   487  					Value: "80", // must be string, not int
   488  				},
   489  				{
   490  					Field: []interface{}{"spec", "containers", 0, "image"},
   491  					Value: "traefik/whoami:1.0", // make sure float string isn't trucated to "1"
   492  				},
   493  			},
   494  		},
   495  		"three subs, two sources, two tokens in the same target field, float string (cached)": {
   496  			target:  pod3,
   497  			sources: []*unstructured.Unstructured{configmap1, ingress1}, // no repeats, because cached
   498  			cache:   cache.NewResourceCacheMap(),
   499  			mutated: true,
   500  			reason:  expectedReason,
   501  			expected: []nestedFieldValue{
   502  				{
   503  					Field: []interface{}{"spec", "containers", 0, "env", 0, "value"},
   504  					Value: "80", // must be string, not int
   505  				},
   506  				{
   507  					Field: []interface{}{"spec", "containers", 0, "image"},
   508  					Value: "traefik/whoami:1.0", // make sure float string isn't trucated to "1"
   509  				},
   510  			},
   511  		},
   512  		"map to json string": {
   513  			target:  configmap2,
   514  			sources: []*unstructured.Unstructured{configmap1},
   515  			mutated: true,
   516  			reason:  expectedReason,
   517  			expected: []nestedFieldValue{
   518  				{
   519  					Field: []interface{}{"data", "json"},
   520  					Value: `[{"π":3.14},{"image":"traefik/whoami","version":"1.0"}]`, // string, not object
   521  				},
   522  			},
   523  		},
   524  		"map to map, array append": {
   525  			target:  ingress2,
   526  			sources: []*unstructured.Unstructured{ingress1},
   527  			mutated: true,
   528  			reason:  expectedReason,
   529  			expected: []nestedFieldValue{
   530  				{
   531  					Field: []interface{}{"spec", "rules", 0, "http", "paths"},
   532  					Value: joinedPaths, // object, not string
   533  				},
   534  			},
   535  		},
   536  		"multi-field selector": {
   537  			target:  service1,
   538  			sources: []*unstructured.Unstructured{deployment1, deployment1}, // repeats, because not cached
   539  			mutated: true,
   540  			reason:  expectedReason,
   541  			expected: []nestedFieldValue{
   542  				{
   543  					Field: []interface{}{"spec", "ports", 0, "targetPort"},
   544  					Value: 8080,
   545  				},
   546  				{
   547  					Field: []interface{}{"spec", "ports", 2, "targetPort"},
   548  					Value: 8081,
   549  				},
   550  			},
   551  		},
   552  		"cluster-scoped": {
   553  			target:  clusterrolebinding1,
   554  			sources: []*unstructured.Unstructured{clusterrole1},
   555  			mutated: true,
   556  			reason:  expectedReason,
   557  			expected: []nestedFieldValue{
   558  				{
   559  					Field: []interface{}{"subjects", 0, "name"},
   560  					Value: "bob@example.com",
   561  				},
   562  			},
   563  		},
   564  	}
   565  
   566  	for name, tc := range tests {
   567  		t.Run(name, func(t *testing.T) {
   568  			getChan := make(chan unstructured.Unstructured)
   569  
   570  			mutator := &ApplyTimeMutator{
   571  				Client: &fakeDynamicClient{
   572  					resourceInterfaceFunc: newFakeNamespaceClientFunc(getChan),
   573  				},
   574  				Mapper: testrestmapper.TestOnlyStaticRESTMapper(
   575  					scheme.Scheme,
   576  					scheme.Scheme.PrioritizedVersionsAllGroups()...,
   577  				),
   578  				ResourceCache: tc.cache, // optional!
   579  			}
   580  
   581  			// send sources when GET is called
   582  			sources := tc.sources
   583  			go func() {
   584  				defer close(getChan)
   585  				for _, source := range sources {
   586  					getChan <- *source
   587  				}
   588  			}()
   589  
   590  			mutated, reason, err := mutator.Mutate(context.TODO(), tc.target)
   591  			if tc.errMsg != "" {
   592  				require.EqualError(t, err, tc.errMsg)
   593  			} else {
   594  				require.NoError(t, err)
   595  			}
   596  			require.Equal(t, tc.mutated, mutated, "unexpected mutated bool")
   597  			require.Equal(t, tc.reason, reason, "unexpected mutated reason")
   598  
   599  			for _, efv := range tc.expected {
   600  				received, found, err := object.NestedField(tc.target.Object, efv.Field...)
   601  				require.NoError(t, err)
   602  				require.True(t, found, "target field not found")
   603  				require.Equal(t, efv.Value, received, "unexpected target field value")
   604  			}
   605  		})
   606  	}
   607  }
   608  
   609  func TestValueToString(t *testing.T) {
   610  	tests := map[string]struct {
   611  		value    interface{}
   612  		expected string
   613  	}{
   614  		"int": {
   615  			value:    1,
   616  			expected: "1",
   617  		},
   618  		"float": {
   619  			value:    1.2345,
   620  			expected: "1.2345",
   621  		},
   622  		"string": {
   623  			value:    "nothing to see",
   624  			expected: "nothing to see",
   625  		},
   626  		"bool": {
   627  			value:    false,
   628  			expected: "false",
   629  		},
   630  		"interface map": {
   631  			value: map[string]interface{}{
   632  				"apiVersion": "v1",
   633  				"kind":       "Pod",
   634  				"metadata": map[string]interface{}{
   635  					"name":      "pod-name",
   636  					"namespace": "test-namespace",
   637  				},
   638  			},
   639  			expected: `{"apiVersion":"v1","kind":"Pod","metadata":{"name":"pod-name","namespace":"test-namespace"}}`,
   640  		},
   641  		"interface list": {
   642  			value: []interface{}{
   643  				"x",
   644  				map[string]interface{}{
   645  					"?": nil,
   646  				},
   647  				0,
   648  			},
   649  			expected: `["x",{"?":null},0]`,
   650  		},
   651  		"string list": {
   652  			value: []string{
   653  				"x",
   654  				"y",
   655  				"z",
   656  			},
   657  			expected: `["x","y","z"]`,
   658  		},
   659  	}
   660  
   661  	for name, tc := range tests {
   662  		t.Run(name, func(t *testing.T) {
   663  			received, err := valueToString(tc.value)
   664  			require.NoError(t, err)
   665  			require.Equal(t, tc.expected, received, "unexpected result")
   666  		})
   667  	}
   668  }
   669  
   670  // fakeNamespaceClient wraps ResourceInterface, overwriting the Get func.
   671  type fakeNamespaceClient struct {
   672  	dynamic.ResourceInterface
   673  	resource  schema.GroupVersionResource
   674  	namespace string
   675  	getChan   <-chan unstructured.Unstructured
   676  }
   677  
   678  func newFakeNamespaceClientFunc(getChan <-chan unstructured.Unstructured) func(resource schema.GroupVersionResource, namespace string) dynamic.ResourceInterface {
   679  	innerGetChan := getChan
   680  	return func(resource schema.GroupVersionResource, namespace string) dynamic.ResourceInterface {
   681  		return &fakeNamespaceClient{
   682  			resource:  resource,
   683  			namespace: namespace,
   684  			getChan:   innerGetChan,
   685  		}
   686  	}
   687  }
   688  
   689  func (c *fakeNamespaceClient) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
   690  	obj, open := <-c.getChan
   691  	if !open {
   692  		return nil, apierrors.NewNotFound(c.resource.GroupResource(), name)
   693  	}
   694  	return &obj, nil
   695  }
   696  
   697  // fakeDynamicClient accepts always returns the same client, just with a different
   698  type fakeDynamicClient struct {
   699  	resourceInterfaceFunc func(resource schema.GroupVersionResource, namespace string) dynamic.ResourceInterface
   700  }
   701  
   702  func (c *fakeDynamicClient) Resource(resource schema.GroupVersionResource) dynamic.NamespaceableResourceInterface {
   703  	return &fakeDynamicResourceClient{
   704  		resourceInterfaceFunc:          c.resourceInterfaceFunc,
   705  		NamespaceableResourceInterface: fake.NewSimpleDynamicClient(scheme.Scheme).Resource(resource),
   706  		resource:                       resource,
   707  	}
   708  }
   709  
   710  type fakeDynamicResourceClient struct {
   711  	dynamic.NamespaceableResourceInterface
   712  	resourceInterfaceFunc func(resource schema.GroupVersionResource, namespace string) dynamic.ResourceInterface
   713  	resource              schema.GroupVersionResource
   714  }
   715  
   716  func (c *fakeDynamicResourceClient) Namespace(ns string) dynamic.ResourceInterface {
   717  	return c.resourceInterfaceFunc(c.resource, ns)
   718  }
   719  

View as plain text