...

Source file src/sigs.k8s.io/kustomize/kyaml/fn/framework/selector_test.go

Documentation: sigs.k8s.io/kustomize/kyaml/fn/framework

     1  // Copyright 2021 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package framework_test
     5  
     6  import (
     7  	"bytes"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  	"sigs.k8s.io/kustomize/kyaml/yaml"
    14  
    15  	"sigs.k8s.io/kustomize/kyaml/fn/framework"
    16  	"sigs.k8s.io/kustomize/kyaml/kio"
    17  )
    18  
    19  func TestSelector(t *testing.T) {
    20  	type Test struct {
    21  		// Name is the name of the test
    22  		Name string
    23  
    24  		// Filter configures the selector
    25  		Fn func(*framework.Selector)
    26  
    27  		// ValueFoo is the value to substitute to select the foo resource
    28  		ValueFoo string
    29  
    30  		// ValueBar is the value to substitute to select the bar resource
    31  		ValueBar string
    32  
    33  		// Value is set by the test to either ValueFoo or ValueBar
    34  		// and substituted into the selector
    35  		Value string
    36  	}
    37  	tests := []Test{
    38  		// Test the name template
    39  		{
    40  			Name: "names",
    41  			Fn: func(s *framework.Selector) {
    42  				s.Names = []string{"{{ .Value }}"}
    43  			},
    44  			ValueFoo: "foo",
    45  			ValueBar: "bar",
    46  		},
    47  
    48  		// Test the kind template
    49  		{
    50  			Name: "kinds",
    51  			Fn: func(s *framework.Selector) {
    52  				s.Kinds = []string{"{{ .Value }}"}
    53  			},
    54  			ValueFoo: "StatefulSet",
    55  			ValueBar: "Deployment",
    56  		},
    57  
    58  		// Test the apiVersion template
    59  		{
    60  			Name: "apiVersion",
    61  			Fn: func(s *framework.Selector) {
    62  				s.APIVersions = []string{"{{ .Value }}"}
    63  			},
    64  			ValueFoo: "apps/v1beta1",
    65  			ValueBar: "apps/v1",
    66  		},
    67  
    68  		// Test the namespace template
    69  		{
    70  			Name: "namespaces",
    71  			Fn: func(s *framework.Selector) {
    72  				s.Namespaces = []string{"{{ .Value }}"}
    73  			},
    74  			ValueFoo: "foo-default",
    75  			ValueBar: "bar-default",
    76  		},
    77  
    78  		// Test the annotations template
    79  		{
    80  			Name: "annotations",
    81  			Fn: func(s *framework.Selector) {
    82  				s.Annotations = map[string]string{"key": "{{ .Value }}"}
    83  			},
    84  			ValueFoo: "foo-a",
    85  			ValueBar: "bar-a",
    86  		},
    87  
    88  		// Test the labels template
    89  		{
    90  			Name: "labels",
    91  			Fn: func(s *framework.Selector) {
    92  				s.Labels = map[string]string{"key": "{{ .Value }}"}
    93  			},
    94  			ValueFoo: "foo-l",
    95  			ValueBar: "bar-l",
    96  		},
    97  	}
    98  
    99  	// input is the input resources that are selected
   100  	input := `
   101  apiVersion: apps/v1beta1
   102  kind: StatefulSet
   103  metadata:
   104   name: foo
   105   namespace: foo-default
   106   annotations:
   107     key: foo-a
   108   labels:
   109     key: foo-l
   110  ---
   111  apiVersion: apps/v1
   112  kind: Deployment
   113  metadata:
   114   name: bar
   115   namespace: bar-default
   116   annotations:
   117     key: bar-a
   118   labels:
   119     key: bar-l
   120  `
   121  	// expectedFoo is the expected output when the FooValue is substituted
   122  	expectedFoo := `
   123  apiVersion: apps/v1beta1
   124  kind: StatefulSet
   125  metadata:
   126    name: foo
   127    namespace: foo-default
   128    annotations:
   129      key: foo-a
   130      config.kubernetes.io/index: '0'
   131      internal.config.kubernetes.io/index: '0'
   132    labels:
   133      key: foo-l
   134  `
   135  	// expectedFoo is the expected output when the BarValue is substituted
   136  	expectedBar := `
   137  apiVersion: apps/v1
   138  kind: Deployment
   139  metadata:
   140    name: bar
   141    namespace: bar-default
   142    annotations:
   143      key: bar-a
   144      config.kubernetes.io/index: '1'
   145      internal.config.kubernetes.io/index: '1'
   146    labels:
   147      key: bar-l
   148  `
   149  
   150  	// Run the tests by substituting the FooValues
   151  	var err error
   152  	for i := range tests {
   153  		test := tests[i]
   154  		t.Run(tests[i].Name+"-foo", func(t *testing.T) {
   155  			test.Value = test.ValueFoo
   156  			var out bytes.Buffer
   157  			rw := &kio.ByteReadWriter{
   158  				Reader:                bytes.NewBufferString(input),
   159  				Writer:                &out,
   160  				KeepReaderAnnotations: true,
   161  			}
   162  			p := func(rl *framework.ResourceList) error {
   163  				s := &framework.Selector{TemplateData: test}
   164  				test.Fn(s)
   165  				rl.Items, err = s.Filter(rl.Items)
   166  				return err
   167  			}
   168  
   169  			require.NoError(t, framework.Execute(framework.ResourceListProcessorFunc(p), rw))
   170  			require.Equal(t, strings.TrimSpace(expectedFoo), strings.TrimSpace(out.String()))
   171  		})
   172  	}
   173  
   174  	// Run the tests by substituting the BarValues
   175  	for i := range tests {
   176  		test := tests[i]
   177  		t.Run(tests[i].Name+"-bar", func(t *testing.T) {
   178  			test.Value = test.ValueBar
   179  			var out bytes.Buffer
   180  			rw := &kio.ByteReadWriter{
   181  				Reader:                bytes.NewBufferString(input),
   182  				Writer:                &out,
   183  				KeepReaderAnnotations: true,
   184  			}
   185  
   186  			p := func(rl *framework.ResourceList) error {
   187  				s := &framework.Selector{TemplateData: test}
   188  				test.Fn(s)
   189  				rl.Items, err = s.Filter(rl.Items)
   190  				return err
   191  			}
   192  			require.NoError(t, framework.Execute(framework.ResourceListProcessorFunc(p), rw))
   193  			require.Equal(t, strings.TrimSpace(expectedBar), strings.TrimSpace(out.String()))
   194  		})
   195  	}
   196  }
   197  
   198  func TestAndOrSelector_Composition(t *testing.T) {
   199  	// This selector should pick the "prime-target" deployment by name
   200  	// as well as any resources with the given labels or annotations regardless of kind
   201  	s := framework.MatchAny(
   202  		framework.MatchAll(
   203  			framework.GVKMatcher("apps/v1/Deployment"),
   204  			framework.NameMatcher("prime-target"),
   205  		),
   206  		framework.MatchAny(
   207  			framework.LabelMatcher(map[string]string{
   208  				"select": "yes",
   209  			}),
   210  			framework.AnnotationMatcher(map[string]string{
   211  				"example.io/select": "yes",
   212  			}),
   213  		),
   214  	)
   215  
   216  	input, err := kio.FromBytes([]byte(`
   217  apiVersion: apps/v1
   218  kind: Deployment
   219  metadata:
   220    name: prime-target
   221  ---
   222  apiVersion: apps/v1
   223  kind: Deployment
   224  metadata:
   225    name: exclude-one
   226  ---
   227  apiVersion: apps/v1
   228  kind: Deployment
   229  metadata:
   230    name: exclude-two
   231    labels:
   232      select: no
   233  ---
   234  apiVersion: v1
   235  kind: ConfigMap
   236  metadata:
   237    name: extra-target
   238    labels:
   239      select: yes
   240  ---
   241  apiVersion: apps/v1
   242  kind: ConfigMap
   243  metadata:
   244    name: prime-target
   245  data:
   246    shouldSelect: false
   247  ---
   248  apiVersion: apps/v1
   249  kind: Deployment
   250  metadata:
   251    name: extra-target-one
   252    labels:
   253      select: yes
   254  ---
   255  apiVersion: apps/v1
   256  kind: Deployment
   257  metadata:
   258    name: extra-target-two
   259    annotations:
   260      example.io/select: yes
   261  `))
   262  	require.NoError(t, err)
   263  	result, err := s.Filter(input)
   264  	require.NoError(t, err)
   265  
   266  	expected := `
   267  apiVersion: apps/v1
   268  kind: Deployment
   269  metadata:
   270    name: prime-target
   271  ---
   272  apiVersion: v1
   273  kind: ConfigMap
   274  metadata:
   275    name: extra-target
   276    labels:
   277      select: yes
   278  ---
   279  apiVersion: apps/v1
   280  kind: Deployment
   281  metadata:
   282    name: extra-target-one
   283    labels:
   284      select: yes
   285  ---
   286  apiVersion: apps/v1
   287  kind: Deployment
   288  metadata:
   289    name: extra-target-two
   290    annotations:
   291      example.io/select: yes
   292  `
   293  	resultStr, err := kio.StringAll(result)
   294  	require.NoError(t, err)
   295  	assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(resultStr))
   296  }
   297  
   298  func TestAndOrSelector_CompositionTemplated(t *testing.T) {
   299  	// This selector should pick the "prime-target" deployment by name
   300  	// as well as any resources with the given labels or annotations regardless of kind
   301  	// Note: very similar to above test, but uses verbose expression to access templating
   302  	type templateStruct struct {
   303  		GVK             string
   304  		Name            string
   305  		LabelValue      string
   306  		AnnotationValue string
   307  	}
   308  
   309  	s := framework.OrSelector{
   310  		// This should get propagated to matchers without explicit data
   311  		TemplateData: &templateStruct{
   312  			GVK:             "apps/v1/Oops",
   313  			Name:            "extra-target",
   314  			LabelValue:      "yes",
   315  			AnnotationValue: "yes",
   316  		},
   317  		Matchers: []framework.ResourceMatcher{
   318  			&framework.AndSelector{
   319  				TemplateData: &templateStruct{
   320  					GVK:  "apps/v1/Deployment",
   321  					Name: "prime-target",
   322  				},
   323  				Matchers: []framework.ResourceMatcher{
   324  					framework.GVKMatcher("{{.GVK}}"),
   325  					framework.NameMatcher("{{.Name}}"),
   326  				},
   327  			},
   328  			&framework.OrSelector{
   329  				Matchers: []framework.ResourceMatcher{
   330  					framework.LabelMatcher(map[string]string{
   331  						"select": "{{.LabelValue}}",
   332  					}),
   333  					framework.AnnotationMatcher(map[string]string{
   334  						"example.io/select": "{{.AnnotationValue}}",
   335  					}),
   336  				},
   337  			},
   338  		},
   339  	}
   340  
   341  	input, err := kio.FromBytes([]byte(`
   342  apiVersion: apps/v1
   343  kind: Deployment
   344  metadata:
   345    name: prime-target
   346  ---
   347  apiVersion: apps/v1
   348  kind: Deployment
   349  metadata:
   350    name: exclude-one
   351  ---
   352  apiVersion: apps/v1
   353  kind: Deployment
   354  metadata:
   355    name: exclude-two
   356    labels:
   357      select: no
   358  ---
   359  apiVersion: v1
   360  kind: ConfigMap
   361  metadata:
   362    name: extra-target
   363    labels:
   364      select: yes
   365  ---
   366  apiVersion: apps/v1
   367  kind: ConfigMap
   368  metadata:
   369    name: prime-target
   370  data:
   371    shouldSelect: false
   372  ---
   373  apiVersion: apps/v1
   374  kind: Deployment
   375  metadata:
   376    name: extra-target-one
   377    labels:
   378      select: yes
   379  ---
   380  apiVersion: apps/v1
   381  kind: Deployment
   382  metadata:
   383    name: extra-target-two
   384    annotations:
   385      example.io/select: yes
   386  `))
   387  	require.NoError(t, err)
   388  	result, err := s.Filter(input)
   389  	require.NoError(t, err)
   390  
   391  	expected := `
   392  apiVersion: apps/v1
   393  kind: Deployment
   394  metadata:
   395    name: prime-target
   396  ---
   397  apiVersion: v1
   398  kind: ConfigMap
   399  metadata:
   400    name: extra-target
   401    labels:
   402      select: yes
   403  ---
   404  apiVersion: apps/v1
   405  kind: Deployment
   406  metadata:
   407    name: extra-target-one
   408    labels:
   409      select: yes
   410  ---
   411  apiVersion: apps/v1
   412  kind: Deployment
   413  metadata:
   414    name: extra-target-two
   415    annotations:
   416      example.io/select: yes
   417  `
   418  	resultStr, err := kio.StringAll(result)
   419  	require.NoError(t, err)
   420  	assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(resultStr))
   421  }
   422  
   423  func TestMatchersAsFilters(t *testing.T) {
   424  	input, err := kio.FromBytes([]byte(`
   425  apiVersion: apps/v1
   426  kind: Deployment
   427  metadata:
   428    name: target
   429    labels:
   430      select: me
   431  ---
   432  apiVersion: extensions/v1beta2
   433  kind: Deployment
   434  metadata:
   435    name: exclude
   436    labels:
   437      select: no
   438  `))
   439  	require.NoError(t, err)
   440  
   441  	expected := `
   442  apiVersion: apps/v1
   443  kind: Deployment
   444  metadata:
   445    name: target
   446    labels:
   447      select: me
   448  `
   449  	matchers := map[string]framework.ResourceMatcher{
   450  		"slice": framework.NameMatcher("target"),
   451  		"map":   framework.LabelMatcher(map[string]string{"select": "me"}),
   452  		"func": framework.ResourceMatcherFunc(func(node *yaml.RNode) bool {
   453  			v := node.Field("apiVersion").Value
   454  			return strings.TrimSpace(v.MustString()) == "apps/v1"
   455  		}),
   456  	}
   457  	for desc, m := range matchers {
   458  		matcher := m
   459  		t.Run(desc, func(t *testing.T) {
   460  			result, err := matcher.Filter(input)
   461  			require.NoError(t, err)
   462  			resultStr, err := kio.StringAll(result)
   463  			require.NoError(t, err)
   464  			assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(resultStr))
   465  		})
   466  	}
   467  }
   468  

View as plain text