...

Source file src/sigs.k8s.io/kustomize/api/krusty/accumulation_test.go

Documentation: sigs.k8s.io/kustomize/api/krusty

     1  // Copyright 2019 The Kubernetes Authors.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package krusty_test
     5  
     6  import (
     7  	"fmt"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strings"
    13  	"testing"
    14  
    15  	"github.com/stretchr/testify/require"
    16  	. "sigs.k8s.io/kustomize/api/internal/target"
    17  	"sigs.k8s.io/kustomize/api/konfig"
    18  	"sigs.k8s.io/kustomize/api/krusty"
    19  	kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest"
    20  )
    21  
    22  const validResource = `
    23  apiVersion: v1
    24  kind: Service
    25  metadata:
    26    name: myService
    27  spec:
    28    selector:
    29      backend: bungie
    30    ports:
    31      - port: 7002
    32  `
    33  
    34  func TestTargetMustHaveKustomizationFile(t *testing.T) {
    35  	th := kusttest_test.MakeHarness(t)
    36  	th.WriteF("service.yaml", `
    37  apiVersion: v1
    38  kind: Service
    39  metadata:
    40    name: aService
    41  `)
    42  	th.WriteF("deeper/service.yaml", `
    43  apiVersion: v1
    44  kind: Service
    45  metadata:
    46    name: anotherService
    47  `)
    48  	err := th.RunWithErr(".", th.MakeDefaultOptions())
    49  	if err == nil {
    50  		t.Fatalf("expected an error")
    51  	}
    52  	if !IsMissingKustomizationFileError(err) {
    53  		t.Fatalf("unexpected error: %q", err)
    54  	}
    55  }
    56  
    57  func TestTargetMustHaveOnlyOneKustomizationFile(t *testing.T) {
    58  	th := kusttest_test.MakeHarness(t)
    59  	for _, n := range konfig.RecognizedKustomizationFileNames() {
    60  		th.WriteF(filepath.Join(".", n), `
    61  apiVersion: kustomize.config.k8s.io/v1beta1
    62  kind: Kustomization
    63  `)
    64  	}
    65  	err := th.RunWithErr(".", th.MakeDefaultOptions())
    66  	if err == nil {
    67  		t.Fatalf("expected an error")
    68  	}
    69  	if !strings.Contains(err.Error(), "Found multiple kustomization files") {
    70  		t.Fatalf("unexpected error: %q", err)
    71  	}
    72  }
    73  
    74  func TestBaseMustHaveKustomizationFile(t *testing.T) {
    75  	th := kusttest_test.MakeHarness(t)
    76  	th.WriteK(".", `
    77  resources:
    78  - base
    79  `)
    80  	th.WriteF("base/service.yaml", validResource)
    81  	err := th.RunWithErr(".", th.MakeDefaultOptions())
    82  	if err == nil {
    83  		t.Fatalf("expected an error")
    84  	}
    85  	if !strings.Contains(err.Error(), "accumulating resources") {
    86  		t.Fatalf("unexpected error: %q", err)
    87  	}
    88  }
    89  
    90  func TestResourceNotFound(t *testing.T) {
    91  	th := kusttest_test.MakeHarness(t)
    92  	th.WriteK(".", `
    93  resources:
    94  - deployment.yaml
    95  `)
    96  	err := th.RunWithErr(".", th.MakeDefaultOptions())
    97  	if err == nil {
    98  		t.Fatalf("expected an error")
    99  	}
   100  	if !strings.Contains(err.Error(), "accumulating resources") {
   101  		t.Fatalf("unexpected error: %q", err)
   102  	}
   103  }
   104  
   105  func TestResourceHasAnchor(t *testing.T) {
   106  	th := kusttest_test.MakeHarness(t)
   107  	th.WriteK(".", `
   108  resources:
   109  - ingress.yaml
   110  `)
   111  	th.WriteF("ingress.yaml", `
   112  apiVersion: networking.k8s.io/v1
   113  kind: Ingress
   114  metadata:
   115    name: blog
   116  spec:
   117    tls:
   118    - hosts:
   119      - xyz.me
   120      - www.xyz.me
   121      secretName: cert-tls
   122    rules:
   123    - host: xyz.me
   124      http: &xxx_rules
   125        paths:
   126        - path: /
   127          pathType: Prefix
   128          backend:
   129            service:
   130              name: service
   131              port:
   132                number: 80
   133    - host: www.xyz.me
   134      http: *xxx_rules
   135  `)
   136  	m := th.Run(".", th.MakeDefaultOptions())
   137  	th.AssertActualEqualsExpected(m, `
   138  apiVersion: networking.k8s.io/v1
   139  kind: Ingress
   140  metadata:
   141    name: blog
   142  spec:
   143    rules:
   144    - host: xyz.me
   145      http:
   146        paths:
   147        - backend:
   148            service:
   149              name: service
   150              port:
   151                number: 80
   152          path: /
   153          pathType: Prefix
   154    - host: www.xyz.me
   155      http:
   156        paths:
   157        - backend:
   158            service:
   159              name: service
   160              port:
   161                number: 80
   162          path: /
   163          pathType: Prefix
   164    tls:
   165    - hosts:
   166      - xyz.me
   167      - www.xyz.me
   168      secretName: cert-tls
   169  `)
   170  }
   171  
   172  func TestAccumulateResourcesErrors(t *testing.T) {
   173  	type testcase struct {
   174  		name     string
   175  		resource string
   176  		// resourceFunc generates a resource string using the URL to the local
   177  		// test server (optional).
   178  		resourceFunc func(string) string
   179  		// resourceServerSetup configures the local test server (optional).
   180  		resourceServerSetup func(*http.ServeMux)
   181  		isAbsolute          bool
   182  		files               map[string]string
   183  		// errFile, errDir are regex for the expected error message output
   184  		// when kustomize tries to accumulate resource as file and dir,
   185  		// respectively. The test substitutes occurrences of "%s" in the
   186  		// error strings with the absolute path where kustomize looks for it.
   187  		errFile, errDir string
   188  	}
   189  	populateAbsolutePaths := func(tc testcase, dir string) testcase {
   190  		filePaths := make(map[string]string, len(tc.files)+1)
   191  		for file, content := range tc.files {
   192  			filePaths[filepath.Join(dir, file)] = content
   193  		}
   194  		resourcePath := filepath.Join(dir, tc.resource)
   195  		if tc.isAbsolute {
   196  			tc.resource = resourcePath
   197  		}
   198  		filePaths[filepath.Join(dir, "kustomization.yaml")] = fmt.Sprintf(`
   199  resources:
   200  - %s
   201  `, tc.resource)
   202  		tc.files = filePaths
   203  		regPath := regexp.QuoteMeta(resourcePath)
   204  		tc.errFile = strings.ReplaceAll(tc.errFile, "%s", regPath)
   205  		tc.errDir = strings.ReplaceAll(tc.errDir, "%s", regPath)
   206  		return tc
   207  	}
   208  	buildError := func(tc testcase) string {
   209  		const (
   210  			prefix            = "accumulating resources"
   211  			filePrefixf       = "accumulating resources from '%s'"
   212  			fileWrapperIfDirf = "accumulation err='%s'"
   213  			separator         = ": "
   214  		)
   215  		parts := []string{
   216  			prefix,
   217  			strings.Join([]string{
   218  				fmt.Sprintf(filePrefixf, regexp.QuoteMeta(tc.resource)),
   219  				tc.errFile,
   220  			}, separator),
   221  		}
   222  		if tc.errDir != "" {
   223  			parts[1] = fmt.Sprintf(fileWrapperIfDirf, parts[1])
   224  			parts = append(parts, tc.errDir)
   225  		}
   226  		return strings.Join(parts, separator)
   227  	}
   228  	for _, test := range []testcase{
   229  		{
   230  			name: "remote file not considered repo",
   231  			resourceFunc: func(url string) string {
   232  				return fmt.Sprintf("%s/segments-too-few-to-be-repo", url)
   233  			},
   234  			resourceServerSetup: func(server *http.ServeMux) {
   235  				server.HandleFunc("/", func(out http.ResponseWriter, req *http.Request) {
   236  					out.WriteHeader(http.StatusNotFound)
   237  				})
   238  			},
   239  			// It's acceptable for the error output of a remote file-like
   240  			// resource to not indicate the resource's status as a
   241  			// local directory. Though it is possible for a remote file-like
   242  			// resource to be a local directory, it is very unlikely.
   243  			errFile: `HTTP Error: status code 404 \(Not Found\)\z`,
   244  		},
   245  		{
   246  			name: "remote file qualifies as repo",
   247  			resourceFunc: func(url string) string {
   248  				return fmt.Sprintf("%s/long/enough/to/have/org/and/repo", url)
   249  			},
   250  			resourceServerSetup: func(server *http.ServeMux) {
   251  				server.HandleFunc("/", func(out http.ResponseWriter, req *http.Request) {
   252  					out.WriteHeader(http.StatusInternalServerError)
   253  				})
   254  			},
   255  			// TODO(4788): This error message is technically wrong. Just
   256  			// because we fail to GET a resource does not mean the resource is
   257  			// not a remote file. We should return the GET status code as well.
   258  			errFile: "URL is a git repository",
   259  			errDir:  `failed to run \S+/git fetch --depth=1 .+`,
   260  		},
   261  		{
   262  			name: "local file qualifies as repo",
   263  			// The .example top level domain is reserved for example purposes,
   264  			// see RFC 2606.
   265  			resource: "package@v1.28.0.example/configs/base",
   266  			errFile:  `evalsymlink failure on '%s' .+`,
   267  			errDir:   `failed to run \S+/git fetch --depth=1 .+`,
   268  		},
   269  		{
   270  			name:     "relative path does not exist",
   271  			resource: "file-or-directory",
   272  			errFile:  `evalsymlink failure on '%s' .+`,
   273  			errDir:   `must build at directory: not a valid directory: evalsymlink failure .+`,
   274  		},
   275  		{
   276  			name:       "absolute path does not exist",
   277  			resource:   "file-or-directory",
   278  			isAbsolute: true,
   279  			errFile:    `evalsymlink failure on '%s' .+`,
   280  			errDir:     `new root '%s' cannot be absolute`,
   281  		},
   282  		{
   283  			name:     "relative file violates restrictions",
   284  			resource: "../base/resource.yaml",
   285  			files: map[string]string{
   286  				"../base/resource.yaml": validResource,
   287  			},
   288  			errFile: "security; file '%s' is not in or below .+",
   289  			// TODO(4348): Over-inclusion of directory error message when we
   290  			// know resource is file.
   291  			errDir: "must build at directory: '%s': file is not directory",
   292  		},
   293  		{
   294  			name:       "absolute file violates restrictions",
   295  			resource:   "../base/resource.yaml",
   296  			isAbsolute: true,
   297  			files: map[string]string{
   298  				"../base/resource.yaml": validResource,
   299  			},
   300  			errFile: "security; file '%s' is not in or below .+",
   301  			// TODO(4348): Over-inclusion of directory error message when we
   302  			// know resource is file.
   303  			errDir: `new root '%s' cannot be absolute`,
   304  		},
   305  	} {
   306  		t.Run(test.name, func(t *testing.T) {
   307  			if test.resourceFunc != nil {
   308  				// Configure test server handler
   309  				handler := http.NewServeMux()
   310  				if test.resourceServerSetup != nil {
   311  					test.resourceServerSetup(handler)
   312  				}
   313  				// Start test server
   314  				svr := httptest.NewServer(handler)
   315  				defer svr.Close()
   316  				// Generate resource with test server address
   317  				test.resource = test.resourceFunc(svr.URL)
   318  			}
   319  
   320  			// Should use real file system to indicate that we are creating
   321  			// new temporary directories on disk when we attempt to fetch repos.
   322  			fs, tmpDir := kusttest_test.Setup(t)
   323  			root := tmpDir.Join("root")
   324  			require.NoError(t, fs.Mkdir(root))
   325  
   326  			test = populateAbsolutePaths(test, root)
   327  			for file, content := range test.files {
   328  				dir := filepath.Dir(file)
   329  				require.NoError(t, fs.MkdirAll(dir))
   330  				require.NoError(t, fs.WriteFile(file, []byte(content)))
   331  			}
   332  
   333  			b := krusty.MakeKustomizer(krusty.MakeDefaultOptions())
   334  			_, err := b.Run(fs, root)
   335  			require.Regexp(t, buildError(test), err.Error())
   336  		})
   337  	}
   338  	// TODO(annasong): add tests that check accumulateResources errors for
   339  	// - repos
   340  	// - local directories
   341  	// - files that yield malformed yaml errors
   342  }
   343  

View as plain text