...

Source file src/edge-infra.dev/pkg/edge/chariot/request_test.go

Documentation: edge-infra.dev/pkg/edge/chariot

     1  package chariot
     2  
     3  import (
     4  	"crypto/md5" //nolint:gosec // trusted input
     5  	crypto "crypto/rand"
     6  	"fmt"
     7  	"math/rand" //nolint:gosec // not a crytpo use.
     8  	"regexp"
     9  	"testing"
    10  
    11  	"gopkg.in/yaml.v2"
    12  )
    13  
    14  // randomChariotYamlObject creates an object with GVKNN data (and some labels) to represent a minimal Chariot Request.Objects member.
    15  func randomChariotYamlObject() []byte {
    16  	// Group/Version
    17  	var groupVersion = "clusterregistry.k8s.io/v1alpha1"
    18  
    19  	// Kind
    20  	var kinds = [3]string{"Cluster", "Namespace", "Tenant"}
    21  	var kind = kinds[rand.Int()%len(kinds)] //nolint:gosec // not doing cryptography
    22  
    23  	// Name
    24  	var nameBytes [16]byte
    25  	//nolint:errcheck // not doing cryptography
    26  	crypto.Read(nameBytes[:]) //nolint:errcheck
    27  	var name = fmt.Sprintf("%x", nameBytes)
    28  
    29  	// Namespace
    30  	var namespace string
    31  	if rand.Int()%2 == 0 { //nolint:gosec // not doing cryptography // half probability of no namespace
    32  		var namespaces = [5]string{"bulldozer", "ci", "godoc", "jack-bot", "policy-bot"}
    33  		namespace = namespaces[rand.Int()%len(namespaces)] //nolint:gosec // not doing cryptography
    34  	}
    35  
    36  	// Label
    37  	var labels map[string]string
    38  	if rand.Int()%2 == 0 { //nolint:gosec // not doing cryptography // half probability of no labels
    39  		var clusterNameBytes [4]byte
    40  		var clusterTypeBytes [4]byte
    41  		var fleetNameBytes [4]byte
    42  		crypto.Read(clusterNameBytes[:]) //nolint:errcheck
    43  		crypto.Read(clusterTypeBytes[:]) //nolint:errcheck
    44  		crypto.Read(fleetNameBytes[:])   //nolint:errcheck
    45  		labels = map[string]string{
    46  			"cluster.edge.ncr.com":      fmt.Sprintf("%x", clusterNameBytes),
    47  			"cluster.edge.ncr.com/type": fmt.Sprintf("%x", clusterTypeBytes),
    48  			"fleet.edge.ncr.com":        fmt.Sprintf("%x", fleetNameBytes),
    49  		}
    50  	}
    51  
    52  	// Build the Yaml object
    53  	var req struct {
    54  		APIVersion string `yaml:"apiVersion"`
    55  		Kind       string `yaml:"kind"`
    56  		Metadata   struct {
    57  			Name      string            `yaml:"name"`
    58  			Namespace string            `yaml:"namespace"`
    59  			Labels    map[string]string `yaml:"labels"`
    60  		} `yaml:"metadata"`
    61  	}
    62  	req.APIVersion = groupVersion
    63  	req.Kind = kind
    64  	req.Metadata.Name = name
    65  	req.Metadata.Namespace = namespace
    66  	req.Metadata.Labels = labels
    67  
    68  	y, _ := yaml.Marshal(req) //nolint:errcheck
    69  	return y
    70  }
    71  
    72  // TestRequestDirOption checks possible configurations of Request.Dir both good and bad.
    73  func TestRequestDirOption(t *testing.T) {
    74  	var req = &Request{
    75  		Operation: "CREATE",
    76  		Banner:    "my-banner",
    77  		Cluster:   "", // empty for banner wide objects.
    78  		Dir:       "", // Empty to check for default value.
    79  		Owner:     "test",
    80  		Objects: [][]byte{
    81  			randomChariotYamlObject(),
    82  			randomChariotYamlObject(),
    83  			randomChariotYamlObject(),
    84  			randomChariotYamlObject(),
    85  			randomChariotYamlObject(),
    86  			randomChariotYamlObject(),
    87  			randomChariotYamlObject(),
    88  			randomChariotYamlObject(),
    89  		},
    90  	}
    91  
    92  	// Testing that default value is set for Dir when empty.
    93  	_, err := req.StorageObjects()
    94  	if err != nil {
    95  		t.Fatal(err)
    96  	} else if req.Dir != defaultChariotDir {
    97  		t.Fatalf("The req.StorageObjects() function should have set the directory to 'chariot' as a default value when Dir is empty. Got %q", req.Dir)
    98  	}
    99  	req.Dir = defaultChariotDir
   100  	_, err = req.StorageObjects()
   101  	if err != nil {
   102  		t.Fatalf("The req.StorageObjects() function should accept the directory value 'chariot' and not return an error.. Got %v", err)
   103  	} else if req.Dir != defaultChariotDir {
   104  		t.Fatalf("The req.StorageObjects() function should keep the directory value 'chariot' when StorageObjects is called.. Got %v", req.Dir)
   105  	}
   106  	for dir := range whitelistedDirs {
   107  		req.Dir = dir
   108  		_, err := req.StorageObjects()
   109  		if err != nil {
   110  			t.Fatalf("The req.StorageObjects() function should accept the whitelisted directory value %q and not return an error.. Got %v", dir, err)
   111  		} else if req.Dir != dir {
   112  			t.Fatalf("The req.StorageObjects() function should keep the whitelisted directory value %q when StorageObjects is called.. Got %v", dir, req.Dir)
   113  		}
   114  	}
   115  
   116  	var badDirs = []string{"jhgkjhfgwljhfgwljhfw", "gwgwigwigwigi", "../foo", ".", "hello/../../foo/../bar/../", ".."}
   117  	for _, dir := range badDirs {
   118  		req.Dir = dir
   119  		_, err = req.StorageObjects()
   120  		if err == nil {
   121  			t.Fatalf("The req.StorageObjects() function should return an error for this invalid directory: %q", dir)
   122  		}
   123  	}
   124  	t.Logf("Successfully validated the Request.Dir optional field validation")
   125  }
   126  
   127  // TestRequestStorageObjectsFunction verifies that the StorageObjects function has not diverted from the Chariot specification.
   128  func TestRequestStorageObjectsFunction(t *testing.T) {
   129  	var req = &Request{
   130  		Operation: "CREATE",
   131  		Banner:    "my-banner",
   132  		Cluster:   "", // empty for banner wide objects.
   133  		Owner:     "test",
   134  		Objects: [][]byte{
   135  			randomChariotYamlObject(),
   136  			randomChariotYamlObject(),
   137  			randomChariotYamlObject(),
   138  			randomChariotYamlObject(),
   139  			randomChariotYamlObject(),
   140  			randomChariotYamlObject(),
   141  			randomChariotYamlObject(),
   142  			randomChariotYamlObject(),
   143  		},
   144  	}
   145  
   146  	bannerWideObjects, err := req.StorageObjects()
   147  	if err != nil {
   148  		t.Fatal(err)
   149  	}
   150  
   151  	// Set the Cluster variable for cluster-wide objects.
   152  	req.Cluster = "test"
   153  	clusterWideObjects, err := req.StorageObjects()
   154  	if err != nil {
   155  		t.Fatal(err)
   156  	}
   157  
   158  	// Ensure idempotency
   159  	t.Logf("Checking that banner-wide and cluster-wide objects are idempotent")
   160  	for i, b := range req.Objects {
   161  		var (
   162  			originalObj    = string(b)
   163  			bannerWideObj  = bannerWideObjects[i].Content
   164  			clusterWideObj = clusterWideObjects[i].Content
   165  		)
   166  		if originalObj != bannerWideObj {
   167  			t.Logf("Original Object:    %q", originalObj)
   168  			t.Logf("Banner Wide Object: %q", bannerWideObj)
   169  			t.Fatalf("Banner wide object is not idempotent")
   170  		} else if originalObj != clusterWideObj {
   171  			t.Logf("Original Object:     %q", originalObj)
   172  			t.Logf("Cluster Wide Object: %q", clusterWideObj)
   173  			t.Fatalf("Cluster wide object is not idempotent")
   174  		}
   175  	}
   176  	t.Logf("Successfully checked that banner-wide and cluster-wide objects are idempotent")
   177  
   178  	// Ensure the hash function is correct.
   179  	t.Logf("Checking that banner-wide and cluster-wide objects are hashed properly")
   180  	var reTrimHash = regexp.MustCompile(fmt.Sprintf("^(.*/%s/)|([.]yaml)$", req.Dir))
   181  	for i, b := range req.Objects {
   182  		var gvknn struct {
   183  			APIVersion string `yaml:"apiVersion"`
   184  			Kind       string `yaml:"kind"`
   185  			Metadata   struct {
   186  				Name      string `yaml:"name"`
   187  				Namespace string `yaml:"namespace"`
   188  			} `yaml:"metadata"`
   189  		}
   190  		if err := yaml.Unmarshal(b, &gvknn); err != nil {
   191  			t.Fatal(err)
   192  		}
   193  		// The unhashed string is a representation of the GVKNN data needed to construct a file name.
   194  		var unhashed = fmt.Sprintf(HashFormatGVKNN, gvknn.APIVersion, gvknn.Kind, gvknn.Metadata.Name, gvknn.Metadata.Namespace)
   195  		// expectedHash is a lowercase base16 MD5 hash.
   196  		var expectedHash = fmt.Sprintf("%x", md5.Sum([]byte(unhashed))) //nolint:gosec // annoying lint
   197  		t.Logf("Checking hash %q", expectedHash)
   198  		// Check the hash
   199  		if expectedHash != reTrimHash.ReplaceAllString(bannerWideObjects[i].Location, "") {
   200  			t.Fatalf("Expected hash %q did not match hash at location %q", expectedHash, bannerWideObjects[i].Location)
   201  		}
   202  		if expectedHash != reTrimHash.ReplaceAllString(clusterWideObjects[i].Location, "") {
   203  			t.Fatalf("Expected hash %q did not match hash at location %q", expectedHash, clusterWideObjects[i].Location)
   204  		}
   205  	}
   206  	t.Logf("Successfully checked that banner-wide and cluster-wide objects are hashed properly")
   207  
   208  	// Ensure that the paths are created properly. The hash has already been verified.
   209  	t.Logf("Checking that banner-wide and cluster-wide object locations are set properly")
   210  	const reGoodBannerWideLocFmt = "^gs://%s/%s/[a-f0-9]{32}[.]yaml$"
   211  	const reGoodClusterWideLocFmt = "^gs://%s/%s/%s/[a-f0-9]{32}[.]yaml$"
   212  	t.Logf("Checking banner-wide regex:  %q", reGoodBannerWideLocFmt)
   213  	t.Logf("Checking cluster-wide regex: %q", reGoodClusterWideLocFmt)
   214  	var reGoodBannerWideLoc = regexp.MustCompile(fmt.Sprintf(reGoodBannerWideLocFmt, req.Banner, req.Dir))
   215  	var reGoodClusterWideLoc = regexp.MustCompile(fmt.Sprintf(reGoodClusterWideLocFmt, req.Banner, req.Cluster, req.Dir))
   216  	for i := range req.Objects {
   217  		var bwo = bannerWideObjects[i].Location
   218  		var cwo = clusterWideObjects[i].Location
   219  		t.Logf("Banner-wide location:  %q", bwo)
   220  		if !reGoodBannerWideLoc.MatchString(bwo) {
   221  			t.Fatalf("The banner-wide object location does not satisfy the regex: %q", bwo)
   222  		}
   223  		t.Logf("Cluster-wide location: %q", cwo)
   224  		if !reGoodClusterWideLoc.MatchString(cwo) {
   225  			t.Fatalf("The cluster-wide object location does not satisfy the regex: %q", cwo)
   226  		}
   227  	}
   228  	t.Logf("Successfully checked that banner-wide and cluster-wide objects have valid locations")
   229  }
   230  
   231  // TestStorageObjectGetGcsBucketAndPathParsingWorks ensures the getGcsBucket and getGcsPath works.
   232  // Yes, this test is O(n^3)
   233  //
   234  // See: https://cloud.google.com/storage/docs/naming-buckets
   235  // See: https://cloud.google.com/storage/docs/naming-objects
   236  //
   237  // Bucket names can only contain lowercase letters, numeric characters, dashes (-), underscores (_), and dots (.).
   238  // Spaces are not allowed. Names containing dots require verification.
   239  // - Bucket names must start and end with a number or letter.
   240  // - Bucket names must contain 3-63 characters.
   241  // - Names containing dots can contain up to 222 characters, but each dot-separated component can be no longer than 63 characters.
   242  // - Bucket names cannot be represented as an IP address in dotted-decimal notation (for example, 192.168.5.4).
   243  // - Bucket names cannot begin with the "goog" prefix.
   244  // - Bucket names cannot contain "google" or close misspellings, such as "g00gle".
   245  //
   246  // Your object names must meet the following requirements:
   247  // - Object names can contain any sequence of valid Unicode characters, of length 1-1024 bytes when UTF-8 encoded.
   248  // - Object names cannot contain Carriage Return or Line Feed characters.
   249  // - Object names cannot start with .well-known/acme-challenge/.
   250  // - Objects cannot be named . or ...
   251  func TestStorageObjectGetGcsBucketAndPathParsingWorks(t *testing.T) {
   252  	var objects = [][]byte{
   253  		randomChariotYamlObject(),
   254  		randomChariotYamlObject(),
   255  		randomChariotYamlObject(),
   256  		randomChariotYamlObject(),
   257  		randomChariotYamlObject(),
   258  		randomChariotYamlObject(),
   259  		randomChariotYamlObject(),
   260  		randomChariotYamlObject(),
   261  	}
   262  	var dirs = []string{
   263  		defaultChariotDir,
   264  		"fluxcfg",
   265  		"multi/path",
   266  	}
   267  	var banners = []string{
   268  		"testbanner",
   269  		"test-banner",
   270  		"test.banner",
   271  		"test_banner",
   272  		"test1banner",
   273  		"test-banner2",
   274  		"3test.banner",
   275  		"4test_banner4",
   276  	}
   277  	var clusters = []string{
   278  		"testcluster",
   279  		"test-cluster",
   280  		"test.cluster",
   281  		"test_cluster",
   282  		"test1cluster",
   283  		"test-cluster2",
   284  		"3test.cluster",
   285  		"4test_cluster4",
   286  	}
   287  
   288  	var numChecks int
   289  	for _, dir := range dirs {
   290  		for _, object := range objects {
   291  			var gvknn struct {
   292  				APIVersion string `yaml:"apiVersion"`
   293  				Kind       string `yaml:"kind"`
   294  				Metadata   struct {
   295  					Name      string `yaml:"name"`
   296  					Namespace string `yaml:"namespace"`
   297  				} `yaml:"metadata"`
   298  			}
   299  			if err := yaml.Unmarshal(object, &gvknn); err != nil {
   300  				t.Fatal(err)
   301  			}
   302  			// The unhashed string is a representation of the GVKNN data needed to construct a file name.
   303  			var unhashed = fmt.Sprintf(HashFormatGVKNN, gvknn.APIVersion, gvknn.Kind, gvknn.Metadata.Name, gvknn.Metadata.Namespace)
   304  			// expectedHash is a lowercase base16 MD5 hash.
   305  			var expectedHash = fmt.Sprintf("%x", md5.Sum([]byte(unhashed))) //nolint:gosec // annoying lint
   306  
   307  			// Check each of the banners.
   308  			for _, banner := range banners {
   309  				var bw = StorageObject{
   310  					Location: fmt.Sprintf("gs://%s/%s/%s.yaml", banner, dir, expectedHash),
   311  				}
   312  
   313  				if banner != bw.getGcsBucket() {
   314  					const msg = "Banner %q does not equal to the calculated bucket %q from location %q"
   315  					t.Fatalf(msg, banner, bw.getGcsBucket(), bw.Location)
   316  				}
   317  
   318  				var expectedPath = fmt.Sprintf("%s/%s.yaml", dir, expectedHash)
   319  				if expectedPath != bw.getGcsPath() {
   320  					const msg = "Expected path %q does not equal the calculated path %q from location %q"
   321  					t.Fatalf(msg, expectedPath, bw.getGcsPath(), bw.Location)
   322  				}
   323  
   324  				// Commented out for future debugging purposes.
   325  				//t.Logf("Got expected bucket %q and path %q from location %q", banner, expectedPath, bw.Location)
   326  
   327  				// Check each of the cluster-wide objects
   328  				for _, cluster := range clusters {
   329  					var cw = StorageObject{
   330  						Location: fmt.Sprintf("gs://%s/clusters/%s/%s/%s.yaml", banner, cluster, dir, expectedHash),
   331  					}
   332  
   333  					if banner != cw.getGcsBucket() {
   334  						const msg = "Banner %q does not equal to the calculated bucket %q from location %q"
   335  						t.Fatalf(msg, banner, cw.getGcsBucket(), cw.Location)
   336  					}
   337  
   338  					var expectedPath = fmt.Sprintf("clusters/%s/%s/%s.yaml", cluster, dir, expectedHash)
   339  					if expectedPath != cw.getGcsPath() {
   340  						const msg = "Expected path %q does not equal the calculated path %q from location %q"
   341  						t.Fatalf(msg, expectedPath, cw.getGcsPath(), cw.Location)
   342  					}
   343  
   344  					// Commented out for future debugging purposes.
   345  					//t.Logf("Got expected bucket %q and path %q from location %q", banner, expectedPath, cw.Location)
   346  					numChecks++
   347  				}
   348  			}
   349  		}
   350  	}
   351  	t.Logf("Successfully verified %d combinations of StorageObject.Location for their 'bucket' and 'path' parsing", numChecks)
   352  }
   353  

View as plain text