sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/yamlprocessor/simple_processor_test.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes 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 yamlprocessor
    18  
    19  import (
    20  	"testing"
    21  
    22  	. "github.com/onsi/gomega"
    23  
    24  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
    25  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test"
    26  )
    27  
    28  func TestSimpleProcessor_GetTemplateName(t *testing.T) {
    29  	g := NewWithT(t)
    30  	p := NewSimpleProcessor()
    31  	g.Expect(p.GetTemplateName("some-version", "some-flavor")).To(Equal("cluster-template-some-flavor.yaml"))
    32  	g.Expect(p.GetTemplateName("", "")).To(Equal("cluster-template.yaml"))
    33  }
    34  
    35  func TestSimpleProcessor_GetVariables(t *testing.T) {
    36  	type args struct {
    37  		data string
    38  	}
    39  	tests := []struct {
    40  		name    string
    41  		args    args
    42  		want    []string
    43  		wantErr bool
    44  	}{
    45  		{
    46  			name: "variable with different spacing around the name",
    47  			args: args{
    48  				data: "yaml with ${A} ${ B} ${ C} ${ D }",
    49  			},
    50  			want: []string{"A", "B", "C", "D"},
    51  		},
    52  		{
    53  			name: "variables used in many places are grouped",
    54  			args: args{
    55  				data: "yaml with ${A } ${A} ${A}",
    56  			},
    57  			want: []string{"A"},
    58  		},
    59  		{
    60  			name: "variables in multiline texts are processed",
    61  			args: args{
    62  				data: "yaml with ${A}\n${B}\n${C}",
    63  			},
    64  			want: []string{"A", "B", "C"},
    65  		},
    66  		{
    67  			name: "variables are sorted",
    68  			args: args{
    69  				data: "yaml with ${C}\n${B}\n${A}",
    70  			},
    71  			want: []string{"A", "B", "C"},
    72  		},
    73  		{
    74  			name: "returns error for variables with regex metacharacters",
    75  			args: args{
    76  				data: "yaml with ${BA$R}\n${FOO}",
    77  			},
    78  			wantErr: true,
    79  		},
    80  		{
    81  			name: "variables with envsubst functions are properly parsed",
    82  			args: args{
    83  				data: "yaml with ${C:=default}\n${B}\n${A=foobar}",
    84  			},
    85  			want: []string{"A", "B", "C"},
    86  		},
    87  	}
    88  
    89  	for _, tt := range tests {
    90  		t.Run(tt.name, func(t *testing.T) {
    91  			g := NewWithT(t)
    92  			p := NewSimpleProcessor()
    93  			actual, err := p.GetVariables([]byte(tt.args.data))
    94  			if tt.wantErr {
    95  				g.Expect(err).To(HaveOccurred())
    96  				return
    97  			}
    98  			g.Expect(err).ToNot(HaveOccurred())
    99  			g.Expect(actual).To(Equal(tt.want))
   100  		})
   101  	}
   102  }
   103  
   104  func TestSimpleProcessor_GetVariablesMap(t *testing.T) {
   105  	type args struct {
   106  		data string
   107  	}
   108  	def := "default"
   109  	aVar := "${A}"
   110  	foobar := "foobar"
   111  	quotes := `""`
   112  	tests := []struct {
   113  		name    string
   114  		args    args
   115  		want    map[string]*string
   116  		wantErr bool
   117  	}{
   118  		{
   119  			name: "variable with different spacing around the name",
   120  			args: args{
   121  				data: "yaml with ${A} ${ B} ${ C} ${ D }",
   122  			},
   123  			want: map[string]*string{"A": nil, "B": nil, "C": nil, "D": nil},
   124  		},
   125  		{
   126  			name: "variables used in many places are grouped",
   127  			args: args{
   128  				data: "yaml with ${A } ${A} ${A}",
   129  			},
   130  			want: map[string]*string{"A": nil},
   131  		},
   132  		{
   133  			name: "variables in multiline texts are processed",
   134  			args: args{
   135  				data: "yaml with ${A}\n${B}\n${C}",
   136  			},
   137  			want: map[string]*string{"A": nil, "B": nil, "C": nil},
   138  		},
   139  		{
   140  			name: "returns error for variables with regex metacharacters",
   141  			args: args{
   142  				data: "yaml with ${BA$R}\n${FOO}",
   143  			},
   144  			wantErr: true,
   145  		},
   146  		{
   147  			name: "variables with envsubst functions are properly parsed",
   148  			args: args{
   149  				data: `yaml with ${C:=default}\n${B}\n${A=foobar}\n${E=""}\n${D:=${A}}`,
   150  			},
   151  			want: map[string]*string{"A": &foobar, "B": nil, "C": &def, "D": &aVar, "E": &quotes},
   152  		},
   153  	}
   154  
   155  	for _, tt := range tests {
   156  		t.Run(tt.name, func(t *testing.T) {
   157  			g := NewWithT(t)
   158  			p := NewSimpleProcessor()
   159  			actual, err := p.GetVariableMap([]byte(tt.args.data))
   160  			if tt.wantErr {
   161  				g.Expect(err).To(HaveOccurred())
   162  				return
   163  			}
   164  			g.Expect(err).ToNot(HaveOccurred())
   165  			g.Expect(actual).To(Equal(tt.want))
   166  		})
   167  	}
   168  }
   169  
   170  func TestSimpleProcessor_Process(t *testing.T) {
   171  	type args struct {
   172  		yaml                  []byte
   173  		configVariablesClient config.VariablesClient
   174  	}
   175  	tests := []struct {
   176  		name             string
   177  		args             args
   178  		want             []byte
   179  		wantErr          bool
   180  		missingVariables []string
   181  	}{
   182  		{
   183  			name: "replaces legacy variables names (with spaces)",
   184  			args: args{
   185  				yaml: []byte("foo ${ BAR }, ${BAR }, ${ BAR}"),
   186  				configVariablesClient: test.NewFakeVariableClient().
   187  					WithVar("BAR", "bar"),
   188  			},
   189  			want:    []byte("foo bar, bar, bar"), //nolint:dupword
   190  			wantErr: false,
   191  		},
   192  		{
   193  			name: "does not escape slashes used for windows named pipes",
   194  			args: args{
   195  				yaml: []byte(`\\ foo ${ BAR }, ${BAR }, ${ BAR}`),
   196  				configVariablesClient: test.NewFakeVariableClient().
   197  					WithVar("BAR", "bar"),
   198  			},
   199  			want:    []byte(`\\ foo bar, bar, bar`), //nolint:dupword
   200  			wantErr: false,
   201  		},
   202  		{
   203  			name: "replaces variables when variable value contains regex metacharacters",
   204  			args: args{
   205  				yaml: []byte("foo ${BAR}"),
   206  				configVariablesClient: test.NewFakeVariableClient().
   207  					WithVar("BAR", "ba$r"),
   208  			},
   209  			want:    []byte("foo ba$r"),
   210  			wantErr: false,
   211  		},
   212  		{
   213  			name: "uses default values if variable doesn't exist in variables client",
   214  			args: args{
   215  				yaml: []byte("foo ${BAR=default_bar} ${BAZ:=default_baz} ${CAR=default_car} ${CAZ:-default_caz} ${DAR=default_dar}"),
   216  				configVariablesClient: test.NewFakeVariableClient().
   217  					// CAZ,DAR is set but has no value
   218  					WithVar("BAR", "ba$r").WithVar("CAZ", "").WithVar("DAR", ""),
   219  			},
   220  			want:    []byte("foo ba$r default_baz default_car default_caz default_dar"),
   221  			wantErr: false,
   222  		},
   223  		{
   224  			name: "uses default variables if main variable is doesn't exist",
   225  			args: args{
   226  				yaml: []byte("foo ${BAR=default_bar} ${BAZ:=prefix${DEF}-suffix} ${CAZ=${DEF}}"),
   227  				configVariablesClient: test.NewFakeVariableClient().
   228  					WithVar("BAR", "ba$r").WithVar("DEF", "football"),
   229  			},
   230  			want:    []byte("foo ba$r prefixfootball-suffix football"),
   231  			wantErr: false,
   232  		},
   233  		{
   234  			name: "returns error with missing template variables listed (for better ux)",
   235  			args: args{
   236  				yaml: []byte("foo ${ BAR} ${BAZ} ${CAR}"),
   237  				configVariablesClient: test.NewFakeVariableClient().
   238  					WithVar("CAR", "car"),
   239  			},
   240  			want:             nil,
   241  			wantErr:          true,
   242  			missingVariables: []string{"BAR", "BAZ"},
   243  		},
   244  		{
   245  			name: "returns error when variable name contains regex metacharacters",
   246  			args: args{
   247  				yaml: []byte("foo ${BA$R} ${BA_R}"),
   248  				configVariablesClient: test.NewFakeVariableClient().
   249  					WithVar("BA$R", "bar").WithVar("BA_R", "ba_r"),
   250  			},
   251  			want:    []byte("foo bar ba_r"),
   252  			wantErr: true,
   253  		},
   254  	}
   255  
   256  	for _, tt := range tests {
   257  		t.Run(tt.name, func(t *testing.T) {
   258  			g := NewWithT(t)
   259  			p := NewSimpleProcessor()
   260  
   261  			got, err := p.Process(tt.args.yaml, tt.args.configVariablesClient.Get)
   262  			if tt.wantErr {
   263  				g.Expect(err).To(HaveOccurred())
   264  				if len(tt.missingVariables) != 0 {
   265  					e, ok := err.(*errMissingVariables)
   266  					g.Expect(ok).To(BeTrue())
   267  					g.Expect(e.Missing).To(ConsistOf(tt.missingVariables))
   268  				}
   269  				// we want to ensure that we keep returning the original yaml
   270  				// as per the intended behavior of Process
   271  				g.Expect(got).To(Equal(tt.args.yaml))
   272  				return
   273  			}
   274  			g.Expect(err).ToNot(HaveOccurred())
   275  
   276  			g.Expect(got).To(Equal(tt.want))
   277  		})
   278  	}
   279  }