sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/cluster/template_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 cluster
    18  
    19  import (
    20  	"context"
    21  	"encoding/base64"
    22  	"fmt"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"net/url"
    26  	"os"
    27  	"path/filepath"
    28  	"testing"
    29  
    30  	"github.com/google/go-github/v53/github"
    31  	. "github.com/onsi/gomega"
    32  	corev1 "k8s.io/api/core/v1"
    33  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    34  
    35  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/config"
    36  	"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
    37  	yaml "sigs.k8s.io/cluster-api/cmd/clusterctl/client/yamlprocessor"
    38  	"sigs.k8s.io/cluster-api/cmd/clusterctl/internal/test"
    39  )
    40  
    41  var template = `apiVersion: cluster.x-k8s.io/v1beta1
    42  kind: Cluster
    43  ---
    44  apiVersion: cluster.x-k8s.io/v1beta1
    45  kind: Machine`
    46  
    47  func Test_templateClient_GetFromConfigMap(t *testing.T) {
    48  	g := NewWithT(t)
    49  
    50  	configClient, err := config.New(context.Background(), "", config.InjectReader(test.NewFakeReader()))
    51  	g.Expect(err).ToNot(HaveOccurred())
    52  
    53  	configMap := &corev1.ConfigMap{
    54  		TypeMeta: metav1.TypeMeta{
    55  			Kind:       "ConfigMap",
    56  			APIVersion: "v1",
    57  		},
    58  		ObjectMeta: metav1.ObjectMeta{
    59  			Namespace: "ns1",
    60  			Name:      "my-template",
    61  		},
    62  		Data: map[string]string{
    63  			"prod": template,
    64  		},
    65  	}
    66  
    67  	type fields struct {
    68  		proxy        Proxy
    69  		configClient config.Client
    70  	}
    71  	type args struct {
    72  		configMapNamespace  string
    73  		configMapName       string
    74  		configMapDataKey    string
    75  		targetNamespace     string
    76  		skipTemplateProcess bool
    77  	}
    78  	tests := []struct {
    79  		name    string
    80  		fields  fields
    81  		args    args
    82  		want    string
    83  		wantErr bool
    84  	}{
    85  		{
    86  			name: "Return template",
    87  			fields: fields{
    88  				proxy:        test.NewFakeProxy().WithObjs(configMap),
    89  				configClient: configClient,
    90  			},
    91  			args: args{
    92  				configMapNamespace:  "ns1",
    93  				configMapName:       "my-template",
    94  				configMapDataKey:    "prod",
    95  				targetNamespace:     "",
    96  				skipTemplateProcess: false,
    97  			},
    98  			want:    template,
    99  			wantErr: false,
   100  		},
   101  		{
   102  			name: "Config map does not exists",
   103  			fields: fields{
   104  				proxy:        test.NewFakeProxy().WithObjs(configMap),
   105  				configClient: configClient,
   106  			},
   107  			args: args{
   108  				configMapNamespace:  "ns1",
   109  				configMapName:       "something-else",
   110  				configMapDataKey:    "prod",
   111  				targetNamespace:     "",
   112  				skipTemplateProcess: false,
   113  			},
   114  			want:    "",
   115  			wantErr: true,
   116  		},
   117  		{
   118  			name: "Config map key does not exists",
   119  			fields: fields{
   120  				proxy:        test.NewFakeProxy().WithObjs(configMap),
   121  				configClient: configClient,
   122  			},
   123  			args: args{
   124  				configMapNamespace:  "ns1",
   125  				configMapName:       "my-template",
   126  				configMapDataKey:    "something-else",
   127  				targetNamespace:     "",
   128  				skipTemplateProcess: false,
   129  			},
   130  			want:    "",
   131  			wantErr: true,
   132  		},
   133  	}
   134  	for _, tt := range tests {
   135  		t.Run(tt.name, func(t *testing.T) {
   136  			g := NewWithT(t)
   137  
   138  			ctx := context.Background()
   139  
   140  			processor := yaml.NewSimpleProcessor()
   141  			tc := newTemplateClient(TemplateClientInput{tt.fields.proxy, tt.fields.configClient, processor})
   142  			got, err := tc.GetFromConfigMap(ctx, tt.args.configMapNamespace, tt.args.configMapName, tt.args.configMapDataKey, tt.args.targetNamespace, tt.args.skipTemplateProcess)
   143  			if tt.wantErr {
   144  				g.Expect(err).To(HaveOccurred())
   145  				return
   146  			}
   147  			g.Expect(err).ToNot(HaveOccurred())
   148  
   149  			wantTemplate, err := repository.NewTemplate(repository.TemplateInput{
   150  				RawArtifact:           []byte(tt.want),
   151  				ConfigVariablesClient: configClient.Variables(),
   152  				Processor:             processor,
   153  				TargetNamespace:       tt.args.targetNamespace,
   154  				SkipTemplateProcess:   tt.args.skipTemplateProcess,
   155  			})
   156  			g.Expect(err).ToNot(HaveOccurred())
   157  			g.Expect(got).To(Equal(wantTemplate))
   158  		})
   159  	}
   160  }
   161  
   162  func Test_templateClient_getGitHubFileContent(t *testing.T) {
   163  	g := NewWithT(t)
   164  
   165  	client, mux, teardown := test.NewFakeGitHub()
   166  	defer teardown()
   167  
   168  	configClient, err := config.New(context.Background(), "", config.InjectReader(test.NewFakeReader()))
   169  	g.Expect(err).ToNot(HaveOccurred())
   170  
   171  	mux.HandleFunc("/repos/kubernetes-sigs/cluster-api/contents/config/default/cluster-template.yaml", func(w http.ResponseWriter, _ *http.Request) {
   172  		fmt.Fprint(w, `{
   173  		  "type": "file",
   174  		  "encoding": "base64",
   175  		  "content": "`+base64.StdEncoding.EncodeToString([]byte(template))+`",
   176  		  "sha": "f5f369044773ff9c6383c087466d12adb6fa0828",
   177  		  "size": 12,
   178  		  "name": "cluster-template.yaml",
   179  		  "path": "config/default/cluster-template.yaml"
   180  		}`)
   181  	})
   182  
   183  	type args struct {
   184  		rURL *url.URL
   185  	}
   186  	tests := []struct {
   187  		name    string
   188  		args    args
   189  		want    []byte
   190  		wantErr bool
   191  	}{
   192  		{
   193  			name: "Return custom template",
   194  			args: args{
   195  				rURL: mustParseURL("https://github.com/kubernetes-sigs/cluster-api/blob/main/config/default/cluster-template.yaml"),
   196  			},
   197  			want:    []byte(template),
   198  			wantErr: false,
   199  		},
   200  		{
   201  			name: "Wrong url",
   202  			args: args{
   203  				rURL: mustParseURL("https://github.com/kubernetes-sigs/cluster-api/blob/main/config/default/something-else.yaml"),
   204  			},
   205  			want:    nil,
   206  			wantErr: true,
   207  		},
   208  	}
   209  	for _, tt := range tests {
   210  		t.Run(tt.name, func(t *testing.T) {
   211  			g := NewWithT(t)
   212  
   213  			ctx := context.Background()
   214  
   215  			c := &templateClient{
   216  				configClient: configClient,
   217  				gitHubClientFactory: func(context.Context, config.VariablesClient) (*github.Client, error) {
   218  					return client, nil
   219  				},
   220  			}
   221  			got, err := c.getGitHubFileContent(ctx, tt.args.rURL)
   222  			if tt.wantErr {
   223  				g.Expect(err).To(HaveOccurred())
   224  				return
   225  			}
   226  
   227  			g.Expect(err).ToNot(HaveOccurred())
   228  
   229  			g.Expect(got).To(Equal(tt.want))
   230  		})
   231  	}
   232  }
   233  
   234  func Test_templateClient_getRawUrlFileContent(t *testing.T) {
   235  	fakeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
   236  		fmt.Fprint(w, template)
   237  	}))
   238  
   239  	defer fakeServer.Close()
   240  
   241  	type args struct {
   242  		rURL string
   243  	}
   244  	tests := []struct {
   245  		name    string
   246  		args    args
   247  		want    []byte
   248  		wantErr bool
   249  	}{
   250  		{
   251  			name: "Return custom template",
   252  			args: args{
   253  				rURL: fakeServer.URL,
   254  			},
   255  			want:    []byte(template),
   256  			wantErr: false,
   257  		},
   258  	}
   259  	for _, tt := range tests {
   260  		t.Run(tt.name, func(t *testing.T) {
   261  			g := NewWithT(t)
   262  
   263  			ctx := context.Background()
   264  
   265  			c := newTemplateClient(TemplateClientInput{})
   266  			got, err := c.getRawURLFileContent(ctx, tt.args.rURL)
   267  			if tt.wantErr {
   268  				g.Expect(err).To(HaveOccurred())
   269  				return
   270  			}
   271  
   272  			g.Expect(err).ToNot(HaveOccurred())
   273  
   274  			g.Expect(got).To(Equal(tt.want))
   275  		})
   276  	}
   277  }
   278  
   279  func Test_templateClient_getLocalFileContent(t *testing.T) {
   280  	g := NewWithT(t)
   281  
   282  	tmpDir, err := os.MkdirTemp("", "cc")
   283  	g.Expect(err).ToNot(HaveOccurred())
   284  	defer os.RemoveAll(tmpDir)
   285  
   286  	path := filepath.Join(tmpDir, "cluster-template.yaml")
   287  	g.Expect(os.WriteFile(path, []byte(template), 0600)).To(Succeed())
   288  
   289  	type args struct {
   290  		rURL *url.URL
   291  	}
   292  	tests := []struct {
   293  		name    string
   294  		args    args
   295  		want    []byte
   296  		wantErr bool
   297  	}{
   298  		{
   299  			name: "Return custom template",
   300  			args: args{
   301  				rURL: mustParseURL(path),
   302  			},
   303  			want:    []byte(template),
   304  			wantErr: false,
   305  		},
   306  		{
   307  			name: "Wrong path",
   308  			args: args{
   309  				rURL: mustParseURL(filepath.Join(tmpDir, "something-else.yaml")),
   310  			},
   311  			want:    nil,
   312  			wantErr: true,
   313  		},
   314  	}
   315  	for _, tt := range tests {
   316  		t.Run(tt.name, func(t *testing.T) {
   317  			g := NewWithT(t)
   318  
   319  			c := &templateClient{}
   320  			got, err := c.getLocalFileContent(tt.args.rURL)
   321  			if tt.wantErr {
   322  				g.Expect(err).To(HaveOccurred())
   323  				return
   324  			}
   325  
   326  			g.Expect(err).ToNot(HaveOccurred())
   327  
   328  			g.Expect(got).To(Equal(tt.want))
   329  		})
   330  	}
   331  }
   332  
   333  func Test_templateClient_GetFromURL(t *testing.T) {
   334  	g := NewWithT(t)
   335  
   336  	tmpDir, err := os.MkdirTemp("", "cc")
   337  	g.Expect(err).ToNot(HaveOccurred())
   338  	defer os.RemoveAll(tmpDir)
   339  
   340  	configClient, err := config.New(context.Background(), "", config.InjectReader(test.NewFakeReader()))
   341  	g.Expect(err).ToNot(HaveOccurred())
   342  
   343  	fakeGithubClient, mux, teardown := test.NewFakeGitHub()
   344  	defer teardown()
   345  
   346  	mux.HandleFunc("/repos/kubernetes-sigs/cluster-api/contents/config/default/cluster-template.yaml", func(w http.ResponseWriter, _ *http.Request) {
   347  		fmt.Fprint(w, `{
   348  		  "type": "file",
   349  		  "encoding": "base64",
   350  		  "content": "`+base64.StdEncoding.EncodeToString([]byte(template))+`",
   351  		  "sha": "f5f369044773ff9c6383c087466d12adb6fa0828",
   352  		  "size": 12,
   353  		  "name": "cluster-template.yaml",
   354  		  "path": "config/default/cluster-template.yaml"
   355  		}`)
   356  	})
   357  
   358  	mux.HandleFunc("/repos/some-owner/some-repo/releases/tags/v1.0.0", func(w http.ResponseWriter, _ *http.Request) {
   359  		fmt.Fprint(w, `{
   360  		  "tag_name": "v1.0.0",
   361  		  "name": "v1.0.0",
   362  		  "id": 12345678,
   363  		  "url": "https://api.github.com/repos/some-owner/some-repo/releases/12345678",
   364  		  "assets": [
   365  			{
   366  			  "id": 87654321,
   367  			  "name": "cluster-template.yaml"
   368  			}
   369  		  ]
   370  		}`)
   371  	})
   372  
   373  	mux.HandleFunc("/repos/some-owner/some-repo/releases/assets/87654321", func(w http.ResponseWriter, _ *http.Request) {
   374  		fmt.Fprint(w, template)
   375  	})
   376  
   377  	mux.HandleFunc("/repos/some-owner/some-repo/releases/tags/v2.0.0", func(w http.ResponseWriter, _ *http.Request) {
   378  		fmt.Fprint(w, `{
   379  		  "tag_name": "v2.0.0",
   380  		  "name": "v2.0.0",
   381  		  "id": 12345678,
   382  		  "url": "https://api.github.com/repos/some-owner/some-repo/releases/12345678",
   383  		  "assets": [
   384  			{
   385  			  "id": 22222222,
   386  			  "name": "cluster-template.yaml"
   387  			}
   388  		  ]
   389  		}`)
   390  	})
   391  
   392  	// redirect asset
   393  	mux.HandleFunc("/repos/some-owner/some-repo/releases/assets/22222222", func(w http.ResponseWriter, _ *http.Request) {
   394  		// add the "/api-v3" prefix to match the prefix of the fake github server
   395  		w.Header().Add("Location", "/api-v3/redirected/22222222")
   396  		w.WriteHeader(http.StatusFound)
   397  	})
   398  
   399  	// redirect location
   400  	mux.HandleFunc("/redirected/22222222", func(w http.ResponseWriter, _ *http.Request) {
   401  		fmt.Fprint(w, template)
   402  	})
   403  
   404  	path := filepath.Join(tmpDir, "cluster-template.yaml")
   405  	g.Expect(os.WriteFile(path, []byte(template), 0600)).To(Succeed())
   406  
   407  	// redirect stdin
   408  	saveStdin := os.Stdin
   409  	defer func() { os.Stdin = saveStdin }()
   410  	os.Stdin, err = os.Open(path) //nolint:gosec
   411  	g.Expect(err).ToNot(HaveOccurred())
   412  
   413  	type args struct {
   414  		templateURL         string
   415  		targetNamespace     string
   416  		skipTemplateProcess bool
   417  	}
   418  	tests := []struct {
   419  		name    string
   420  		args    args
   421  		want    string
   422  		wantErr bool
   423  	}{
   424  		{
   425  			name: "Get from local file system",
   426  			args: args{
   427  				templateURL:         path,
   428  				targetNamespace:     "",
   429  				skipTemplateProcess: false,
   430  			},
   431  			want:    template,
   432  			wantErr: false,
   433  		},
   434  		{
   435  			name: "Get from GitHub",
   436  			args: args{
   437  				templateURL:         "https://github.com/kubernetes-sigs/cluster-api/blob/main/config/default/cluster-template.yaml",
   438  				targetNamespace:     "",
   439  				skipTemplateProcess: false,
   440  			},
   441  			want:    template,
   442  			wantErr: false,
   443  		},
   444  		{
   445  			name: "Get asset from GitHub release",
   446  			args: args{
   447  				templateURL:         "https://github.com/some-owner/some-repo/releases/download/v1.0.0/cluster-template.yaml",
   448  				targetNamespace:     "",
   449  				skipTemplateProcess: false,
   450  			},
   451  			want:    template,
   452  			wantErr: false,
   453  		},
   454  		{
   455  			name: "Get asset from GitHub release + redirect",
   456  			args: args{
   457  				templateURL:         "https://github.com/some-owner/some-repo/releases/download/v2.0.0/cluster-template.yaml",
   458  				targetNamespace:     "",
   459  				skipTemplateProcess: false,
   460  			},
   461  			want:    template,
   462  			wantErr: false,
   463  		},
   464  		{
   465  			name: "Get asset from GitHub release with a wrong URL",
   466  			args: args{
   467  				templateURL:         "https://github.com/some-owner/some-repo/releases/wrong/v1.0.0/cluster-template.yaml",
   468  				targetNamespace:     "",
   469  				skipTemplateProcess: false,
   470  			},
   471  			want:    "",
   472  			wantErr: true,
   473  		},
   474  		{
   475  			name: "Get from stdin",
   476  			args: args{
   477  				templateURL:         "-",
   478  				targetNamespace:     "",
   479  				skipTemplateProcess: false,
   480  			},
   481  			want:    template,
   482  			wantErr: false,
   483  		},
   484  	}
   485  	for _, tt := range tests {
   486  		t.Run(tt.name, func(t *testing.T) {
   487  			g := NewWithT(t)
   488  
   489  			ctx := context.Background()
   490  
   491  			gitHubClientFactory := func(context.Context, config.VariablesClient) (*github.Client, error) {
   492  				return fakeGithubClient, nil
   493  			}
   494  			processor := yaml.NewSimpleProcessor()
   495  			c := newTemplateClient(TemplateClientInput{nil, configClient, processor})
   496  			// override the github client factory
   497  			c.gitHubClientFactory = gitHubClientFactory
   498  
   499  			got, err := c.GetFromURL(ctx, tt.args.templateURL, tt.args.targetNamespace, tt.args.skipTemplateProcess)
   500  			if tt.wantErr {
   501  				g.Expect(err).To(HaveOccurred())
   502  				return
   503  			}
   504  
   505  			g.Expect(err).ToNot(HaveOccurred())
   506  
   507  			wantTemplate, err := repository.NewTemplate(repository.TemplateInput{
   508  				RawArtifact:           []byte(tt.want),
   509  				ConfigVariablesClient: configClient.Variables(),
   510  				Processor:             processor,
   511  				TargetNamespace:       tt.args.targetNamespace,
   512  				SkipTemplateProcess:   tt.args.skipTemplateProcess,
   513  			})
   514  			g.Expect(err).ToNot(HaveOccurred())
   515  			g.Expect(got).To(Equal(wantTemplate))
   516  		})
   517  	}
   518  }
   519  
   520  func mustParseURL(rawURL string) *url.URL {
   521  	rURL, err := url.Parse(rawURL)
   522  	if err != nil {
   523  		panic(err)
   524  	}
   525  	return rURL
   526  }