diff --git a/test/acceptance/releases_images_test.go b/test/acceptance/releases_images_test.go index dd37732..cd9f5f0 100644 --- a/test/acceptance/releases_images_test.go +++ b/test/acceptance/releases_images_test.go @@ -2,6 +2,7 @@ package acceptance import ( "fmt" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -40,4 +41,76 @@ var _ = Describe("Trusted Artifact Signer Releases", Ordered, func() { Expect(mapped).To(HaveEach(1)) Expect(len(snapshotData.Images)).To(BeNumerically("==", len(mapped))) }) + + It("operator and operator bundle have both the same git reference", func() { + operatorReference, err := support.GetImageLabel(snapshotData.Images[support.OperatorImageKey], "vcs-ref") + Expect(err).NotTo(HaveOccurred()) + Expect(operatorReference).NotTo(BeEmpty()) + operatorBundleReference, err := support.GetImageLabel(snapshotData.Images[support.OperatorBundleImageKey], "vcs-ref") + Expect(err).NotTo(HaveOccurred()) + Expect(operatorBundleReference).NotTo(BeEmpty()) + Expect(operatorReference).To(Equal(operatorBundleReference)) + }) + + It("snapshot.json images have correct labels", func() { + var imageDataList []support.ImageData + imageLabelsErrors := make(map[string][]string) + + // Collect all images and their labels + for imageName, imageDefinition := range snapshotData.Images { + labels, err := support.InspectImageForLabels(imageDefinition) + Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Failed to inspect labels for image %s (%s)", imageName, imageDefinition)) + + imageData := support.ImageData{ + Image: imageDefinition, + Labels: labels, + } + imageDataList = append(imageDataList, imageData) + } + + // Check that all images have required labels with correct values + requiredLabels := support.RequiredImageLabels() + for _, imageData := range imageDataList { + var currentImageErrors []string + + for labelName, expectedValue := range requiredLabels { + if _, exists := imageData.Labels[labelName]; !exists { + currentImageErrors = append(currentImageErrors, + fmt.Sprintf(" %s: missing", labelName)) + continue + } + + if expectedValue != "" { // Specific value expected + if imageData.Labels[labelName] != expectedValue { + currentImageErrors = append(currentImageErrors, + fmt.Sprintf(" %s: %s, expected: %s", + labelName, imageData.Labels[labelName], expectedValue)) + } + } else { // Label must not be empty + if imageData.Labels[labelName] == "" { + currentImageErrors = append(currentImageErrors, + fmt.Sprintf(" %s: missing", labelName)) + } + } + } + + if len(currentImageErrors) > 0 { + imageLabelsErrors[imageData.Image] = currentImageErrors + } + } + + // Format errors in a human-readable way + if len(imageLabelsErrors) > 0 { + var errorReport strings.Builder + for image, errors := range imageLabelsErrors { + errorReport.WriteString(image) + errorReport.WriteString(":\n") + for _, error := range errors { + errorReport.WriteString(error) + errorReport.WriteString("\n") + } + } + Fail("Label validation errors found:\n" + errorReport.String()) + } + }) }) diff --git a/test/support/common.go b/test/support/common.go index 9bf58fe..53c2147 100644 --- a/test/support/common.go +++ b/test/support/common.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "os" + "slices" ) func GetEnv(key string) string { @@ -26,7 +27,7 @@ func getEnv(key string, isSecret bool) string { return envValue } -func GetMapKeys(m map[string]string) []string { +func GetMapKeys[V any](m map[string]V) []string { result := make([]string, 0, len(m)) for k := range m { result = append(result, k) @@ -42,6 +43,12 @@ func GetMapValues(m map[string]string) []string { return result } +func GetMapKeysSorted[V any](m map[string]V) []string { + keys := GetMapKeys(m) + slices.Sort(keys) + return keys +} + func SplitMap(original map[string]string, keysToKeep []string) (map[string]string, map[string]string) { remaining := make(map[string]string) moved := make(map[string]string) @@ -73,10 +80,18 @@ func LogArray(message string, data []string) { log.Print(result) } -func LogMap(message string, data map[string]string) { +func LogMap[V any](message string, data map[string]V) { result := message + "\n" for key, value := range data { - result += fmt.Sprintf(" [%-41s] %s\n", key, value) + result += fmt.Sprintf(" [%-41v] %v\n", key, value) + } + log.Print(result) +} + +func LogMapByProvidedKeys[V any](message string, data map[string]V, keysToLog []string) { + result := message + "\n" + for _, key := range keysToLog { + result += fmt.Sprintf(" [%-53v] %v\n", key, data[key]) } log.Print(result) } diff --git a/test/support/image_utils.go b/test/support/image_utils.go index 617e97a..5bc9642 100644 --- a/test/support/image_utils.go +++ b/test/support/image_utils.go @@ -16,21 +16,56 @@ import ( "github.com/docker/docker/client" ) +type ImageData struct { + Image string + Labels map[string]string +} + +func PullImageIfNotPresentLocally(ctx context.Context, imageDefinition string) error { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("could not create docker client: %w", err) + } + defer cli.Close() + + // Inspect the image to check if it exists locally. + _, _, err = cli.ImageInspectWithRaw(ctx, imageDefinition) + if err == nil { + return nil // image is present already + } + + if client.IsErrNotFound(err) { + log.Printf("Image '%s' not found locally, pulling...\n", imageDefinition) + pullResp, pullErr := cli.ImagePull(ctx, imageDefinition, image.PullOptions{ + Platform: "linux/amd64", + }) + if pullErr != nil { + return fmt.Errorf("failed to pull image: %w", pullErr) + } + defer pullResp.Close() + // ensure the pull operation completes. + if _, err := io.Copy(io.Discard, pullResp); err != nil { + return fmt.Errorf("failed to read pull response: %w", err) + } + return nil + } + + // another type of error, return it. + return fmt.Errorf("failed to inspect image: %w", err) +} + func RunImage(imageDefinition string, commands []string) (string, error) { ctx := context.TODO() + err := PullImageIfNotPresentLocally(ctx, imageDefinition) + if err != nil { + return "", err + } + log.Printf("Running image %s with commands %v\n", imageDefinition, commands) cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return "", fmt.Errorf("error while initializing docker client: %w", err) } - reader, err := cli.ImagePull(ctx, imageDefinition, image.PullOptions{ - Platform: "linux/amd64", - }) - if err != nil { - return "", fmt.Errorf("cannot pull image %s: %w", imageDefinition, err) - } - _, _ = io.Copy(os.Stdout, reader) - _ = reader.Close() resp, err := cli.ContainerCreate(ctx, &container.Config{ Image: imageDefinition, @@ -69,21 +104,53 @@ func RunImage(imageDefinition string, commands []string) (string, error) { return buf.String(), nil } -func FileFromImage(ctx context.Context, imageName, filePath, outputPath string) error { - // Initialize the Docker client +func InspectImageForLabels(imageDefinition string) (map[string]string, error) { + ctx := context.TODO() + err := PullImageIfNotPresentLocally(ctx, imageDefinition) + if err != nil { + return nil, err + } + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { - panic(err) + return nil, fmt.Errorf("error while initializing docker client: %w", err) + } + defer cli.Close() + + inspectData, _, err := cli.ImageInspectWithRaw(ctx, imageDefinition) + if err != nil { + return nil, fmt.Errorf("cannot inspect image %s: %w", imageDefinition, err) + } + if inspectData.Config == nil || len(inspectData.Config.Labels) == 0 { + log.Printf("Image [%s] does not have any labels\n", imageDefinition) + return make(map[string]string), nil + } + + return inspectData.Config.Labels, nil +} + +func GetImageLabel(imageDefinition, labelName string) (string, error) { + labels, err := InspectImageForLabels(imageDefinition) + if err != nil { + return "", err + } + if labelValue, ok := labels[labelName]; ok { + return labelValue, nil } + return "", fmt.Errorf("label [%s] not found in image %s", labelName, imageDefinition) +} - reader, err := cli.ImagePull(ctx, imageName, image.PullOptions{ - Platform: "linux/amd64", - }) +func FileFromImage(ctx context.Context, imageName, filePath, outputPath string) error { + err := PullImageIfNotPresentLocally(ctx, imageName) if err != nil { - return fmt.Errorf("cannot pull image %s: %w", imageName, err) + return err + } + + // Initialize the Docker client + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) } - _, _ = io.Copy(io.Discard, reader) - _ = reader.Close() // Create a container from the image resp, err := cli.ContainerCreate(ctx, &container.Config{ @@ -102,7 +169,7 @@ func FileFromImage(ctx context.Context, imageName, filePath, outputPath string) }() // Use Docker's API to copy the file from the container's filesystem - reader, _, err = cli.CopyFromContainer(ctx, resp.ID, filePath) + reader, _, err := cli.CopyFromContainer(ctx, resp.ID, filePath) if err != nil { return fmt.Errorf("failed to copy file from container: %w", err) } diff --git a/test/support/test_constants.go b/test/support/test_constants.go index 329abba..096d4a5 100644 --- a/test/support/test_constants.go +++ b/test/support/test_constants.go @@ -93,3 +93,14 @@ func GetOSArchMatrix() OSArchMatrix { "windows": {"amd64"}, } } + +// If no value is provided, the label must exist, but can have any non-empty value. +func RequiredImageLabels() map[string]string { + return map[string]string{ + "architecture": "x86_64", + "build-date": "", + "vcs-ref": "", + "vcs-type": "git", + "vendor": "Red Hat, Inc.", + } +}