From 2f1e0796ea4c6f41769f2d05ecdfa80a5508d703 Mon Sep 17 00:00:00 2001 From: Brett Tofel Date: Thu, 19 Oct 2023 13:25:27 +0200 Subject: [PATCH 1/9] Add set of Dev Guide docs --- DEV_GUIDE_ACTIONS.md | 28 ++++++++++ DEV_GUIDE_ADDING_CMDS.md | 115 +++++++++++++++++++++++++++++++++++++++ DEV_GUIDE_CUSTOM.md | 47 ++++++++++++++++ DEV_GUIDE_EXT_TOOLS.md | 40 ++++++++++++++ DEV_GUIDE_MODELS.md | 8 +++ DEV_GUIDE_VALIDATION.md | 10 ++++ 6 files changed, 248 insertions(+) create mode 100644 DEV_GUIDE_ACTIONS.md create mode 100644 DEV_GUIDE_ADDING_CMDS.md create mode 100644 DEV_GUIDE_CUSTOM.md create mode 100644 DEV_GUIDE_EXT_TOOLS.md create mode 100644 DEV_GUIDE_MODELS.md create mode 100644 DEV_GUIDE_VALIDATION.md diff --git a/DEV_GUIDE_ACTIONS.md b/DEV_GUIDE_ACTIONS.md new file mode 100644 index 00000000..87a5cd75 --- /dev/null +++ b/DEV_GUIDE_ACTIONS.md @@ -0,0 +1,28 @@ +## PKG/ACTIONS Directory Run Down + +The `pkg/actions` directory focuses on core actions and functionalities of the audit tool. + +### run_validators.go + +- **RunValidators**: Main function to run various validators. +- **checkBundleAgainstCommonCriteria**: Checks bundles against common criteria. +- **fromOCPValidator**: Related to OCP validation. +- **fromAuditValidatorsBundleSize**: Checks bundle sizes as part of the validation. + +### run_scorecard.go + +- **RunScorecard**: Main function to run the scorecard functionality. +- **writeScorecardConfig**: Writes or updates the configuration for the scorecard. + +### get_bundle.go + +- **GetDataFromBundleImage**: Fetches data from a bundle image. +- **createBundleDir**: Creates a directory for the bundle. +- **extractBundleFromImage**: Extracts bundle data from an image. +- **cleanupBundleDir**: Cleans up the bundle directory after processing. +- **DownloadImage**: Downloads an image for further processing or analysis. + +### extract_index.go + +- **ExtractIndexDBorCatalogs**: Extracts database or catalogs from an index. +- **GetVersionTagFromImage**: Retrieves the version tag from an image. diff --git a/DEV_GUIDE_ADDING_CMDS.md b/DEV_GUIDE_ADDING_CMDS.md new file mode 100644 index 00000000..e760d79d --- /dev/null +++ b/DEV_GUIDE_ADDING_CMDS.md @@ -0,0 +1,115 @@ +# Commands and Sub-Commands Development Guide + +This section of the development guide focuses on understanding, adding, and modifying commands and sub-commands within +the audit tool. By following the described patterns, developers can seamlessly introduce new functionalities. + +## Adding a New Primary Command + +1. **Define Your Command**: + Begin by defining your command structure using the `cobra.Command` type, including: + - `Use`: Command's name. + - `Short`: A brief description. + - `Long`: An extended description. + + Example from the `audit-tool`: + ```go + rootCmd := &cobra.Command{ + Use: "audit-tool", + Short: "An analytic tool to audit operator bundles and index catalogs", + Long: "The audit is an analytic tool which uses the Operator Framework solutions ...", + } + ``` + +2. **Add Sub-Commands (if necessary)**: + For embedding sub-commands to your main command, employ the `AddCommand` method. As observed in the `audit-tool`, + sub-commands like `index` and `custom` are integrated as: + ```go + rootCmd.AddCommand(index.NewCmd()) + rootCmd.AddCommand(custom.NewCmd()) + ``` + +3. **Execute the Command**: + Ensure the primary command's execution in the `main` function: + ```go + if err := rootCmd.Execute(); err != nil { + log.Fatal(err) + } + ``` + +## Tutorial: Add a Sub-Command to the `index` Command + +The `index` command has sub-commands like `bundles` and `eus`. To introduce a new sub-command: + +1. **Create a Sub-directory**: + Organize by creating a sub-directory within `index`. Name it as per your sub-command. E.g., for a sub-command + named `sample`, formulate a `sample` directory. + +2. **Define Your Sub-Command**: + In this directory, create a `command.go` file and define your sub-command structure: + ```go + package sample + + import ( + "github.com/spf13/cobra" + ) + + func NewCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "sample", + Short: "Short description of sample", + Long: "Detailed description of sample...", + } + return cmd + } + ``` + +3. **Add Flags (optional)**: + For flag additions to your sub-command, utilize the `Flags` method. For instance, to integrate a `--test` flag: + ```go + cmd.Flags().BoolP("test", "t", false, "Description of test flag") + ``` + +4. **Integrate Sub-Command**: + Navigate back to the `main.go` of `index` and add your new sub-command: + ```go + indexCmd.AddCommand(sample.NewCmd()) + ``` + +--- + +## Adding Flags with parameters to the `sample` Sub-Command + +Flags offer flexibility to commands by allowing users to specify options or provide additional input. Here, we'll delve +into adding both boolean flags and flags that accept parameters to the `sample` sub-command. + +1. **Boolean Flag**: + A flag that signifies a simple `true` or `false` option. + + Example: Adding a `--test` flag to the `sample` sub-command: + ```go + cmd.Flags().BoolP("test", "t", false, "Description of test flag") + ``` + Use: `audit-tool index sample --test` + +2. **Flag with Parameter**: + A flag that necessitates an accompanying value. + + Example: Introducing a `--input` flag which requires a string parameter: + ```go + cmd.Flags().StringP("input", "i", "", "Provide input data for the sample command") + ``` + Use: `audit-tool index sample --input "This is sample input"` + +3. **Utilize Flag Parameters in Command Logic**: + To harness the values provided through flags, use the `cmd.Flag("flag-name").Value.String()` method. + + Example: Using the `--input` flag's value within the `sample` sub-command: + ```go + var input string = cmd.Flag("input").Value.String() + if input != "" { + fmt.Println("Received input:", input) + } else { + fmt.Println("No input provided.") + } + ``` + This code snippet checks if the `--input` flag has been provided a value, and if so, it prints the received input. diff --git a/DEV_GUIDE_CUSTOM.md b/DEV_GUIDE_CUSTOM.md new file mode 100644 index 00000000..53aa0d59 --- /dev/null +++ b/DEV_GUIDE_CUSTOM.md @@ -0,0 +1,47 @@ +## CUSTOM Directory Run Down + +The `custom` directory focuses on providing specific functionalities that are tailored to the unique requirements of the +audit tool. These functionalities are organized as sub-commands, each having its own directory. + +### NewCmd + +This function initializes the overarching command for the 'custom' functionalities. It sets up the primary CLI structure +for the custom operations, guiding users to its specific sub-commands. + +### Subdirectories: + +Each subdirectory represents a specific custom functionality or operation. Here are the details: + +#### validator + +- **NewCmd**: This function sets up the command structure for the 'validator' functionality. It provides a brief + description to the user about its purpose and defines available flags and options. +- **validation**: This function validates the provided flags and arguments specific to the 'validator' operation. It + ensures that necessary inputs are present and correctly formatted. +- **run**: This function drives the core logic of the 'validator' functionality, making necessary calls to validate the + data as per the defined criteria. + +#### deprecate + +- **NewCmd**: This function initializes the command for the 'deprecate' functionality, defining its purpose and + available flags. +- **validation**: This function checks the provided flags and arguments to ensure they align with the 'deprecate' + operation requirements. +- **run**: This function manages the 'deprecate' operation, handling the necessary steps to mark certain data as + deprecated. + +#### qa + +- **NewCmd**: This function sets up the command for the 'qa' functionality, providing a brief description and defining + the available flags. +- **validation**: This function validates user input, ensuring it matches the criteria set for the 'qa' operation. +- **run**: This function handles the 'qa' operation, performing quality assurance checks on the provided data. + +#### multiarch + +- **NewCmd**: This function initializes the command for the 'multiarch' functionality. It provides a description and + lists available flags for the user. +- **validation**: This function checks the flags and arguments to ensure they fit the 'multiarch' operation's + requirements. +- **run**: This function manages the 'multiarch' functionality, handling operations and checks related to multiple + architectures. diff --git a/DEV_GUIDE_EXT_TOOLS.md b/DEV_GUIDE_EXT_TOOLS.md new file mode 100644 index 00000000..aa3a4f10 --- /dev/null +++ b/DEV_GUIDE_EXT_TOOLS.md @@ -0,0 +1,40 @@ +# External Tool Integration in the Audit Tool + +This document showcases how external tools have been integrated into the Audit Tool. By examining these specific +implementations, developers can gain insights into adding similar integrations in the future. + +## `operator-sdk` Integration + +1. **Tool Purpose**: The `operator-sdk` tool assists in building, testing, and deploying Operator projects. +2. **Invocation in the Audit Tool**: The binary is invoked in the audit tool via system command calls. For instance: + ```go + cmd := exec.Command("operator-sdk", "bundle", "validate", "--select-optional", "suite=operatorframework") + ``` +3. **Handling Results**: Output and errors from the tool are captured and processed. For example: + ```go + output, err := cmd.CombinedOutput() + if err != nil { + log.Errorf("operator-sdk validation failed: %s", output) + } + ``` + +## `check-payload` Integration + +1. **Tool Purpose**: The `check-payload` tool scans operator images to ensure compliance with specific standards. +2. **Invocation in the Audit Tool**: The binary is invoked similarly to the `operator-sdk`, but with different + arguments: + ```go + cmd := exec.Command("/path/to/check-payload", "scan", "operator", "--spec", imageRef) + ``` +3. **Handling Results**: Output, warnings, and errors from this tool are captured and processed. Here's an example of + how these results can be processed and incorporated into the audit tool's report: + ```go + output, err := cmd.CombinedOutput() + if err != nil { + // Handle error, potentially adding it to the report + } else { + // Process the output and distinguish between warnings and errors + // Add warnings and errors to the report as appropriate + } + ``` + diff --git a/DEV_GUIDE_MODELS.md b/DEV_GUIDE_MODELS.md new file mode 100644 index 00000000..2bd70ec8 --- /dev/null +++ b/DEV_GUIDE_MODELS.md @@ -0,0 +1,8 @@ +## PKG/MODELS Directory Run Down + +The `pkg/models` directory contains data structures or models that provide a representation of the core entities within +the system. + +### bundle.go + +- **AuditBundle**: Represents the model for an audit bundle. diff --git a/DEV_GUIDE_VALIDATION.md b/DEV_GUIDE_VALIDATION.md new file mode 100644 index 00000000..e0d409de --- /dev/null +++ b/DEV_GUIDE_VALIDATION.md @@ -0,0 +1,10 @@ +## PKG/VALIDATION Directory Run Down + +The `pkg/validation` directory contains code related to validation checks and functionalities within the application. + +### bundle_size.go + +- **validateBundleSizeValidator**: A function related to validating the size of a bundle. +- **validateBundleSize**: Function focused on bundle size validation. +- **checkBundleSize**: Checks the size of a bundle. +- **formatBytesInUnit**: Utility function to format bytes into a specific unit (e.g., KB, MB). From 3a7c67b1276f89b209027c18ac17b39150714286 Mon Sep 17 00:00:00 2001 From: Brett Tofel Date: Thu, 19 Oct 2023 13:27:44 +0200 Subject: [PATCH 2/9] Change permissions on check-license.sh --- hack/check-license.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 hack/check-license.sh diff --git a/hack/check-license.sh b/hack/check-license.sh old mode 100755 new mode 100644 From 51eac189d7b94b31c40c8cec8a7a83dec37e566d Mon Sep 17 00:00:00 2001 From: Brett Tofel Date: Fri, 8 Dec 2023 11:03:38 +0100 Subject: [PATCH 3/9] Dockerfile from RH image oci not all tests working --- pkg/actions/get_bundle.go | 10 +++ pkg/helpers.go | 129 ++++++++++++++++++++++++++++++++++++++ pkg/helpers_test.go | 105 +++++++++++++++++++++++++++++++ pkg/models/bundle.go | 9 ++- 4 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 pkg/helpers_test.go diff --git a/pkg/actions/get_bundle.go b/pkg/actions/get_bundle.go index 7c25f4db..1bb68755 100644 --- a/pkg/actions/get_bundle.go +++ b/pkg/actions/get_bundle.go @@ -79,7 +79,17 @@ func GetDataFromBundleImage(auditBundle *models.AuditBundle, } } auditBundle.BundleImageLabels = inspectManifest.DockerConfig.Labels + } + dockerfile, err := pkg.RunSkopeoLayerExtract(auditBundle.OperatorBundleImagePath) + if err != nil { + log.Printf("Error extracting Dockerfile: %s", err) + // Handle the error, e.g., by returning or continuing with other logic + } else { + // Process the extracted Dockerfile commands + for _, cmd := range dockerfile.Commands { + log.Printf("Command: %s, Value: %s", cmd.CommandType, cmd.Value) + } } // Read the bundle diff --git a/pkg/helpers.go b/pkg/helpers.go index e8bbe026..ab1c9fef 100644 --- a/pkg/helpers.go +++ b/pkg/helpers.go @@ -39,6 +39,15 @@ const Podman = "podman" const InfrastructureAnnotation = "operators.openshift.io/infrastructure-features" +type DockerfileCommand struct { + CommandType string + Value string +} + +type Dockerfile struct { + Commands []DockerfileCommand +} + // PropertiesAnnotation used to Unmarshal the JSON in the CSV annotation type PropertiesAnnotation struct { Type string @@ -202,6 +211,126 @@ func RunDockerInspect(image string, containerEngine string) (DockerInspect, erro return dockerInspect[0], nil } +func RunSkopeoLayerExtract(image string) (Dockerfile, error) { + var dockerfile Dockerfile + + // Create a temporary directory for OCI layout + tmpDir, err := os.MkdirTemp("", "oci-layout-") + if err != nil { + log.Printf("Failed to create temporary directory for OCI layout: %s", err) + return dockerfile, err + } + defer func() { + if err := os.RemoveAll(tmpDir); err != nil { + log.Printf("Failed to clean up temporary directory %s: %s", tmpDir, err) + } + }() + + ociDir := filepath.Join(tmpDir, "oci") + log.Printf("Copying image to local OCI layout using Skopeo: %s", image) + + // Copy the image to local OCI layout using Skopeo + copyCmd := exec.Command("skopeo", "copy", image, "oci:"+ociDir) + copyOutput, err := copyCmd.CombinedOutput() + if err != nil { + log.Printf("Failed to copy image with Skopeo: %s", err) + log.Printf("Skopeo copy command output: %s", string(copyOutput)) + return dockerfile, err + } + + log.Printf("Inspecting image to get layer SHAs using Skopeo") + + // Inspect the image to get layer SHAs using Skopeo + inspectCmd := exec.Command("skopeo", "inspect", "--format", "{{json .Layers}}", "oci:"+ociDir) + inspectOut, err := inspectCmd.Output() + if err != nil { + log.Printf("Failed to inspect image with Skopeo: %s", err) + return dockerfile, err + } + + // Extract layer SHAs + var layerSHAs []string + err = json.Unmarshal(inspectOut, &layerSHAs) + if err != nil { + log.Printf("Failed to unmarshal layer SHAs: %s", err) + return dockerfile, err + } + + // Process each layer + for _, layerSHA := range layerSHAs { + layerSHA = strings.TrimPrefix(layerSHA, "sha256:") + + // Construct the correct layer file path + layerFile := filepath.Join(ociDir, "blobs", "sha256", layerSHA) + + // Create a temporary directory for this layer + layerTmpDir, err := os.MkdirTemp("", "layer-") + if err != nil { + log.Printf("Failed to create temporary directory for layer: %s", err) + continue + } + + // Extract the layer into the temporary directory + tarCmd := exec.Command("tar", "-xf", layerFile, "-C", layerTmpDir) + tarOutput, err := tarCmd.CombinedOutput() + if err != nil { + log.Printf("Failed to extract layer with tar command: %s", err) + log.Printf("Tar command output: %s", string(tarOutput)) + cleanUpTempDir(layerTmpDir) + continue + } + + // Search for Dockerfile in the extracted layer using Walk + var foundDockerfilePath string + err = filepath.Walk(layerTmpDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if strings.HasPrefix(info.Name(), "Dockerfile") { + foundDockerfilePath = path + return filepath.SkipDir // Found, no need to continue walking + } + return nil + }) + if err != nil || foundDockerfilePath == "" { + cleanUpTempDir(layerTmpDir) + continue + } + + // Read Dockerfile content + content, err := os.ReadFile(foundDockerfilePath) + if err != nil { + log.Printf("Failed to read Dockerfile: %s", err) + cleanUpTempDir(layerTmpDir) + continue + } + + // Parse Dockerfile content + lines := strings.Split(string(content), "\n") + for _, line := range lines { + parts := strings.SplitN(line, " ", 2) + if len(parts) == 2 { + dockerfile.Commands = append(dockerfile.Commands, DockerfileCommand{ + CommandType: parts[0], + Value: parts[1], + }) + } + } + + // Clean up the temporary directory for this layer + cleanUpTempDir(layerTmpDir) + } + + return dockerfile, nil +} + +// cleanUpTempDir handles the cleanup of a temporary directory and logs any errors. +func cleanUpTempDir(dir string) { + if err := os.RemoveAll(dir); err != nil { + log.Printf("Failed to clean up temporary directory %s: %s", dir, err) + } +} + func RunDockerManifestInspect(image string, containerEngine string) (DockerManifestInspect, error) { cmd := exec.Command(containerEngine, "manifest", "inspect", image) output, err := RunCommand(cmd) diff --git a/pkg/helpers_test.go b/pkg/helpers_test.go new file mode 100644 index 00000000..b4c7ec2b --- /dev/null +++ b/pkg/helpers_test.go @@ -0,0 +1,105 @@ +package pkg + +import ( + "log" + "os/exec" + "reflect" + "testing" +) + +func TestRunSkopeoLayerExtractSuite(t *testing.T) { + tests := []struct { + name string + imageRef string + expectedDockerfile Dockerfile + }{ + { + name: "TestQuayOperatorBundle", + imageRef: "docker://registry.redhat.io/quay/quay-operator-bundle@sha256:a97a63899d23e23d039ea36bd575c018d7b6295b7942b15a8bded52f09736bda", + expectedDockerfile: Dockerfile{ + Commands: []DockerfileCommand{ + {CommandType: "FROM", Value: "scratch"}, + {CommandType: "LABEL", Value: `com.redhat.delivery.operator.bundle=true`}, + {CommandType: "LABEL", Value: `com.redhat.delivery.openshift.ocp.versions="v4.8"`}, + {CommandType: "LABEL", Value: `com.redhat.openshift.versions="v4.8"`}, + {CommandType: "LABEL", Value: `com.redhat.delivery.backport=false`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.mediatype.v1=registry+v1`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.manifests.v1=manifests/`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.metadata.v1=metadata/`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.package.v1=quay-operator`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.channels.v1=stable-3.8`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.channel.default.v1=stable-3.8`}, + {CommandType: "LABEL", Value: `com.redhat.component="quay-operator-bundle-container"`}, + {CommandType: "LABEL", Value: `name="quay/quay-operator-bundle"`}, + {CommandType: "LABEL", Value: `summary="Quay Operator bundle container image"`}, + {CommandType: "LABEL", Value: `description="Operator bundle for Quay Operator"`}, + {CommandType: "LABEL", Value: `maintainer="Red Hat "`}, + {CommandType: "LABEL", Value: `version=v3.8.11`}, + {CommandType: "LABEL", Value: `io.k8s.display-name="Red Hat Quay Operator Bundle"`}, + {CommandType: "LABEL", Value: `io.openshift.tags="quay"`}, + {CommandType: "COPY", Value: `bundle/manifests/*.yaml /manifests/`}, + {CommandType: "COPY", Value: `bundle/manifests/metadata/annotations.yaml /metadata/annotations.yaml`}, + {CommandType: "LABEL", Value: `release=20`}, + {CommandType: "ADD", Value: `quay-operator-bundle-container-v3.8.11-20.json /root/buildinfo/content_manifests/quay-operator-bundle-container-v3.8.11-20.json`}, + {CommandType: "LABEL", Value: `"com.redhat.license_terms"="https://www.redhat.com/agreements" "distribution-scope"="public" "vendor"="Red Hat, Inc." "build-date"="2023-08-07T23:21:46" "architecture"="x86_64" "vcs-type"="git" "vcs-ref"="f6eb857b8bd8768d51a311bc274f53ce7856ae04" "io.k8s.description"="Operator bundle for Quay Operator" "url"="https://access.redhat.com/containers/#/registry.access.redhat.com/quay/quay-operator-bundle/images/v3.8.11-20"`}, + }, + }, + }, + //{ + // name: "Test3ScaleOperatorBundle", + // imageRef: "docker://registry.redhat.io/3scale-mas/3scale-rhel7-operator@sha256:0a6673eae2f0e8d95b919b0243e44d2c0383d13e2e616ac8d3f80742d496d292", + // expectedDockerfile: Dockerfile{ + // Commands: []DockerfileCommand{ + // {CommandType: "FROM", Value: "registry.redhat.io/devtools/go-toolset-rhel7:1.19.13-1.1697640714 AS builder"}, + // {CommandType: "ENV", Value: `PROJECT_NAME="3scale-operator"`}, + // {CommandType: "ENV", Value: `OUTPUT_DIR="/tmp/_output"`}, + // {CommandType: "ENV", Value: `BINARY_NAME="manager"`}, + // {CommandType: "ENV", Value: `BUILD_PATH="${REMOTE_SOURCE_DIR}/app"`}, + // {CommandType: "WORKDIR", Value: `${BUILD_PATH}`}, + // {CommandType: "COPY", Value: `$REMOTE_SOURCE $REMOTE_SOURCE_DIR`}, + // {CommandType: "ADD", Value: `patches /tmp/patches`}, + // {CommandType: "RUN", Value: `find /tmp/patches -type f -name '*.patch' -print0 | sort --zero-terminated | xargs -t -0 -n 1 patch --force -p1`}, + // {CommandType: "USER", Value: `root`}, + // {CommandType: "RUN", Value: `mkdir -p ${OUTPUT_DIR}`}, + // {CommandType: "RUN", Value: `echo "build path: ${BUILD_PATH}"`}, + // {CommandType: "RUN", Value: `echo "output path: ${OUTPUT_DIR}"`}, + // {CommandType: "RUN", Value: `scl enable go-toolset-1.19 "GOOS=linux GOARCH=$(scl enable go-toolset-1.19 'go env GOARCH') CGO_ENABLED=0 GO111MODULE=on go build -o ${OUTPUT_DIR}/${BINARY_NAME} main.go"`}, + // {CommandType: "RUN", Value: `mkdir ${OUTPUT_DIR}/licenses/`}, + // {CommandType: "RUN", Value: `cp "./licenses.xml" "${OUTPUT_DIR}/licenses/"`}, + // {CommandType: "FROM", Value: `registry.redhat.io/ubi7/ubi-minimal:7.9-1196`}, + // {CommandType: "LABEL", Value: `com.redhat.component="3scale-mas-operator-container" name="3scale-mas/3scale-rhel7-operator" version="1.17.0" summary="3scale Operator container image" description="Operator provides a way to install a 3scale API Management and ability to define 3scale API definitions." io.k8s.display-name="3scale Operator" io.openshift.expose-services="" io.openshift.tags="3scale, 3scale-amp, api" upstream_repo="https://github.com/3scale/3scale-operator" upstream_ref="a5d72cc78a29ce38f3c60761cd7d2afff0727feb" maintainer="eastizle@redhat.com"`}, + // {CommandType: "ENV", Value: `OPERATOR_BINARY_NAME="manager" USER_UID=1001 USER_NAME=3scale-operator`}, + // {CommandType: "USER", Value: `root`}, + // {CommandType: "COPY", Value: `--from=builder /tmp/_output/${OPERATOR_BINARY_NAME} /`}, + // {CommandType: "RUN", Value: `chown ${USER_UID} /${OPERATOR_BINARY_NAME}`}, + // {CommandType: "ENV", Value: `LICENSES_DIR="/root/licenses/3scale-operator/"`}, + // {CommandType: "RUN", Value: `mkdir -p ${LICENSES_DIR}`}, + // {CommandType: "COPY", Value: `--from=builder /tmp/_output/licenses/licenses.xml ${LICENSES_DIR}`}, + // {CommandType: "RUN", Value: `chown ${USER_UID} ${LICENSES_DIR}/licenses.xml`}, + // {CommandType: "ENTRYPOINT", Value: `["/manager"]`}, + // {CommandType: "USER", Value: `${USER_UID}`}, + // }, + // }, + //}, + // Additional tests can be added here + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() // Enable parallel execution of this subtest + + result, err := RunSkopeoLayerExtract(tt.imageRef) + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + log.Printf("Skopeo command failed with exit code: %d", exitError.ExitCode()) + log.Printf("Stderr: %s", exitError.Stderr) + } + t.Fatalf("RunSkopeoLayerExtract returned an error: %v", err) + } + + if !reflect.DeepEqual(result, tt.expectedDockerfile) { + t.Errorf("RunSkopeoLayerExtract() = %v, want %v", result, tt.expectedDockerfile) + } + }) + } +} diff --git a/pkg/models/bundle.go b/pkg/models/bundle.go index 7203bb2a..b661f92c 100644 --- a/pkg/models/bundle.go +++ b/pkg/models/bundle.go @@ -39,13 +39,16 @@ type AuditBundle struct { IsHeadOfChannel bool BundleImageLabels map[string]string `json:"bundleImageLabels,omitempty"` BundleAnnotations map[string]string `json:"bundleAnnotations,omitempty"` + BundleDockerfile pkg.Dockerfile Errors []string } func NewAuditBundle(operatorBundleName, operatorBundleImagePath string) *AuditBundle { - auditBundle := AuditBundle{} - auditBundle.OperatorBundleName = operatorBundleName - auditBundle.OperatorBundleImagePath = operatorBundleImagePath + auditBundle := AuditBundle{ + OperatorBundleName: operatorBundleName, + OperatorBundleImagePath: operatorBundleImagePath, + BundleDockerfile: pkg.Dockerfile{Commands: []pkg.DockerfileCommand{}}, + } return &auditBundle } From 8e04f003005c5413792317bf6c25ce42012114ac Mon Sep 17 00:00:00 2001 From: Brett Tofel Date: Fri, 8 Dec 2023 17:15:55 +0100 Subject: [PATCH 4/9] Dockerfile in AuditBundle, multi-layer not working --- pkg/helpers.go | 45 ++++++++++++++++++--------- pkg/helpers_test.go | 74 ++++++++++++++++++++++----------------------- 2 files changed, 68 insertions(+), 51 deletions(-) diff --git a/pkg/helpers.go b/pkg/helpers.go index ab1c9fef..884259ab 100644 --- a/pkg/helpers.go +++ b/pkg/helpers.go @@ -214,8 +214,11 @@ func RunDockerInspect(image string, containerEngine string) (DockerInspect, erro func RunSkopeoLayerExtract(image string) (Dockerfile, error) { var dockerfile Dockerfile + // Specify a base directory you have full control over + baseDir := "/tmp" // Update this path + // Create a temporary directory for OCI layout - tmpDir, err := os.MkdirTemp("", "oci-layout-") + tmpDir, err := os.MkdirTemp(baseDir, "oci-layout-") if err != nil { log.Printf("Failed to create temporary directory for OCI layout: %s", err) return dockerfile, err @@ -229,8 +232,9 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { ociDir := filepath.Join(tmpDir, "oci") log.Printf("Copying image to local OCI layout using Skopeo: %s", image) - // Copy the image to local OCI layout using Skopeo - copyCmd := exec.Command("skopeo", "copy", image, "oci:"+ociDir) + // Copy the image to local OCI layout using Skopeo with override flags + copyCmd := exec.Command("skopeo", "copy", "--override-arch", "amd64", "--override-os", "linux", image, "oci:"+ociDir) + copyOutput, err := copyCmd.CombinedOutput() if err != nil { log.Printf("Failed to copy image with Skopeo: %s", err) @@ -238,10 +242,14 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { return dockerfile, err } + if err := adjustPermissions(ociDir); err != nil { + log.Printf("Failed to adjust permissions for directory %s: %s", ociDir, err) + } + log.Printf("Inspecting image to get layer SHAs using Skopeo") - // Inspect the image to get layer SHAs using Skopeo - inspectCmd := exec.Command("skopeo", "inspect", "--format", "{{json .Layers}}", "oci:"+ociDir) + // Inspect the image to get layer SHAs using Skopeo with override flags + inspectCmd := exec.Command("skopeo", "inspect", "--override-arch", "amd64", "--override-os", "linux", "--format", "{{json .Layers}}", "oci:"+ociDir) inspectOut, err := inspectCmd.Output() if err != nil { log.Printf("Failed to inspect image with Skopeo: %s", err) @@ -264,7 +272,7 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { layerFile := filepath.Join(ociDir, "blobs", "sha256", layerSHA) // Create a temporary directory for this layer - layerTmpDir, err := os.MkdirTemp("", "layer-") + layerTmpDir, err := os.MkdirTemp(baseDir, "layer-") if err != nil { log.Printf("Failed to create temporary directory for layer: %s", err) continue @@ -276,7 +284,7 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { if err != nil { log.Printf("Failed to extract layer with tar command: %s", err) log.Printf("Tar command output: %s", string(tarOutput)) - cleanUpTempDir(layerTmpDir) + adjustAndCleanDir(layerTmpDir) continue } @@ -293,7 +301,7 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { return nil }) if err != nil || foundDockerfilePath == "" { - cleanUpTempDir(layerTmpDir) + adjustAndCleanDir(layerTmpDir) continue } @@ -301,7 +309,7 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { content, err := os.ReadFile(foundDockerfilePath) if err != nil { log.Printf("Failed to read Dockerfile: %s", err) - cleanUpTempDir(layerTmpDir) + adjustAndCleanDir(layerTmpDir) continue } @@ -316,21 +324,30 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { }) } } - // Clean up the temporary directory for this layer - cleanUpTempDir(layerTmpDir) + adjustAndCleanDir(layerTmpDir) } return dockerfile, nil } -// cleanUpTempDir handles the cleanup of a temporary directory and logs any errors. -func cleanUpTempDir(dir string) { +func adjustAndCleanDir(dir string) { + // Adjust permissions if needed + if err := adjustPermissions(dir); err != nil { + log.Printf("Failed to adjust permissions for directory %s: %s", dir, err) + } + + // Clean up the directory if err := os.RemoveAll(dir); err != nil { - log.Printf("Failed to clean up temporary directory %s: %s", dir, err) + log.Printf("Failed to clean up directory %s: %s", dir, err) } } +func adjustPermissions(path string) error { + cmd := exec.Command("chmod", "-R", "ugo+rwx", path) + return cmd.Run() +} + func RunDockerManifestInspect(image string, containerEngine string) (DockerManifestInspect, error) { cmd := exec.Command(containerEngine, "manifest", "inspect", image) output, err := RunCommand(cmd) diff --git a/pkg/helpers_test.go b/pkg/helpers_test.go index b4c7ec2b..35f336e9 100644 --- a/pkg/helpers_test.go +++ b/pkg/helpers_test.go @@ -45,48 +45,48 @@ func TestRunSkopeoLayerExtractSuite(t *testing.T) { }, }, }, - //{ - // name: "Test3ScaleOperatorBundle", - // imageRef: "docker://registry.redhat.io/3scale-mas/3scale-rhel7-operator@sha256:0a6673eae2f0e8d95b919b0243e44d2c0383d13e2e616ac8d3f80742d496d292", - // expectedDockerfile: Dockerfile{ - // Commands: []DockerfileCommand{ - // {CommandType: "FROM", Value: "registry.redhat.io/devtools/go-toolset-rhel7:1.19.13-1.1697640714 AS builder"}, - // {CommandType: "ENV", Value: `PROJECT_NAME="3scale-operator"`}, - // {CommandType: "ENV", Value: `OUTPUT_DIR="/tmp/_output"`}, - // {CommandType: "ENV", Value: `BINARY_NAME="manager"`}, - // {CommandType: "ENV", Value: `BUILD_PATH="${REMOTE_SOURCE_DIR}/app"`}, - // {CommandType: "WORKDIR", Value: `${BUILD_PATH}`}, - // {CommandType: "COPY", Value: `$REMOTE_SOURCE $REMOTE_SOURCE_DIR`}, - // {CommandType: "ADD", Value: `patches /tmp/patches`}, - // {CommandType: "RUN", Value: `find /tmp/patches -type f -name '*.patch' -print0 | sort --zero-terminated | xargs -t -0 -n 1 patch --force -p1`}, - // {CommandType: "USER", Value: `root`}, - // {CommandType: "RUN", Value: `mkdir -p ${OUTPUT_DIR}`}, - // {CommandType: "RUN", Value: `echo "build path: ${BUILD_PATH}"`}, - // {CommandType: "RUN", Value: `echo "output path: ${OUTPUT_DIR}"`}, - // {CommandType: "RUN", Value: `scl enable go-toolset-1.19 "GOOS=linux GOARCH=$(scl enable go-toolset-1.19 'go env GOARCH') CGO_ENABLED=0 GO111MODULE=on go build -o ${OUTPUT_DIR}/${BINARY_NAME} main.go"`}, - // {CommandType: "RUN", Value: `mkdir ${OUTPUT_DIR}/licenses/`}, - // {CommandType: "RUN", Value: `cp "./licenses.xml" "${OUTPUT_DIR}/licenses/"`}, - // {CommandType: "FROM", Value: `registry.redhat.io/ubi7/ubi-minimal:7.9-1196`}, - // {CommandType: "LABEL", Value: `com.redhat.component="3scale-mas-operator-container" name="3scale-mas/3scale-rhel7-operator" version="1.17.0" summary="3scale Operator container image" description="Operator provides a way to install a 3scale API Management and ability to define 3scale API definitions." io.k8s.display-name="3scale Operator" io.openshift.expose-services="" io.openshift.tags="3scale, 3scale-amp, api" upstream_repo="https://github.com/3scale/3scale-operator" upstream_ref="a5d72cc78a29ce38f3c60761cd7d2afff0727feb" maintainer="eastizle@redhat.com"`}, - // {CommandType: "ENV", Value: `OPERATOR_BINARY_NAME="manager" USER_UID=1001 USER_NAME=3scale-operator`}, - // {CommandType: "USER", Value: `root`}, - // {CommandType: "COPY", Value: `--from=builder /tmp/_output/${OPERATOR_BINARY_NAME} /`}, - // {CommandType: "RUN", Value: `chown ${USER_UID} /${OPERATOR_BINARY_NAME}`}, - // {CommandType: "ENV", Value: `LICENSES_DIR="/root/licenses/3scale-operator/"`}, - // {CommandType: "RUN", Value: `mkdir -p ${LICENSES_DIR}`}, - // {CommandType: "COPY", Value: `--from=builder /tmp/_output/licenses/licenses.xml ${LICENSES_DIR}`}, - // {CommandType: "RUN", Value: `chown ${USER_UID} ${LICENSES_DIR}/licenses.xml`}, - // {CommandType: "ENTRYPOINT", Value: `["/manager"]`}, - // {CommandType: "USER", Value: `${USER_UID}`}, - // }, - // }, - //}, + { + name: "Test3ScaleOperatorBundle", + imageRef: "docker://registry.redhat.io/3scale-mas/3scale-rhel7-operator@sha256:0a6673eae2f0e8d95b919b0243e44d2c0383d13e2e616ac8d3f80742d496d292", + expectedDockerfile: Dockerfile{ + Commands: []DockerfileCommand{ + {CommandType: "FROM", Value: "registry.redhat.io/devtools/go-toolset-rhel7:1.19.13-1.1697640714 AS builder"}, + {CommandType: "ENV", Value: `PROJECT_NAME="3scale-operator"`}, + {CommandType: "ENV", Value: `OUTPUT_DIR="/tmp/_output"`}, + {CommandType: "ENV", Value: `BINARY_NAME="manager"`}, + {CommandType: "ENV", Value: `BUILD_PATH="${REMOTE_SOURCE_DIR}/app"`}, + {CommandType: "WORKDIR", Value: `${BUILD_PATH}`}, + {CommandType: "COPY", Value: `$REMOTE_SOURCE $REMOTE_SOURCE_DIR`}, + {CommandType: "ADD", Value: `patches /tmp/patches`}, + {CommandType: "RUN", Value: `find /tmp/patches -type f -name '*.patch' -print0 | sort --zero-terminated | xargs -t -0 -n 1 patch --force -p1`}, + {CommandType: "USER", Value: `root`}, + {CommandType: "RUN", Value: `mkdir -p ${OUTPUT_DIR}`}, + {CommandType: "RUN", Value: `echo "build path: ${BUILD_PATH}"`}, + {CommandType: "RUN", Value: `echo "output path: ${OUTPUT_DIR}"`}, + {CommandType: "RUN", Value: `scl enable go-toolset-1.19 "GOOS=linux GOARCH=$(scl enable go-toolset-1.19 'go env GOARCH') CGO_ENABLED=0 GO111MODULE=on go build -o ${OUTPUT_DIR}/${BINARY_NAME} main.go"`}, + {CommandType: "RUN", Value: `mkdir ${OUTPUT_DIR}/licenses/`}, + {CommandType: "RUN", Value: `cp "./licenses.xml" "${OUTPUT_DIR}/licenses/"`}, + {CommandType: "FROM", Value: `registry.redhat.io/ubi7/ubi-minimal:7.9-1196`}, + {CommandType: "LABEL", Value: `com.redhat.component="3scale-mas-operator-container" name="3scale-mas/3scale-rhel7-operator" version="1.17.0" summary="3scale Operator container image" description="Operator provides a way to install a 3scale API Management and ability to define 3scale API definitions." io.k8s.display-name="3scale Operator" io.openshift.expose-services="" io.openshift.tags="3scale, 3scale-amp, api" upstream_repo="https://github.com/3scale/3scale-operator" upstream_ref="a5d72cc78a29ce38f3c60761cd7d2afff0727feb" maintainer="eastizle@redhat.com"`}, + {CommandType: "ENV", Value: `OPERATOR_BINARY_NAME="manager" USER_UID=1001 USER_NAME=3scale-operator`}, + {CommandType: "USER", Value: `root`}, + {CommandType: "COPY", Value: `--from=builder /tmp/_output/${OPERATOR_BINARY_NAME} /`}, + {CommandType: "RUN", Value: `chown ${USER_UID} /${OPERATOR_BINARY_NAME}`}, + {CommandType: "ENV", Value: `LICENSES_DIR="/root/licenses/3scale-operator/"`}, + {CommandType: "RUN", Value: `mkdir -p ${LICENSES_DIR}`}, + {CommandType: "COPY", Value: `--from=builder /tmp/_output/licenses/licenses.xml ${LICENSES_DIR}`}, + {CommandType: "RUN", Value: `chown ${USER_UID} ${LICENSES_DIR}/licenses.xml`}, + {CommandType: "ENTRYPOINT", Value: `["/manager"]`}, + {CommandType: "USER", Value: `${USER_UID}`}, + }, + }, + }, // Additional tests can be added here } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Parallel() // Enable parallel execution of this subtest + //t.Parallel() // Do not enable parallel execution, these tests share skopeo and filesystem resources result, err := RunSkopeoLayerExtract(tt.imageRef) if err != nil { From 48fb8a3e8987078b039985617c95614f5d2eccff Mon Sep 17 00:00:00 2001 From: Brett Tofel Date: Sat, 9 Dec 2023 13:02:16 +0100 Subject: [PATCH 5/9] Dockerfile in AuditBundle, ENV,LABEL tests ~lax --- pkg/actions/get_bundle.go | 10 +- pkg/helpers.go | 86 +++++++++++++---- pkg/helpers_test.go | 188 ++++++++++++++++++++++++-------------- pkg/models/bundle.go | 4 +- 4 files changed, 196 insertions(+), 92 deletions(-) diff --git a/pkg/actions/get_bundle.go b/pkg/actions/get_bundle.go index 1bb68755..f3fcaa4b 100644 --- a/pkg/actions/get_bundle.go +++ b/pkg/actions/get_bundle.go @@ -81,15 +81,13 @@ func GetDataFromBundleImage(auditBundle *models.AuditBundle, auditBundle.BundleImageLabels = inspectManifest.DockerConfig.Labels } - dockerfile, err := pkg.RunSkopeoLayerExtract(auditBundle.OperatorBundleImagePath) + dockerfiles, err := pkg.RunSkopeoLayerExtract(auditBundle.OperatorBundleImagePath) if err != nil { - log.Printf("Error extracting Dockerfile: %s", err) + log.Printf("Error extracting Dockerfiles: %s", err) // Handle the error, e.g., by returning or continuing with other logic } else { - // Process the extracted Dockerfile commands - for _, cmd := range dockerfile.Commands { - log.Printf("Command: %s, Value: %s", cmd.CommandType, cmd.Value) - } + // Store the extracted Dockerfiles in the auditBundle + auditBundle.BundleDockerfiles = dockerfiles } // Read the bundle diff --git a/pkg/helpers.go b/pkg/helpers.go index 884259ab..f38084ac 100644 --- a/pkg/helpers.go +++ b/pkg/helpers.go @@ -15,6 +15,7 @@ package pkg import ( + "bufio" "bytes" "encoding/json" "errors" @@ -211,8 +212,63 @@ func RunDockerInspect(image string, containerEngine string) (DockerInspect, erro return dockerInspect[0], nil } -func RunSkopeoLayerExtract(image string) (Dockerfile, error) { - var dockerfile Dockerfile +func ParseDockerfile(content string) ([]DockerfileCommand, error) { + var commands []DockerfileCommand + scanner := bufio.NewScanner(strings.NewReader(content)) + var currentCommand string + var isContinuation bool + + for scanner.Scan() { + line := scanner.Text() + line = strings.TrimSpace(line) + + // Skip comments and empty lines + if strings.HasPrefix(line, "#") || line == "" { + continue + } + + // Check for line continuation + if strings.HasSuffix(line, "\\") { + currentCommand += line[:len(line)-1] + " " + isContinuation = true + continue + } else if isContinuation { + currentCommand += line + isContinuation = false + } else { + currentCommand = line + } + + // Special handling for ENV instructions + if strings.HasPrefix(currentCommand, "ENV ") { + envCommand := strings.TrimPrefix(currentCommand, "ENV ") + commands = append(commands, DockerfileCommand{ + CommandType: "ENV", + Value: envCommand, + }) + } else { + // Split command and arguments for other instructions + parts := strings.SplitN(currentCommand, " ", 2) + if len(parts) == 2 { + commands = append(commands, DockerfileCommand{ + CommandType: strings.ToUpper(parts[0]), + Value: parts[1], + }) + } + } + + currentCommand = "" + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return commands, nil +} + +func RunSkopeoLayerExtract(image string) ([]Dockerfile, error) { + var dockerfiles []Dockerfile // Specify a base directory you have full control over baseDir := "/tmp" // Update this path @@ -221,7 +277,7 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { tmpDir, err := os.MkdirTemp(baseDir, "oci-layout-") if err != nil { log.Printf("Failed to create temporary directory for OCI layout: %s", err) - return dockerfile, err + return dockerfiles, err } defer func() { if err := os.RemoveAll(tmpDir); err != nil { @@ -239,7 +295,7 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { if err != nil { log.Printf("Failed to copy image with Skopeo: %s", err) log.Printf("Skopeo copy command output: %s", string(copyOutput)) - return dockerfile, err + return dockerfiles, err } if err := adjustPermissions(ociDir); err != nil { @@ -253,7 +309,7 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { inspectOut, err := inspectCmd.Output() if err != nil { log.Printf("Failed to inspect image with Skopeo: %s", err) - return dockerfile, err + return dockerfiles, err } // Extract layer SHAs @@ -261,11 +317,13 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { err = json.Unmarshal(inspectOut, &layerSHAs) if err != nil { log.Printf("Failed to unmarshal layer SHAs: %s", err) - return dockerfile, err + return dockerfiles, err } // Process each layer for _, layerSHA := range layerSHAs { + var dockerfile Dockerfile + layerSHA = strings.TrimPrefix(layerSHA, "sha256:") // Construct the correct layer file path @@ -314,21 +372,17 @@ func RunSkopeoLayerExtract(image string) (Dockerfile, error) { } // Parse Dockerfile content - lines := strings.Split(string(content), "\n") - for _, line := range lines { - parts := strings.SplitN(line, " ", 2) - if len(parts) == 2 { - dockerfile.Commands = append(dockerfile.Commands, DockerfileCommand{ - CommandType: parts[0], - Value: parts[1], - }) - } + parsedCommands, err := ParseDockerfile(string(content)) + if err != nil { + log.Fatalf("Error parsing Dockerfile: %v", err) } + dockerfile.Commands = parsedCommands + dockerfiles = append(dockerfiles, dockerfile) // Clean up the temporary directory for this layer adjustAndCleanDir(layerTmpDir) } - return dockerfile, nil + return dockerfiles, nil } func adjustAndCleanDir(dir string) { diff --git a/pkg/helpers_test.go b/pkg/helpers_test.go index 35f336e9..c605a56a 100644 --- a/pkg/helpers_test.go +++ b/pkg/helpers_test.go @@ -1,83 +1,110 @@ package pkg import ( - "log" - "os/exec" "reflect" + "strings" // Import the "strings" package "testing" ) func TestRunSkopeoLayerExtractSuite(t *testing.T) { tests := []struct { - name string - imageRef string - expectedDockerfile Dockerfile + name string + imageRef string + expectedDockerfiles []Dockerfile }{ { name: "TestQuayOperatorBundle", imageRef: "docker://registry.redhat.io/quay/quay-operator-bundle@sha256:a97a63899d23e23d039ea36bd575c018d7b6295b7942b15a8bded52f09736bda", - expectedDockerfile: Dockerfile{ - Commands: []DockerfileCommand{ - {CommandType: "FROM", Value: "scratch"}, - {CommandType: "LABEL", Value: `com.redhat.delivery.operator.bundle=true`}, - {CommandType: "LABEL", Value: `com.redhat.delivery.openshift.ocp.versions="v4.8"`}, - {CommandType: "LABEL", Value: `com.redhat.openshift.versions="v4.8"`}, - {CommandType: "LABEL", Value: `com.redhat.delivery.backport=false`}, - {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.mediatype.v1=registry+v1`}, - {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.manifests.v1=manifests/`}, - {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.metadata.v1=metadata/`}, - {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.package.v1=quay-operator`}, - {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.channels.v1=stable-3.8`}, - {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.channel.default.v1=stable-3.8`}, - {CommandType: "LABEL", Value: `com.redhat.component="quay-operator-bundle-container"`}, - {CommandType: "LABEL", Value: `name="quay/quay-operator-bundle"`}, - {CommandType: "LABEL", Value: `summary="Quay Operator bundle container image"`}, - {CommandType: "LABEL", Value: `description="Operator bundle for Quay Operator"`}, - {CommandType: "LABEL", Value: `maintainer="Red Hat "`}, - {CommandType: "LABEL", Value: `version=v3.8.11`}, - {CommandType: "LABEL", Value: `io.k8s.display-name="Red Hat Quay Operator Bundle"`}, - {CommandType: "LABEL", Value: `io.openshift.tags="quay"`}, - {CommandType: "COPY", Value: `bundle/manifests/*.yaml /manifests/`}, - {CommandType: "COPY", Value: `bundle/manifests/metadata/annotations.yaml /metadata/annotations.yaml`}, - {CommandType: "LABEL", Value: `release=20`}, - {CommandType: "ADD", Value: `quay-operator-bundle-container-v3.8.11-20.json /root/buildinfo/content_manifests/quay-operator-bundle-container-v3.8.11-20.json`}, - {CommandType: "LABEL", Value: `"com.redhat.license_terms"="https://www.redhat.com/agreements" "distribution-scope"="public" "vendor"="Red Hat, Inc." "build-date"="2023-08-07T23:21:46" "architecture"="x86_64" "vcs-type"="git" "vcs-ref"="f6eb857b8bd8768d51a311bc274f53ce7856ae04" "io.k8s.description"="Operator bundle for Quay Operator" "url"="https://access.redhat.com/containers/#/registry.access.redhat.com/quay/quay-operator-bundle/images/v3.8.11-20"`}, + expectedDockerfiles: []Dockerfile{ + { + Commands: []DockerfileCommand{ + {CommandType: "FROM", Value: "scratch"}, + {CommandType: "LABEL", Value: `com.redhat.delivery.operator.bundle=true`}, + {CommandType: "LABEL", Value: `com.redhat.delivery.openshift.ocp.versions="v4.8"`}, + {CommandType: "LABEL", Value: `com.redhat.openshift.versions="v4.8"`}, + {CommandType: "LABEL", Value: `com.redhat.delivery.backport=false`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.mediatype.v1=registry+v1`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.manifests.v1=manifests/`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.metadata.v1=metadata/`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.package.v1=quay-operator`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.channels.v1=stable-3.8`}, + {CommandType: "LABEL", Value: `operators.operatorframework.io.bundle.channel.default.v1=stable-3.8`}, + {CommandType: "LABEL", Value: `com.redhat.component="quay-operator-bundle-container"`}, + {CommandType: "LABEL", Value: `name="quay/quay-operator-bundle"`}, + {CommandType: "LABEL", Value: `summary="Quay Operator bundle container image"`}, + {CommandType: "LABEL", Value: `description="Operator bundle for Quay Operator"`}, + {CommandType: "LABEL", Value: `maintainer="Red Hat "`}, + {CommandType: "LABEL", Value: `version=v3.8.11`}, + {CommandType: "LABEL", Value: `io.k8s.display-name="Red Hat Quay Operator Bundle"`}, + {CommandType: "LABEL", Value: `io.openshift.tags="quay"`}, + {CommandType: "COPY", Value: `bundle/manifests/*.yaml /manifests/`}, + {CommandType: "COPY", Value: `bundle/manifests/metadata/annotations.yaml /metadata/annotations.yaml`}, + {CommandType: "LABEL", Value: `release=20`}, + {CommandType: "ADD", Value: `quay-operator-bundle-container-v3.8.11-20.json /root/buildinfo/content_manifests/quay-operator-bundle-container-v3.8.11-20.json`}, + {CommandType: "LABEL", Value: `"com.redhat.license_terms"="https://www.redhat.com/agreements" "distribution-scope"="public" "vendor"="Red Hat, Inc." "build-date"="2023-08-07T23:21:46" "architecture"="x86_64" "vcs-type"="git" "vcs-ref"="f6eb857b8bd8768d51a311bc274f53ce7856ae04" "io.k8s.description"="Operator bundle for Quay Operator" "url"="https://access.redhat.com/containers/#/registry.access.redhat.com/quay/quay-operator-bundle/images/v3.8.11-20"`}, + }, }, }, }, { name: "Test3ScaleOperatorBundle", imageRef: "docker://registry.redhat.io/3scale-mas/3scale-rhel7-operator@sha256:0a6673eae2f0e8d95b919b0243e44d2c0383d13e2e616ac8d3f80742d496d292", - expectedDockerfile: Dockerfile{ - Commands: []DockerfileCommand{ - {CommandType: "FROM", Value: "registry.redhat.io/devtools/go-toolset-rhel7:1.19.13-1.1697640714 AS builder"}, - {CommandType: "ENV", Value: `PROJECT_NAME="3scale-operator"`}, - {CommandType: "ENV", Value: `OUTPUT_DIR="/tmp/_output"`}, - {CommandType: "ENV", Value: `BINARY_NAME="manager"`}, - {CommandType: "ENV", Value: `BUILD_PATH="${REMOTE_SOURCE_DIR}/app"`}, - {CommandType: "WORKDIR", Value: `${BUILD_PATH}`}, - {CommandType: "COPY", Value: `$REMOTE_SOURCE $REMOTE_SOURCE_DIR`}, - {CommandType: "ADD", Value: `patches /tmp/patches`}, - {CommandType: "RUN", Value: `find /tmp/patches -type f -name '*.patch' -print0 | sort --zero-terminated | xargs -t -0 -n 1 patch --force -p1`}, - {CommandType: "USER", Value: `root`}, - {CommandType: "RUN", Value: `mkdir -p ${OUTPUT_DIR}`}, - {CommandType: "RUN", Value: `echo "build path: ${BUILD_PATH}"`}, - {CommandType: "RUN", Value: `echo "output path: ${OUTPUT_DIR}"`}, - {CommandType: "RUN", Value: `scl enable go-toolset-1.19 "GOOS=linux GOARCH=$(scl enable go-toolset-1.19 'go env GOARCH') CGO_ENABLED=0 GO111MODULE=on go build -o ${OUTPUT_DIR}/${BINARY_NAME} main.go"`}, - {CommandType: "RUN", Value: `mkdir ${OUTPUT_DIR}/licenses/`}, - {CommandType: "RUN", Value: `cp "./licenses.xml" "${OUTPUT_DIR}/licenses/"`}, - {CommandType: "FROM", Value: `registry.redhat.io/ubi7/ubi-minimal:7.9-1196`}, - {CommandType: "LABEL", Value: `com.redhat.component="3scale-mas-operator-container" name="3scale-mas/3scale-rhel7-operator" version="1.17.0" summary="3scale Operator container image" description="Operator provides a way to install a 3scale API Management and ability to define 3scale API definitions." io.k8s.display-name="3scale Operator" io.openshift.expose-services="" io.openshift.tags="3scale, 3scale-amp, api" upstream_repo="https://github.com/3scale/3scale-operator" upstream_ref="a5d72cc78a29ce38f3c60761cd7d2afff0727feb" maintainer="eastizle@redhat.com"`}, - {CommandType: "ENV", Value: `OPERATOR_BINARY_NAME="manager" USER_UID=1001 USER_NAME=3scale-operator`}, - {CommandType: "USER", Value: `root`}, - {CommandType: "COPY", Value: `--from=builder /tmp/_output/${OPERATOR_BINARY_NAME} /`}, - {CommandType: "RUN", Value: `chown ${USER_UID} /${OPERATOR_BINARY_NAME}`}, - {CommandType: "ENV", Value: `LICENSES_DIR="/root/licenses/3scale-operator/"`}, - {CommandType: "RUN", Value: `mkdir -p ${LICENSES_DIR}`}, - {CommandType: "COPY", Value: `--from=builder /tmp/_output/licenses/licenses.xml ${LICENSES_DIR}`}, - {CommandType: "RUN", Value: `chown ${USER_UID} ${LICENSES_DIR}/licenses.xml`}, - {CommandType: "ENTRYPOINT", Value: `["/manager"]`}, - {CommandType: "USER", Value: `${USER_UID}`}, + expectedDockerfiles: []Dockerfile{ + { + Commands: []DockerfileCommand{ + {CommandType: "FROM", Value: "koji/image-build"}, + {CommandType: "LABEL", Value: `maintainer="Red Hat, Inc."`}, + {CommandType: "LABEL", Value: `com.redhat.component="ubi7-minimal-container"`}, + {CommandType: "LABEL", Value: `name="ubi7-minimal"`}, + {CommandType: "LABEL", Value: `version="7.9"`}, + {CommandType: "LABEL", Value: `com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI"`}, + {CommandType: "LABEL", Value: `summary="Provides the latest release of the minimal Red Hat Universal Base Image 7."`}, + {CommandType: "LABEL", Value: `description="The Universal Base Image Minimal is a stripped down image that uses microdnf as a package manager. This base image is freely redistributable, but Red Hat only supports Red Hat technologies through subscriptions for Red Hat products. This image is maintained by Red Hat and updated regularly."`}, + {CommandType: "LABEL", Value: `io.k8s.display-name="Red Hat Universal Base Image 7 Minimal"`}, + {CommandType: "LABEL", Value: `io.openshift.tags="minimal rhel7"`}, + {CommandType: "ENV", Value: `container oci`}, + {CommandType: "ENV", Value: `PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`}, + {CommandType: "CMD", Value: `["/bin/bash"]`}, + {CommandType: "RUN", Value: `rm -rf /var/log/*`}, + {CommandType: "LABEL", Value: `release=1196`}, + {CommandType: "ADD", Value: `ubi7-minimal-container-7.9-1196.json /root/buildinfo/content_manifests/ubi7-minimal-container-7.9-1196.json`}, + {CommandType: "LABEL", Value: `"distribution-scope"="public" "vendor"="Red Hat, Inc." "build-date"="2023-10-03T16:26:49" "architecture"="x86_64" "vcs-type"="git" "vcs-ref"="3c9aa520910e3198259ca894ee51c40b086a3e75" "io.k8s.description"="The Universal Base Image Minimal is a stripped down image that uses microdnf as a package manager. This base image is freely redistributable, but Red Hat only supports Red Hat technologies through subscriptions for Red Hat products. This image is maintained by Red Hat and updated regularly." "url"="https://access.redhat.com/containers/#/registry.access.redhat.com/ubi7-minimal/images/7.9-1196"`}, + }, + }, + { + Commands: []DockerfileCommand{ + {CommandType: "FROM", Value: "registry.redhat.io/devtools/go-toolset-rhel7:1.19.13-1.1697640714 AS builder"}, + {CommandType: "ENV", Value: `PROJECT_NAME="3scale-operator"`}, + {CommandType: "ENV", Value: `OUTPUT_DIR="/tmp/_output"`}, + {CommandType: "ENV", Value: `BINARY_NAME="manager"`}, + {CommandType: "ENV", Value: `BUILD_PATH="${REMOTE_SOURCE_DIR}/app"`}, + {CommandType: "WORKDIR", Value: `${BUILD_PATH}`}, + {CommandType: "COPY", Value: `$REMOTE_SOURCE $REMOTE_SOURCE_DIR`}, + {CommandType: "ADD", Value: `patches /tmp/patches`}, + {CommandType: "RUN", Value: `find /tmp/patches -type f -name '*.patch' -print0 | sort --zero-terminated | xargs -t -0 -n 1 patch --force -p1`}, + {CommandType: "USER", Value: `root`}, + {CommandType: "RUN", Value: `mkdir -p ${OUTPUT_DIR}`}, + {CommandType: "RUN", Value: `echo "build path: ${BUILD_PATH}"`}, + {CommandType: "RUN", Value: `echo "output path: ${OUTPUT_DIR}"`}, + {CommandType: "RUN", Value: `scl enable go-toolset-1.19 "GOOS=linux GOARCH=$(scl enable go-toolset-1.19 'go env GOARCH') CGO_ENABLED=0 GO111MODULE=on go build -o ${OUTPUT_DIR}/${BINARY_NAME} main.go"`}, + {CommandType: "RUN", Value: `mkdir ${OUTPUT_DIR}/licenses/`}, + {CommandType: "RUN", Value: `cp "./licenses.xml" "${OUTPUT_DIR}/licenses/"`}, + {CommandType: "FROM", Value: `registry.redhat.io/ubi7/ubi-minimal:7.9-1196`}, + {CommandType: "LABEL", Value: `com.redhat.component="3scale-mas-operator-container" name="3scale-mas/3scale-rhel7-operator" version="1.17.0" summary="3scale Operator container image" description="Operator provides a way to install a 3scale API Management and ability to define 3scale API definitions." io.k8s.display-name="3scale Operator" io.openshift.expose-services="" io.openshift.tags="3scale, 3scale-amp, api" upstream_repo="https://github.com/3scale/3scale-operator" upstream_ref="a5d72cc78a29ce38f3c60761cd7d2afff0727feb" maintainer="eastizle@redhat.com"`}, + {CommandType: "ENV", Value: `OPERATOR_BINARY_NAME="manager" USER_UID=1001 USER_NAME=3scale-operator`}, + {CommandType: "USER", Value: `root`}, + {CommandType: "COPY", Value: `--from=builder /tmp/_output/${OPERATOR_BINARY_NAME} /`}, + {CommandType: "RUN", Value: `chown ${USER_UID} /${OPERATOR_BINARY_NAME}`}, + {CommandType: "ENV", Value: `LICENSES_DIR="/root/licenses/3scale-operator/"`}, + {CommandType: "RUN", Value: `mkdir -p ${LICENSES_DIR}`}, + {CommandType: "COPY", Value: `--from=builder /tmp/_output/licenses/licenses.xml ${LICENSES_DIR}`}, + {CommandType: "RUN", Value: `chown ${USER_UID} ${LICENSES_DIR}/licenses.xml`}, + {CommandType: "ENTRYPOINT", Value: `["/manager"]`}, + {CommandType: "USER", Value: `${USER_UID}`}, + {CommandType: "LABEL", Value: `release=11`}, + {CommandType: "ADD", Value: `3scale-mas-operator-container-1.17.0-11.json /root/buildinfo/content_manifests/3scale-mas-operator-container-1.17.0-11.json`}, + {CommandType: "LABEL", Value: `"com.redhat.license_terms"="https://www.redhat.com/agreements" "distribution-scope"="public" "vendor"="Red Hat, Inc." "build-date"="2023-11-06T16:50:01" "architecture"="x86_64" "vcs-type"="git" "vcs-ref"="b92d1249215767318f28e419cb5f3d9c378d4b75" "io.k8s.description"="Operator provides a way to install a 3scale API Management and ability to define 3scale API definitions." "url"="https://access.redhat.com/containers/#/registry.access.redhat.com/3scale-mas/3scale-rhel7-operator/images/1.17.0-11"`}, + }, }, }, }, @@ -86,19 +113,44 @@ func TestRunSkopeoLayerExtractSuite(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - //t.Parallel() // Do not enable parallel execution, these tests share skopeo and filesystem resources + t.Parallel() // Allow to run concurrently + // Test execution code result, err := RunSkopeoLayerExtract(tt.imageRef) if err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - log.Printf("Skopeo command failed with exit code: %d", exitError.ExitCode()) - log.Printf("Stderr: %s", exitError.Stderr) - } t.Fatalf("RunSkopeoLayerExtract returned an error: %v", err) } - if !reflect.DeepEqual(result, tt.expectedDockerfile) { - t.Errorf("RunSkopeoLayerExtract() = %v, want %v", result, tt.expectedDockerfile) + if len(result) != len(tt.expectedDockerfiles) { + t.Errorf("Mismatch in the number of Dockerfiles: got %d, want %d", len(result), len(tt.expectedDockerfiles)) + return + } + + // Iterate through each Dockerfile + for i := range result { + // Iterate through each command in the Dockerfile + for j, cmd := range result[i].Commands { + if j < len(tt.expectedDockerfiles[i].Commands) { + // Handle LABEL and ENV commands with whitespace differences + if cmd.CommandType == "LABEL" || cmd.CommandType == "ENV" { + actualValue := strings.Join(strings.Fields(cmd.Value), " ") // Remove extra whitespace + expectedValue := strings.Join(strings.Fields(tt.expectedDockerfiles[i].Commands[j].Value), " ") + if actualValue != expectedValue { + t.Errorf("Mismatch at %s command %d in Dockerfile %d: got %s, want %s", cmd.CommandType, j, i, actualValue, expectedValue) + } + } else if !reflect.DeepEqual(cmd, tt.expectedDockerfiles[i].Commands[j]) { + t.Errorf("Mismatch at command %d in Dockerfile %d: got %v, want %v", j, i, cmd, tt.expectedDockerfiles[i].Commands[j]) + } + } else { + t.Errorf("Extra command at %d in Dockerfile %d: %v", j, i, cmd) + } + } + + if len(tt.expectedDockerfiles[i].Commands) > len(result[i].Commands) { + for j := len(result[i].Commands); j < len(tt.expectedDockerfiles[i].Commands); j++ { + t.Errorf("Missing expected command at %d in Dockerfile %d: %v", j, i, tt.expectedDockerfiles[i].Commands[j]) + } + } } }) } diff --git a/pkg/models/bundle.go b/pkg/models/bundle.go index b661f92c..17a5db6a 100644 --- a/pkg/models/bundle.go +++ b/pkg/models/bundle.go @@ -39,7 +39,7 @@ type AuditBundle struct { IsHeadOfChannel bool BundleImageLabels map[string]string `json:"bundleImageLabels,omitempty"` BundleAnnotations map[string]string `json:"bundleAnnotations,omitempty"` - BundleDockerfile pkg.Dockerfile + BundleDockerfiles []pkg.Dockerfile Errors []string } @@ -47,7 +47,7 @@ func NewAuditBundle(operatorBundleName, operatorBundleImagePath string) *AuditBu auditBundle := AuditBundle{ OperatorBundleName: operatorBundleName, OperatorBundleImagePath: operatorBundleImagePath, - BundleDockerfile: pkg.Dockerfile{Commands: []pkg.DockerfileCommand{}}, + BundleDockerfiles: []pkg.Dockerfile{}, } return &auditBundle From e05ba999f953e762a4fe45ec4f70bdf2a195fafc Mon Sep 17 00:00:00 2001 From: Brett Tofel Date: Sat, 9 Dec 2023 17:22:12 +0100 Subject: [PATCH 6/9] Ensure image paths have transport for Skopeo --- pkg/actions/get_bundle.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/actions/get_bundle.go b/pkg/actions/get_bundle.go index f3fcaa4b..a0f5e078 100644 --- a/pkg/actions/get_bundle.go +++ b/pkg/actions/get_bundle.go @@ -21,6 +21,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" // "strings" @@ -81,7 +82,13 @@ func GetDataFromBundleImage(auditBundle *models.AuditBundle, auditBundle.BundleImageLabels = inspectManifest.DockerConfig.Labels } - dockerfiles, err := pkg.RunSkopeoLayerExtract(auditBundle.OperatorBundleImagePath) + // Ensure the image path has the 'docker://' prefix + formattedImagePath := auditBundle.OperatorBundleImagePath + if !strings.HasPrefix(formattedImagePath, "docker://") { + formattedImagePath = "docker://" + formattedImagePath + } + + dockerfiles, err := pkg.RunSkopeoLayerExtract(formattedImagePath) if err != nil { log.Printf("Error extracting Dockerfiles: %s", err) // Handle the error, e.g., by returning or continuing with other logic From 40ec16d5f537f5d6ba57275d6dc573d4ffdc5793 Mon Sep 17 00:00:00 2001 From: Brett Tofel Date: Sat, 9 Dec 2023 17:22:34 +0100 Subject: [PATCH 7/9] Add dockerfile info to standard report --- pkg/reports/bundles/columns.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/reports/bundles/columns.go b/pkg/reports/bundles/columns.go index a6d270b2..26e768f8 100644 --- a/pkg/reports/bundles/columns.go +++ b/pkg/reports/bundles/columns.go @@ -51,6 +51,7 @@ type Column struct { BundleAnnotations map[string]string `json:"bundleAnnotations,omitempty"` BundleCSV *v1alpha1.ClusterServiceVersion `json:"csv,omitempty"` PropertiesFromDB []pkg.PropertiesAnnotation `json:"propertiesFromDB,omitempty"` + BundleDockerfiles []pkg.Dockerfile `json:"bundleDockerfiles,omitempty"` } func NewColumn(v models.AuditBundle) *Column { @@ -65,6 +66,7 @@ func NewColumn(v models.AuditBundle) *Column { col.BundleImageLabels = v.BundleImageLabels col.BundleAnnotations = v.BundleAnnotations col.PropertiesFromDB = v.PropertiesDB + col.BundleDockerfiles = v.BundleDockerfiles if v.Bundle != nil && v.Bundle.CSV != nil { col.BundleCSV = v.Bundle.CSV From 7579feb0e326f45576f1fd502415b06967d04e83 Mon Sep 17 00:00:00 2001 From: Brett Tofel Date: Mon, 11 Dec 2023 17:33:46 +0100 Subject: [PATCH 8/9] Note new prereqs for Dockerfile scraping --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d1ee79ea..f8927009 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ For further information about its motivation see the [EP Audit command operation - go 1.19 - docker or podman +- tar and skopeo (to scrape Dockerfile from image layer oci dirs -- common to all Red Hat built operators) - access to the registry where the index catalog and operator bundle images are distributed - access to a Kubernetes cluster - [operator-sdk][operator-sdk] installed >= `1.5.0 From 6cb0954d496c73b01af182d506863c2bb5c36942 Mon Sep 17 00:00:00 2001 From: Brett Tofel Date: Mon, 11 Dec 2023 17:34:08 +0100 Subject: [PATCH 9/9] Add integration tag to new skopeo tests --- pkg/helpers_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/helpers_test.go b/pkg/helpers_test.go index c605a56a..7f8443c4 100644 --- a/pkg/helpers_test.go +++ b/pkg/helpers_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + package pkg import (