From bc2c9dbfcd9a487452647e77520431b399ef77f3 Mon Sep 17 00:00:00 2001 From: Jonathan Gangi Date: Tue, 3 Dec 2024 10:07:41 -0300 Subject: [PATCH] feat(SPSTRAT-465): add task for marketplacesvm This commit introduces a new task named `marketplaces-push-disk-images` which will be used to deliver disk images to various cloud marketplaces using the `marketplacesvm_push_wrapper`. Signed-off-by: Jonathan Gangi --- schema/dataKeys.json | 49 +++++ tasks/collect-marketplacesvm-secret/README.md | 9 + .../collect-marketplacesvm-secret.yaml | 44 +++++ ...test-collect-marketplacesvm-no-secret.yaml | 53 ++++++ ...ct-marketplacesvm-secret-fail-no-data.yaml | 22 +++ .../test-collect-marketplacesvm-secret.yaml | 72 ++++++++ .../marketplacesvm-push-disk-images/README.md | 14 ++ .../marketplaces-push-disk-images.yaml | 147 +++++++++++++++ .../tests/mocks.sh | 31 ++++ .../tests/pre-apply-task-hook.sh | 11 ++ .../test-marketplacesvm-push-disk-images.yaml | 172 ++++++++++++++++++ 11 files changed, 624 insertions(+) create mode 100644 tasks/collect-marketplacesvm-secret/README.md create mode 100644 tasks/collect-marketplacesvm-secret/collect-marketplacesvm-secret.yaml create mode 100644 tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-no-secret.yaml create mode 100644 tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-secret-fail-no-data.yaml create mode 100644 tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-secret.yaml create mode 100644 tasks/marketplacesvm-push-disk-images/README.md create mode 100644 tasks/marketplacesvm-push-disk-images/marketplaces-push-disk-images.yaml create mode 100644 tasks/marketplacesvm-push-disk-images/tests/mocks.sh create mode 100755 tasks/marketplacesvm-push-disk-images/tests/pre-apply-task-hook.sh create mode 100644 tasks/marketplacesvm-push-disk-images/tests/test-marketplacesvm-push-disk-images.yaml diff --git a/schema/dataKeys.json b/schema/dataKeys.json index 892585352..9e76f38f9 100644 --- a/schema/dataKeys.json +++ b/schema/dataKeys.json @@ -437,6 +437,55 @@ } } }, + "productInfo": { + "type": "object", + "properties": { + "productName": { + "type": "string", + "description": "productName in content gateway" + }, + "productCode": { + "type": "string", + "description": "productCode in content gateway" + }, + "productVersionName": { + "type": "string", + "description": "productVersionName in content gateway" + } + } + }, + "starmap": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Artifact name" + }, + "workflow": { + "type": "string", + "description": "Push workflow" + }, + "cloud": { + "type": "string", + "description": "Cloud provider's name" + }, + "mappings": { + "type": "object", + "description": "Mappings for the given artifact" + }, + "billing-code-config": { + "type": "object", + "description": "Billing configuration for the community worklow" + } + } + } + }, + "cloudMarketplacesSecret": { + "type": "string", + "description": "Secret for cloud marketplaces" + }, "pushSourceContainer": { "type": "boolean", "description": "Indicates if the source container should be pushed" diff --git a/tasks/collect-marketplacesvm-secret/README.md b/tasks/collect-marketplacesvm-secret/README.md new file mode 100644 index 000000000..3bc9bcbe4 --- /dev/null +++ b/tasks/collect-marketplacesvm-secret/README.md @@ -0,0 +1,9 @@ +# collect-marketplacesvm-secret + +Tekton task that collects the secret for the cloud marketplaces from the data file + +## Parameters + +| Name | Description | Optional | Default value | +|--------------|------------------------------------------------------------------|----------|---------------| +| dataPath | Path to the merged data JSON file generated by collect-data task | No | - | diff --git a/tasks/collect-marketplacesvm-secret/collect-marketplacesvm-secret.yaml b/tasks/collect-marketplacesvm-secret/collect-marketplacesvm-secret.yaml new file mode 100644 index 000000000..322721ef9 --- /dev/null +++ b/tasks/collect-marketplacesvm-secret/collect-marketplacesvm-secret.yaml @@ -0,0 +1,44 @@ +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: collect-marketplaces-secret + labels: + app.kubernetes.io/version: "0.1.0" + annotations: + tekton.dev/pipelines.minVersion: "0.12.1" + tekton.dev/tags: release +spec: + description: >- + Tekton task that collects the secret for the cloud marketplaces from the data file + params: + - name: dataPath + type: string + description: Path to the merged data JSON file generated by collect-data task + workspaces: + - name: data + description: The workspace where the data json file resides + results: + - name: cloudMarketplacesSecret + type: string + description: "The base64 encoded secret to use for various cloud marketplaces." + steps: + - name: collect-marketplacesvm-secret + image: + quay.io/konflux-ci/release-service-utils:7f7a156a835c773bbcd7e5d7e44df2f573db14f2 + script: | + #!/usr/bin/env bash + set -eux + + DATA_FILE="$(workspaces.data.path)/$(params.dataPath)" + if [ ! -f "${DATA_FILE}" ] ; then + echo "No valid data file was provided." + exit 1 + fi + + if [ "$(jq '.mapping | has("cloudMarketplacesSecret")' "$DATA_FILE")" == false ] ; then + echo "Marketplaces secret missing in data JSON file" + exit 1 + fi + + jq -j '.mapping.cloudMarketplacesSecret' "$DATA_FILE" | tee "$(results.cloudMarketplacesSecret.path)" diff --git a/tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-no-secret.yaml b/tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-no-secret.yaml new file mode 100644 index 000000000..fe492a443 --- /dev/null +++ b/tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-no-secret.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: test-collect-marketplaces-secret-fail-no-secret + annotations: + test/assert-task-failure: "run-task" +spec: + description: | + Run the collect-marketplaces-secret task with no secret in the data file and + verify the task fails as expected + workspaces: + - name: tests-workspace + tasks: + - name: setup + workspaces: + - name: data + workspace: tests-workspace + taskSpec: + workspaces: + - name: data + steps: + - name: setup-values + image: quay.io/konflux-ci/release-service-utils:7f7a156a835c773bbcd7e5d7e44df2f573db14f2 + script: | + #!/usr/bin/env sh + set -eux + + cat > $(workspaces.data.path)/data.json << EOF + { + "mapping": { + "components": [ + { + "name": "mycomponent" + } + ], + "defaults": { + "public": true + } + } + } + EOF + - name: run-task + taskRef: + name: collect-marketplaces-secret + params: + - name: dataPath + value: data.json + workspaces: + - name: data + workspace: tests-workspace + runAfter: + - setup diff --git a/tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-secret-fail-no-data.yaml b/tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-secret-fail-no-data.yaml new file mode 100644 index 000000000..7a7d7d054 --- /dev/null +++ b/tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-secret-fail-no-data.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: test-collect-marketplaces-secret-fail-no-data + annotations: + test/assert-task-failure: "run-task" +spec: + description: | + Run the test-collect-marketplaces-secret task with no data file and verify the taks fails as expected + workspaces: + - name: tests-workspace + tasks: + - name: run-task + taskRef: + name: collect-marketplaces-secret + params: + - name: dataPath + value: data.json + workspaces: + - name: data + workspace: tests-workspace diff --git a/tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-secret.yaml b/tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-secret.yaml new file mode 100644 index 000000000..90ab7b84b --- /dev/null +++ b/tasks/collect-marketplacesvm-secret/tests/test-collect-marketplacesvm-secret.yaml @@ -0,0 +1,72 @@ +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: test-collect-marketplaces-secret +spec: + description: | + Run the collect-marketplaces-secret task with the secret required and verify that + it will return the secret string. + workspaces: + - name: tests-workspace + tasks: + - name: setup + workspaces: + - name: data + workspace: tests-workspace + taskSpec: + workspaces: + - name: data + steps: + - name: setup-values + image: quay.io/konflux-ci/release-service-utils:7f7a156a835c773bbcd7e5d7e44df2f573db14f2 + script: | + #!/usr/bin/env sh + set -eux + + cat > $(workspaces.data.path)/data.json << EOF + { + "mapping": { + "components": [ + { + "name": "mycomponent1" + }, + { + "name": "mycomponent2", + "public": true + } + ], + "defaults": {}, + "cloudMarketplacesSecret": "eyJ0ZXN0Ijoic2VjcmV0In0K" + } + } + EOF + - name: run-task + taskRef: + name: collect-marketplaces-secret + params: + - name: dataPath + value: data.json + workspaces: + - name: data + workspace: tests-workspace + runAfter: + - setup + - name: check-result + params: + - name: secret + value: $(tasks.run-task.results.cloudMarketplacesSecret) + taskSpec: + params: + - name: secret + steps: + - name: check-result + image: quay.io/konflux-ci/release-service-utils:7f7a156a835c773bbcd7e5d7e44df2f573db14f2 + env: + - name: "SECRET" + value: '$(params.secret)' + script: | + #!/usr/bin/env sh + set -eux + + test "$SECRET" = "eyJ0ZXN0Ijoic2VjcmV0In0K" diff --git a/tasks/marketplacesvm-push-disk-images/README.md b/tasks/marketplacesvm-push-disk-images/README.md new file mode 100644 index 000000000..4fc8a7a7e --- /dev/null +++ b/tasks/marketplacesvm-push-disk-images/README.md @@ -0,0 +1,14 @@ +# marketplaces-push-disk-images + +Tekton Task to publish VM disk images into various cloud marketplaces using `pubtools-marketplacesvm`. + +It currently supports images in `raw` and `vhd` formats for `AWS` and `Azure` respectively. + +## Parameters + +| Name | Description | Optional | Default value | +| ----------------------- | -------------------------------------------------------------------------------------- | -------- | --------------- | +| snapshotPath | Path to the JSON string of the mapped snapshot spec in the data workspace. | No | - | +| dataPath | Path to the data JSON in the workspace containing the marketplacesvm options to use. | No | - | +| cloudMarketplacesSecret | Env specific secret containing the marketplaces credentials. | No | - | +| concurrentLimit | The maximum number of images to be pulled at once. | Yes | 3 | diff --git a/tasks/marketplacesvm-push-disk-images/marketplaces-push-disk-images.yaml b/tasks/marketplacesvm-push-disk-images/marketplaces-push-disk-images.yaml new file mode 100644 index 000000000..9de7e424e --- /dev/null +++ b/tasks/marketplacesvm-push-disk-images/marketplaces-push-disk-images.yaml @@ -0,0 +1,147 @@ +--- +apiVersion: tekton.dev/v1 +kind: Task +metadata: + name: marketplaces-push-disk-images + labels: + app.kubernetes.io/version: "0.1.0" + annotations: + tekton.dev/pipelines.minVersion: "0.12.1" + tekton.dev/tags: release +spec: + description: >- + Tekton task to push disk images to Cloud Marketplaces + params: + - name: snapshotPath + type: string + description: | + Path to the JSON string of the mapped snapshot spec in the data workspace. + It must be processed by the "apply-mapping" task first. + - name: dataPath + description: Path to the JSON string of the merged data to use in the data workspace + - name: cloudMarketplacesSecret + type: string + description: Env specific secret containing the marketplaces credentials. + - name: concurrentLimit + type: string + description: The maximum number of images to be pulled at once. + default: 3 + workspaces: + - name: data + description: The workspace where the snapshot spec json file resides + steps: + - name: pull-and-push-images-to-marketplaces + image: quay.io/konflux-ci/release-service-utils:7f7a156a835c773bbcd7e5d7e44df2f573db14f2 + env: + - name: CLOUD_CREDENTIALS + valueFrom: + secretKeyRef: + name: $(params.cloudMarketplacesSecret) + key: key + script: | + #!/usr/bin/env bash + set -ex + + # Setup required variables + set +x + SNAPSHOT_JSON=$(jq -c '.' "$(workspaces.data.path)/$(params.snapshotPath)") + PRODUCT_INFO=$(jq -c '.mapping.productInfo' "$(workspaces.data.path)/$(params.dataPath)") + STARMAP_MAPPING=$(jq -c '.mapping.starmap' "$(workspaces.data.path)/$(params.dataPath)") + STARMAP_MAPPING_FILE=$(workspaces.data.path)/starmap.yaml + yq -p json -o yaml <<< "$STARMAP_MAPPING" > "$STARMAP_MAPPING_FILE" + set -x + + BASE_DIR="$(mktemp -d)" + DISK_IMGS_DIR="${BASE_DIR}/starmap/CLOUD_IMAGES" + mkdir -p "${DISK_IMGS_DIR}" + + RUNNING_JOBS="\j" # Bash parameter for number of jobs currently running + NUM_COMPONENTS=$(jq '.components | length' <<< "$SNAPSHOT_JSON") + + process_component() { # Expected argument is [component json] + COMPONENT=$1 + PULLSPEC=$(jq -er '.containerImage' <<< "${COMPONENT}") + IMG_NAME=$(jq -er '.name' <<< "${COMPONENT}") + BUILD_NAME=$(jq -er '.productCode' <<< "${PRODUCT_INFO}") + BUILD_VERSION=$(jq -er '.productVersionName' <<< "${PRODUCT_INFO}") + BUILD_ARCH=$(jq -er '.staged.files[0].filename' <<< "${COMPONENT}") + BUILD_ARCH=${BUILD_ARCH%\.*} # Rstrip on . to remove the extension + BUILD_ARCH=${BUILD_ARCH##*-} # Lstrip on - on get the arch + RESOURCES_JSON=' + { + "api": "v1", + "resource": "CloudImage", + "description": "", + "boot_mode": "hybrid", + "build": {}, + "images": [] + }' + RESOURCES_JSON=$(jq -c \ + --arg build_name "$BUILD_NAME" \ + --arg build_arch "$BUILD_ARCH" \ + --arg build_version "$BUILD_VERSION" \ + '.build.name=$build_name | + .build.arch=$build_arch | + .build.version=$build_version' <<< "$RESOURCES_JSON" + ) + DESTINATION="${DISK_IMGS_DIR}/${IMG_NAME}" + mkdir -p "${DESTINATION}" + DOWNLOAD_DIR=$(mktemp -d) + cd "$DOWNLOAD_DIR" + # oras has very limited support for selecting the right auth entry, + # so create a custom auth file with just one entry + AUTH_FILE=$(mktemp) + select-oci-auth "${PULLSPEC}" > "$AUTH_FILE" + oras pull --registry-config "$AUTH_FILE" "$PULLSPEC" + NUM_MAPPED_FILES=$(jq '.staged.files | length' <<< "${COMPONENT}") + for ((i = 0; i < NUM_MAPPED_FILES; i++)); do + FILE=$(jq -c --arg i "$i" '.staged.files[$i|tonumber]' <<< "$COMPONENT") + SOURCE=$(jq -er '.source' <<< "$FILE") + FILENAME=$(jq -er '.filename' <<< "$FILE") + if [ -f "${SOURCE}.gz" ]; then + gzip -d "${SOURCE}.gz" + fi + if [ -f "${DESTINATION}/${FILENAME}" ]; then + echo -n "Multiple files use the same destination value: $DESTINATION" >&2 + echo " and filename value: $FILENAME. Failing..." >&2 + exit 1 + fi + if [ "${FILENAME##*\.}" = "vhd" ]; then + image_type="VHD" + elif [ "${FILENAME##*\.}" = "raw" ]; then + image_type="AMI" + else + continue + fi + mv "$SOURCE" "${DESTINATION_FILE}" || echo "didn't find mapped file: ${SOURCE}" + RESOURCES_JSON=$(jq --arg filename "$FILENAME" \ + '.images[.images | length] = {"path": $filename, "architecture": "$arch"}' <<< "$RESOURCES_JSON") + RESOURCES_JSON=$(jq --arg image_type "$image_type" \ + '.type = "$image_type"' <<< "$RESOURCES_JSON") + done + echo "$RESOURCES_JSON" | yq -P -I 4 > "$DESTINATION/resources.yaml" + } + + # Process each component in parallel + for ((i = 0; i < NUM_COMPONENTS; i++)); do + COMPONENT=$(jq -c --arg i "$i" '.components[$i|tonumber]' <<< "$SNAPSHOT_JSON") + # Limit batch size to concurrent limit + while (( ${RUNNING_JOBS@P} >= $(params.concurrentLimit) )); do + wait -n + done + process_component "$COMPONENT" & + done + + # Wait for remaining processes to finish + while (( ${RUNNING_JOBS@P} > 0 )); do + wait -n + done + + # Change to the base directory + cd "${BASE_DIR}" + + # Validate the staged structure using pushsource-ls + pushsource-ls "staged:${BASE_DIR}" + + # Process the push + marketplacesvm_push_wrapper --debug --source "${BASE_DIR}" --starmap-file "$STARMAP_MAPPING_FILE" diff --git a/tasks/marketplacesvm-push-disk-images/tests/mocks.sh b/tasks/marketplacesvm-push-disk-images/tests/mocks.sh new file mode 100644 index 000000000..8678c8d35 --- /dev/null +++ b/tasks/marketplacesvm-push-disk-images/tests/mocks.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -ex + +# mocks to be injected into task step scripts +function select-oci-auth() { + echo Mock select-oci-auth called with: $* + echo $* > "$(workspaces.data.path)/mock_select-oci-auth.txt" +} + +function oras() { + echo Mock oras called with: $* + echo $* > "$(workspaces.data.path)/mock_oras.txt" + + if [[ "$*" != "pull --registry-config"* ]]; then + echo Error: Unexpected call to oras + exit 1 + fi +} + +function marketplacesvm_push_wrapper() { + echo Mock imarketplacesvm_push_wrapper called with: $* + echo $* > "$(workspaces.data.path)/mock_wrapper.txt" + + /home/pubtools-marketplacesvm-wrapper/marketplacesvm_push_wrapper "$@" --dry-run + + if ! [[ "$?" -eq 0 ]]; then + echo Unexpected call to marketplacesvm_push_wrapper + exit 1 + fi +} + diff --git a/tasks/marketplacesvm-push-disk-images/tests/pre-apply-task-hook.sh b/tasks/marketplacesvm-push-disk-images/tests/pre-apply-task-hook.sh new file mode 100755 index 000000000..ee20b9765 --- /dev/null +++ b/tasks/marketplacesvm-push-disk-images/tests/pre-apply-task-hook.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +TASK_PATH="$1" +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +# Add mocks to the beginning of task step script +yq -i '.spec.steps[0].script = load_str("'$SCRIPT_DIR'/mocks.sh") + .spec.steps[0].script' "$TASK_PATH" + +# Create a dummy secret (and delete it first if it exists) +kubectl delete secret marketplacesvm-test-secret --ignore-not-found +kubectl create secret generic marketplacesvm-test-secret --from-literal=key=eyJ0ZXN0Ijoic2VjcmV0In0K diff --git a/tasks/marketplacesvm-push-disk-images/tests/test-marketplacesvm-push-disk-images.yaml b/tasks/marketplacesvm-push-disk-images/tests/test-marketplacesvm-push-disk-images.yaml new file mode 100644 index 000000000..fb29922c1 --- /dev/null +++ b/tasks/marketplacesvm-push-disk-images/tests/test-marketplacesvm-push-disk-images.yaml @@ -0,0 +1,172 @@ +--- +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: test-marketplacesvm-push-disk-images +spec: + description: | + Run the marketplaces-push-disk-images task with valid data to ensure it succeeds + workspaces: + - name: tests-workspace + tasks: + - name: setup + taskSpec: + steps: + - name: setup + image: quay.io/konflux-ci/release-service-utils:7f7a156a835c773bbcd7e5d7e44df2f573db14f2 + script: | + #!/usr/bin/env bash + set -eux + + cat > "$(workspaces.data.path)/snapshot_spec.json" << EOF + { + "application": "amd-bootc-1-3-raw-disk-image", + "artifacts": {}, + "components": [ + { + "containerImage": "quay.io/workload/tenant/test-product/amd-bootc-1-3-raw-disk-image@sha256:123456", + "name": "amd-bootc-1-3-raw-disk-image", + "source": { + "git": { + "revision": "1abbfcdbc1c5e8b7aba07673297237ed192b50e2", + "url": "https://gitlab.com/konflux/test-product/disk-images/amd-bootc" + } + } + "staged": { + "destination": "test-product-1_DOT_3-x86_64-isos", + "files": [ + { + "filename": "test-product-amd-1.3-1732045201-x86_64.raw", + "source": "disk.raw" + } + ], + "version": "1.3" + } + } + ] + } + EOF + + cat > "$(workspaces.data.path)/data.json" << EOF + { + "mapping": { + "productInfo": { + "filePrefix": "test-product-amd-1.3", + "productCode": "TESTPRDOCUT", + "productName": "Test Product", + "productVersionName": "1.3" + }, + "starmap": [ + { + "cloud": "aws", + "mappings": { + "aws-emea": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "00000000-0000-0000-0000-000000000000", + "overwrite": false, + "restrict_version": true + } + ], + "provider": null + }, + "aws-na": { + "destinations": [ + { + "architecture": "x86_64", + "destination": "ffffffff-ffff-ffff-ffff-ffffffffffff", + "overwrite": false, + "restrict_version": true + } + ], + "provider": null + } + }, + "name": "test-product", + "workflow": "stratosphere" + } + ], + "components": [ + { + "name": "MyComponentName" + } + ] + } + } + EOF + workspaces: + - name: data + workspace: tests-workspace + + - name: run-task + taskRef: + name: marketplaces-push-disk-images + params: + - name: snapshotPath + value: "snapshot_spec.json" + - name: dataPath + value: "data.json" + - name: cloudMarketplacesSecret + value: test-secret + runAfter: + - setup + workspaces: + - name: data + workspace: tests-workspace + + - name: check-result + params: + - name: result + value: $(tasks.run-task.results.result) + workspaces: + - name: data + workspace: tests-workspace + runAfter: + - run-task + taskSpec: + workspaces: + - name: data + steps: + - name: check-result + image: quay.io/konflux-ci/release-service-utils:7f7a156a835c773bbcd7e5d7e44df2f573db14f2 + script: | + #!/usr/bin/env bash + set -ex + + STARMAP_EXPECTED_DATA=$(jq -c '.mapping.starmap' "$(workspaces.data.path)/data.json") + TASK_STARMAP_DATA=$(yq -p yaml -o json < "$(workspaces.data.path)/starmap.yaml") + TASK_STARMAP_DATA=$(jq -c '.' <<< "$TASK_STARMAP_DATA") + if [[ "$TASK_STARMAP_DATA" != "$STARMAP_EXPECTED_DATA" ]]; then + echo "Execution failed: Mismatch on expected StArMap data." + echo "Got: $TASK_STARMAP_DATA" + echo "Expected: $STARMAP_EXPECTED_DATA" + exit 1 + fi + + # Single component fixture: Expected "process_component" to be called once + expected_pullspec="quay.io/workload/tenant/test-product/amd-bootc-1-3-raw-disk-image@sha256:123456" + if [[ $(wc -l < "$(workspaces.data.path)/mock_select-oci-auth.txt") != 1 ]]; then + echo "Error: select-oci-auth was expected to be called a 1 time. Actual calls:" + cat "$(workspaces.data.path)/mock_select-oci-auth.txt" + exit 1 + elif [[ $(<"$(workspaces.data.path)/mock_select-oci-auth.txt") != "$expected_pullspec" ]]; then + echo "Error: select-oci-auth was expected to be called with $expected_pullspec. Actual: " + cat "$(workspaces.data.path)/mock_select-oci-auth.txt" + exit 1 + fi + if [[ $(wc -l < "$(workspaces.data.path)/mock_oras.txt") != 1 ]]; then + echo "Error: oras was expected to be called a 1 time. Actual calls:" + cat "$(workspaces.data.path)/mock_oras.txt" + exit 1 + elif [[ $(<"$(workspaces.data.path)/mock_oras.txt") != *"$expected_pullspec" ]]; then + echo "Error: oras was expected to be called pullspec $expected_pullspec. Actual: " + cat "$(workspaces.data.path)/mock_oras.txt" + exit 1 + fi + + if [[ $(wc -l < "$(workspaces.data.path)/mock_wrapper.txt") != 1 ]]; then + echo "Error: wrapper was expected to be called a 1 time. Actual calls:" + cat "$(workspaces.data.path)/mock_wrapper.txt" + exit 1 + fi