package chariot import ( "crypto/md5" //nolint:gosec // trusted input crypto "crypto/rand" "fmt" "math/rand" //nolint:gosec // not a crytpo use. "regexp" "testing" "gopkg.in/yaml.v2" ) // randomChariotYamlObject creates an object with GVKNN data (and some labels) to represent a minimal Chariot Request.Objects member. func randomChariotYamlObject() []byte { // Group/Version var groupVersion = "clusterregistry.k8s.io/v1alpha1" // Kind var kinds = [3]string{"Cluster", "Namespace", "Tenant"} var kind = kinds[rand.Int()%len(kinds)] //nolint:gosec // not doing cryptography // Name var nameBytes [16]byte //nolint:errcheck // not doing cryptography crypto.Read(nameBytes[:]) //nolint:errcheck var name = fmt.Sprintf("%x", nameBytes) // Namespace var namespace string if rand.Int()%2 == 0 { //nolint:gosec // not doing cryptography // half probability of no namespace var namespaces = [5]string{"bulldozer", "ci", "godoc", "jack-bot", "policy-bot"} namespace = namespaces[rand.Int()%len(namespaces)] //nolint:gosec // not doing cryptography } // Label var labels map[string]string if rand.Int()%2 == 0 { //nolint:gosec // not doing cryptography // half probability of no labels var clusterNameBytes [4]byte var clusterTypeBytes [4]byte var fleetNameBytes [4]byte crypto.Read(clusterNameBytes[:]) //nolint:errcheck crypto.Read(clusterTypeBytes[:]) //nolint:errcheck crypto.Read(fleetNameBytes[:]) //nolint:errcheck labels = map[string]string{ "cluster.edge.ncr.com": fmt.Sprintf("%x", clusterNameBytes), "cluster.edge.ncr.com/type": fmt.Sprintf("%x", clusterTypeBytes), "fleet.edge.ncr.com": fmt.Sprintf("%x", fleetNameBytes), } } // Build the Yaml object var req struct { APIVersion string `yaml:"apiVersion"` Kind string `yaml:"kind"` Metadata struct { Name string `yaml:"name"` Namespace string `yaml:"namespace"` Labels map[string]string `yaml:"labels"` } `yaml:"metadata"` } req.APIVersion = groupVersion req.Kind = kind req.Metadata.Name = name req.Metadata.Namespace = namespace req.Metadata.Labels = labels y, _ := yaml.Marshal(req) //nolint:errcheck return y } // TestRequestDirOption checks possible configurations of Request.Dir both good and bad. func TestRequestDirOption(t *testing.T) { var req = &Request{ Operation: "CREATE", Banner: "my-banner", Cluster: "", // empty for banner wide objects. Dir: "", // Empty to check for default value. Owner: "test", Objects: [][]byte{ randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), }, } // Testing that default value is set for Dir when empty. _, err := req.StorageObjects() if err != nil { t.Fatal(err) } else if req.Dir != defaultChariotDir { 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) } req.Dir = defaultChariotDir _, err = req.StorageObjects() if err != nil { t.Fatalf("The req.StorageObjects() function should accept the directory value 'chariot' and not return an error.. Got %v", err) } else if req.Dir != defaultChariotDir { t.Fatalf("The req.StorageObjects() function should keep the directory value 'chariot' when StorageObjects is called.. Got %v", req.Dir) } for dir := range whitelistedDirs { req.Dir = dir _, err := req.StorageObjects() if err != nil { t.Fatalf("The req.StorageObjects() function should accept the whitelisted directory value %q and not return an error.. Got %v", dir, err) } else if req.Dir != dir { t.Fatalf("The req.StorageObjects() function should keep the whitelisted directory value %q when StorageObjects is called.. Got %v", dir, req.Dir) } } var badDirs = []string{"jhgkjhfgwljhfgwljhfw", "gwgwigwigwigi", "../foo", ".", "hello/../../foo/../bar/../", ".."} for _, dir := range badDirs { req.Dir = dir _, err = req.StorageObjects() if err == nil { t.Fatalf("The req.StorageObjects() function should return an error for this invalid directory: %q", dir) } } t.Logf("Successfully validated the Request.Dir optional field validation") } // TestRequestStorageObjectsFunction verifies that the StorageObjects function has not diverted from the Chariot specification. func TestRequestStorageObjectsFunction(t *testing.T) { var req = &Request{ Operation: "CREATE", Banner: "my-banner", Cluster: "", // empty for banner wide objects. Owner: "test", Objects: [][]byte{ randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), }, } bannerWideObjects, err := req.StorageObjects() if err != nil { t.Fatal(err) } // Set the Cluster variable for cluster-wide objects. req.Cluster = "test" clusterWideObjects, err := req.StorageObjects() if err != nil { t.Fatal(err) } // Ensure idempotency t.Logf("Checking that banner-wide and cluster-wide objects are idempotent") for i, b := range req.Objects { var ( originalObj = string(b) bannerWideObj = bannerWideObjects[i].Content clusterWideObj = clusterWideObjects[i].Content ) if originalObj != bannerWideObj { t.Logf("Original Object: %q", originalObj) t.Logf("Banner Wide Object: %q", bannerWideObj) t.Fatalf("Banner wide object is not idempotent") } else if originalObj != clusterWideObj { t.Logf("Original Object: %q", originalObj) t.Logf("Cluster Wide Object: %q", clusterWideObj) t.Fatalf("Cluster wide object is not idempotent") } } t.Logf("Successfully checked that banner-wide and cluster-wide objects are idempotent") // Ensure the hash function is correct. t.Logf("Checking that banner-wide and cluster-wide objects are hashed properly") var reTrimHash = regexp.MustCompile(fmt.Sprintf("^(.*/%s/)|([.]yaml)$", req.Dir)) for i, b := range req.Objects { var gvknn struct { APIVersion string `yaml:"apiVersion"` Kind string `yaml:"kind"` Metadata struct { Name string `yaml:"name"` Namespace string `yaml:"namespace"` } `yaml:"metadata"` } if err := yaml.Unmarshal(b, &gvknn); err != nil { t.Fatal(err) } // The unhashed string is a representation of the GVKNN data needed to construct a file name. var unhashed = fmt.Sprintf(HashFormatGVKNN, gvknn.APIVersion, gvknn.Kind, gvknn.Metadata.Name, gvknn.Metadata.Namespace) // expectedHash is a lowercase base16 MD5 hash. var expectedHash = fmt.Sprintf("%x", md5.Sum([]byte(unhashed))) //nolint:gosec // annoying lint t.Logf("Checking hash %q", expectedHash) // Check the hash if expectedHash != reTrimHash.ReplaceAllString(bannerWideObjects[i].Location, "") { t.Fatalf("Expected hash %q did not match hash at location %q", expectedHash, bannerWideObjects[i].Location) } if expectedHash != reTrimHash.ReplaceAllString(clusterWideObjects[i].Location, "") { t.Fatalf("Expected hash %q did not match hash at location %q", expectedHash, clusterWideObjects[i].Location) } } t.Logf("Successfully checked that banner-wide and cluster-wide objects are hashed properly") // Ensure that the paths are created properly. The hash has already been verified. t.Logf("Checking that banner-wide and cluster-wide object locations are set properly") const reGoodBannerWideLocFmt = "^gs://%s/%s/[a-f0-9]{32}[.]yaml$" const reGoodClusterWideLocFmt = "^gs://%s/%s/%s/[a-f0-9]{32}[.]yaml$" t.Logf("Checking banner-wide regex: %q", reGoodBannerWideLocFmt) t.Logf("Checking cluster-wide regex: %q", reGoodClusterWideLocFmt) var reGoodBannerWideLoc = regexp.MustCompile(fmt.Sprintf(reGoodBannerWideLocFmt, req.Banner, req.Dir)) var reGoodClusterWideLoc = regexp.MustCompile(fmt.Sprintf(reGoodClusterWideLocFmt, req.Banner, req.Cluster, req.Dir)) for i := range req.Objects { var bwo = bannerWideObjects[i].Location var cwo = clusterWideObjects[i].Location t.Logf("Banner-wide location: %q", bwo) if !reGoodBannerWideLoc.MatchString(bwo) { t.Fatalf("The banner-wide object location does not satisfy the regex: %q", bwo) } t.Logf("Cluster-wide location: %q", cwo) if !reGoodClusterWideLoc.MatchString(cwo) { t.Fatalf("The cluster-wide object location does not satisfy the regex: %q", cwo) } } t.Logf("Successfully checked that banner-wide and cluster-wide objects have valid locations") } // TestStorageObjectGetGcsBucketAndPathParsingWorks ensures the getGcsBucket and getGcsPath works. // Yes, this test is O(n^3) // // See: https://cloud.google.com/storage/docs/naming-buckets // See: https://cloud.google.com/storage/docs/naming-objects // // Bucket names can only contain lowercase letters, numeric characters, dashes (-), underscores (_), and dots (.). // Spaces are not allowed. Names containing dots require verification. // - Bucket names must start and end with a number or letter. // - Bucket names must contain 3-63 characters. // - Names containing dots can contain up to 222 characters, but each dot-separated component can be no longer than 63 characters. // - Bucket names cannot be represented as an IP address in dotted-decimal notation (for example, 192.168.5.4). // - Bucket names cannot begin with the "goog" prefix. // - Bucket names cannot contain "google" or close misspellings, such as "g00gle". // // Your object names must meet the following requirements: // - Object names can contain any sequence of valid Unicode characters, of length 1-1024 bytes when UTF-8 encoded. // - Object names cannot contain Carriage Return or Line Feed characters. // - Object names cannot start with .well-known/acme-challenge/. // - Objects cannot be named . or ... func TestStorageObjectGetGcsBucketAndPathParsingWorks(t *testing.T) { var objects = [][]byte{ randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), randomChariotYamlObject(), } var dirs = []string{ defaultChariotDir, "fluxcfg", "multi/path", } var banners = []string{ "testbanner", "test-banner", "test.banner", "test_banner", "test1banner", "test-banner2", "3test.banner", "4test_banner4", } var clusters = []string{ "testcluster", "test-cluster", "test.cluster", "test_cluster", "test1cluster", "test-cluster2", "3test.cluster", "4test_cluster4", } var numChecks int for _, dir := range dirs { for _, object := range objects { var gvknn struct { APIVersion string `yaml:"apiVersion"` Kind string `yaml:"kind"` Metadata struct { Name string `yaml:"name"` Namespace string `yaml:"namespace"` } `yaml:"metadata"` } if err := yaml.Unmarshal(object, &gvknn); err != nil { t.Fatal(err) } // The unhashed string is a representation of the GVKNN data needed to construct a file name. var unhashed = fmt.Sprintf(HashFormatGVKNN, gvknn.APIVersion, gvknn.Kind, gvknn.Metadata.Name, gvknn.Metadata.Namespace) // expectedHash is a lowercase base16 MD5 hash. var expectedHash = fmt.Sprintf("%x", md5.Sum([]byte(unhashed))) //nolint:gosec // annoying lint // Check each of the banners. for _, banner := range banners { var bw = StorageObject{ Location: fmt.Sprintf("gs://%s/%s/%s.yaml", banner, dir, expectedHash), } if banner != bw.getGcsBucket() { const msg = "Banner %q does not equal to the calculated bucket %q from location %q" t.Fatalf(msg, banner, bw.getGcsBucket(), bw.Location) } var expectedPath = fmt.Sprintf("%s/%s.yaml", dir, expectedHash) if expectedPath != bw.getGcsPath() { const msg = "Expected path %q does not equal the calculated path %q from location %q" t.Fatalf(msg, expectedPath, bw.getGcsPath(), bw.Location) } // Commented out for future debugging purposes. //t.Logf("Got expected bucket %q and path %q from location %q", banner, expectedPath, bw.Location) // Check each of the cluster-wide objects for _, cluster := range clusters { var cw = StorageObject{ Location: fmt.Sprintf("gs://%s/clusters/%s/%s/%s.yaml", banner, cluster, dir, expectedHash), } if banner != cw.getGcsBucket() { const msg = "Banner %q does not equal to the calculated bucket %q from location %q" t.Fatalf(msg, banner, cw.getGcsBucket(), cw.Location) } var expectedPath = fmt.Sprintf("clusters/%s/%s/%s.yaml", cluster, dir, expectedHash) if expectedPath != cw.getGcsPath() { const msg = "Expected path %q does not equal the calculated path %q from location %q" t.Fatalf(msg, expectedPath, cw.getGcsPath(), cw.Location) } // Commented out for future debugging purposes. //t.Logf("Got expected bucket %q and path %q from location %q", banner, expectedPath, cw.Location) numChecks++ } } } } t.Logf("Successfully verified %d combinations of StorageObject.Location for their 'bucket' and 'path' parsing", numChecks) }