From 589eb2e42f2686b8083e8eb51fc3a8c361fa2538 Mon Sep 17 00:00:00 2001 From: Anas Husseini Date: Thu, 7 Nov 2024 08:35:44 +0200 Subject: [PATCH] Revert "Merge branch 'main' into ROCKS-1224/support-deprecated-channels-tags" This reverts commit 353789eb327b78e78ce0bf44a4089e36def92778, reversing changes made to 22b17eba3fc1eeb056474f7690fa299603ea402a. --- .github/actions/checkout/action.yaml | 52 --- .github/actions/validate-actor/action.yaml | 38 --- .../validate-actor/test-validate-actor.bats | 46 --- .../actions/validate-actor/validate-actor.sh | 36 -- .github/base_digests/20.04 | 2 +- .github/base_digests/22.04 | 2 +- .github/base_digests/24.04 | 2 - .github/workflows/Announcements.yaml | 9 +- .github/workflows/Build-Rock.yaml | 315 ++++++++---------- .github/workflows/Documentation.yaml | 22 +- .github/workflows/Image.yaml | 240 ++++--------- .github/workflows/Release.yaml | 26 +- .github/workflows/Tests.yaml | 21 +- .github/workflows/Vulnerability-Scan.yaml | 118 +------ .github/workflows/_Auto-updates.yaml | 4 +- .github/workflows/_CLI-Client.yaml | 37 -- .github/workflows/_Test-OCI-Factory.yaml | 85 +---- .gitignore | 4 +- CODEOWNERS | 1 - README.md | 26 +- assets/img/oci-factory-workflow.png | Bin 116699 -> 111218 bytes .../mock-rock/1.0}/rockcraft.yaml | 12 +- examples/mock-rock/1.1/rockcraft.yaml | 15 + examples/mock-rock/1.2/rockcraft.yaml | 16 + oci/alertmanager/_releases.json | 56 ++-- oci/hydra/.trivyignore | 8 - oci/hydra/_releases.json | 17 - oci/hydra/contacts.yaml | 7 - oci/hydra/documentation.yaml | 48 --- oci/hydra/image.yaml | 12 - oci/identity-platform-admin-ui/_releases.json | 44 --- oci/identity-platform-admin-ui/contacts.yaml | 7 - .../documentation.yaml | 139 -------- oci/identity-platform-admin-ui/image.yaml | 12 - oci/identity-platform-login-ui/contacts.yaml | 7 - .../documentation.yaml | 54 --- oci/identity-platform-login-ui/image.yaml | 12 - oci/kafka/_releases.json | 2 +- oci/kratos/.trivyignore | 10 - oci/kratos/_releases.json | 17 - oci/kratos/contacts.yaml | 7 - oci/kratos/documentation.yaml | 67 ---- oci/kratos/image.yaml | 12 - oci/loki/_releases.json | 96 +++--- oci/mimir/_releases.json | 112 +++---- oci/mimir/image.yaml | 14 +- oci/mock-rock/_releases.json | 20 +- oci/mock-rock/image.yaml | 18 +- oci/prometheus-pushgateway/_releases.json | 72 ++-- oci/prometheus/.trivyignore | 14 - oci/prometheus/_releases.json | 172 ++++------ oci/prometheus/image.yaml | 24 +- oci/python/_releases.json | 34 +- oci/python/image.yaml | 20 +- oci/tempo/_releases.json | 34 +- oci/tempo/image.yaml | 14 +- oci/zookeeper/_releases.json | 2 +- src/build_rock/assemble_rock/assemble.sh | 54 --- src/build_rock/assemble_rock/requirements.sh | 6 - .../configure/generate_build_matrix.py | 120 ------- src/build_rock/configure/requirements.txt | 2 - src/build_rock/lpci_build/lpci_build.sh | 58 ---- src/docs/generate_oci_doc_yaml.py | 4 +- src/docs/schema/triggers.py | 1 - src/image/define_image_revision.sh | 2 +- .../prepare_single_image_build_matrix.py | 47 +-- src/image/requirements.sh | 7 +- src/image/requirements.txt | 4 +- src/image/utils/schema/triggers.py | 17 - src/shared/github_output.py | 52 --- src/shared/release_info.py | 7 +- src/tests/get_released_revisions.py | 8 +- src/uploads/infer_image_track.py | 70 ++-- src/uploads/preempt_swift_slots.sh | 22 -- src/uploads/swift_lockfile_lock.sh | 38 --- src/uploads/swift_lockfile_unlock.sh | 17 - src/uploads/upload_to_swift.sh | 4 +- {tools => src}/workflow-engine/README.md | 0 .../charms/temporal-worker/README.md | 2 +- .../charms/temporal-worker/charm.tf | 0 .../temporal-worker/config.yml.template | 1 - .../temporal-worker/oci_factory/.env.template | 3 - .../oci_factory/activities}/__init__.py | 0 .../activities/activity_consume_events.py | 45 +-- .../activities/consumer}/__init__.py | 0 .../oci_factory/activities/consumer/ca.crt | 30 ++ .../oci_factory/activities/consumer/config.py | 0 .../oci_factory/activities/consumer/schema.py | 0 .../schemas/bo.schema.avro.Architecture.avsc | 0 .../schemas/bo.schema.avro.Artefact.avsc | 0 .../schemas/bo.schema.avro.Layer.avsc | 0 .../consumer/schemas/bo.schema.avro.Oci.avsc | 0 .../activities/consumer/schemas/example.json | 0 .../activities/find_images_to_update.py | 18 +- .../workflows/consume_events_workflow.py | 0 .../charms/temporal-worker/pyproject.toml | 2 +- .../charms/temporal-worker/variables.tf | 0 tests/__init__.py | 3 - tests/data/junit_xml_failure.xml | 18 - tests/etc/requirements.txt | 2 - tests/fixtures/buffers.py | 22 -- tests/fixtures/sample_data.py | 26 -- .../test_convert_junit_xml_to_markdown.py | 18 - .../test_junit_to_markdown_output.py | 18 - tests/unit/__init__.py | 0 tests/unit/test_generate_build_matrix.py | 78 ----- tests/unit/test_github_output.py | 49 --- .../unit/test_junit_to_markdown_formatting.py | 148 -------- tools/cli-client/README.md | 68 ---- tools/cli-client/cmd/oci-factory/main.go | 14 - tools/cli-client/go.mod | 43 --- tools/cli-client/go.sum | 165 --------- tools/cli-client/internals/cli/cli_main.go | 79 ----- tools/cli-client/internals/cli/cli_upload.go | 157 --------- .../internals/cli/cli_upload_test.go | 171 ---------- tools/cli-client/internals/cli/export_test.go | 6 - tools/cli-client/internals/cli/validator.go | 18 - .../internals/cli/validator_test.go | 34 -- tools/cli-client/internals/client/client.go | 62 ---- .../internals/client/client_test.go | 80 ----- .../internals/client/export_test.go | 13 - .../internals/client/wf_dispatcher.go | 66 ---- .../internals/client/wf_dispatcher_test.go | 71 ---- .../cli-client/internals/client/wf_poller.go | 197 ----------- .../internals/client/wf_poller_test.go | 39 --- tools/cli-client/internals/logger/logger.go | 128 ------- tools/cli-client/internals/token/token.go | 50 --- .../cli-client/internals/token/token_test.go | 26 -- .../internals/trigger/build_metadata.go | 103 ------ .../internals/trigger/build_metadata_test.go | 67 ---- tools/cli-client/internals/trigger/trigger.go | 69 ---- .../internals/trigger/trigger_test.go | 81 ----- tools/cli-client/snap/snapcraft.yaml | 25 -- tools/junit_to_markdown/__init__.py | 0 tools/junit_to_markdown/__main__.py | 25 -- tools/junit_to_markdown/convert.py | 145 -------- .../oci_factory/activities/__init__.py | 0 .../activities/consumer/__init__.py | 0 .../oci_factory/activities/consumer/ca.crt | 30 -- .../notification/mattermost_notifier.py | 96 ------ 140 files changed, 641 insertions(+), 4823 deletions(-) delete mode 100644 .github/actions/checkout/action.yaml delete mode 100644 .github/actions/validate-actor/action.yaml delete mode 100755 .github/actions/validate-actor/test-validate-actor.bats delete mode 100755 .github/actions/validate-actor/validate-actor.sh delete mode 100644 .github/base_digests/24.04 delete mode 100644 .github/workflows/_CLI-Client.yaml delete mode 100644 CODEOWNERS rename {tests/data => examples/mock-rock/1.0}/rockcraft.yaml (75%) create mode 100644 examples/mock-rock/1.1/rockcraft.yaml create mode 100644 examples/mock-rock/1.2/rockcraft.yaml delete mode 100644 oci/hydra/.trivyignore delete mode 100644 oci/hydra/_releases.json delete mode 100644 oci/hydra/contacts.yaml delete mode 100644 oci/hydra/documentation.yaml delete mode 100644 oci/hydra/image.yaml delete mode 100644 oci/identity-platform-admin-ui/_releases.json delete mode 100644 oci/identity-platform-admin-ui/contacts.yaml delete mode 100644 oci/identity-platform-admin-ui/documentation.yaml delete mode 100644 oci/identity-platform-admin-ui/image.yaml delete mode 100644 oci/identity-platform-login-ui/contacts.yaml delete mode 100644 oci/identity-platform-login-ui/documentation.yaml delete mode 100644 oci/identity-platform-login-ui/image.yaml delete mode 100644 oci/kratos/.trivyignore delete mode 100644 oci/kratos/_releases.json delete mode 100644 oci/kratos/contacts.yaml delete mode 100644 oci/kratos/documentation.yaml delete mode 100644 oci/kratos/image.yaml delete mode 100755 src/build_rock/assemble_rock/assemble.sh delete mode 100755 src/build_rock/assemble_rock/requirements.sh delete mode 100755 src/build_rock/configure/generate_build_matrix.py delete mode 100644 src/build_rock/configure/requirements.txt delete mode 100755 src/build_rock/lpci_build/lpci_build.sh delete mode 100755 src/shared/github_output.py mode change 100644 => 100755 src/shared/release_info.py delete mode 100755 src/uploads/preempt_swift_slots.sh delete mode 100755 src/uploads/swift_lockfile_lock.sh delete mode 100755 src/uploads/swift_lockfile_unlock.sh rename {tools => src}/workflow-engine/README.md (100%) rename {tools => src}/workflow-engine/charms/temporal-worker/README.md (99%) rename {tools => src}/workflow-engine/charms/temporal-worker/charm.tf (100%) rename {tools => src}/workflow-engine/charms/temporal-worker/config.yml.template (99%) rename {tools => src}/workflow-engine/charms/temporal-worker/oci_factory/.env.template (83%) rename {tests/fixtures => src/workflow-engine/charms/temporal-worker/oci_factory/activities}/__init__.py (100%) rename {tools => src}/workflow-engine/charms/temporal-worker/oci_factory/activities/activity_consume_events.py (64%) rename {tests/integration => src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer}/__init__.py (100%) create mode 100644 src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/ca.crt rename {tools => src}/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/config.py (100%) rename {tools => src}/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schema.py (100%) rename {tools => src}/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Architecture.avsc (100%) rename {tools => src}/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Artefact.avsc (100%) rename {tools => src}/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Layer.avsc (100%) rename {tools => src}/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Oci.avsc (100%) rename {tools => src}/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/example.json (100%) rename {tools => src}/workflow-engine/charms/temporal-worker/oci_factory/activities/find_images_to_update.py (96%) rename {tools => src}/workflow-engine/charms/temporal-worker/oci_factory/workflows/consume_events_workflow.py (100%) rename {tools => src}/workflow-engine/charms/temporal-worker/pyproject.toml (98%) rename {tools => src}/workflow-engine/charms/temporal-worker/variables.tf (100%) delete mode 100644 tests/__init__.py delete mode 100644 tests/data/junit_xml_failure.xml delete mode 100644 tests/etc/requirements.txt delete mode 100644 tests/fixtures/buffers.py delete mode 100644 tests/fixtures/sample_data.py delete mode 100644 tests/integration/test_convert_junit_xml_to_markdown.py delete mode 100644 tests/integration/test_junit_to_markdown_output.py delete mode 100644 tests/unit/__init__.py delete mode 100644 tests/unit/test_generate_build_matrix.py delete mode 100755 tests/unit/test_github_output.py delete mode 100644 tests/unit/test_junit_to_markdown_formatting.py delete mode 100644 tools/cli-client/README.md delete mode 100644 tools/cli-client/cmd/oci-factory/main.go delete mode 100644 tools/cli-client/go.mod delete mode 100644 tools/cli-client/go.sum delete mode 100644 tools/cli-client/internals/cli/cli_main.go delete mode 100644 tools/cli-client/internals/cli/cli_upload.go delete mode 100644 tools/cli-client/internals/cli/cli_upload_test.go delete mode 100644 tools/cli-client/internals/cli/export_test.go delete mode 100644 tools/cli-client/internals/cli/validator.go delete mode 100644 tools/cli-client/internals/cli/validator_test.go delete mode 100644 tools/cli-client/internals/client/client.go delete mode 100644 tools/cli-client/internals/client/client_test.go delete mode 100644 tools/cli-client/internals/client/export_test.go delete mode 100644 tools/cli-client/internals/client/wf_dispatcher.go delete mode 100644 tools/cli-client/internals/client/wf_dispatcher_test.go delete mode 100644 tools/cli-client/internals/client/wf_poller.go delete mode 100644 tools/cli-client/internals/client/wf_poller_test.go delete mode 100644 tools/cli-client/internals/logger/logger.go delete mode 100644 tools/cli-client/internals/token/token.go delete mode 100644 tools/cli-client/internals/token/token_test.go delete mode 100644 tools/cli-client/internals/trigger/build_metadata.go delete mode 100644 tools/cli-client/internals/trigger/build_metadata_test.go delete mode 100644 tools/cli-client/internals/trigger/trigger.go delete mode 100644 tools/cli-client/internals/trigger/trigger_test.go delete mode 100644 tools/cli-client/snap/snapcraft.yaml delete mode 100644 tools/junit_to_markdown/__init__.py delete mode 100644 tools/junit_to_markdown/__main__.py delete mode 100755 tools/junit_to_markdown/convert.py delete mode 100644 tools/workflow-engine/charms/temporal-worker/oci_factory/activities/__init__.py delete mode 100644 tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/__init__.py delete mode 100644 tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/ca.crt delete mode 100644 tools/workflow-engine/charms/temporal-worker/oci_factory/notification/mattermost_notifier.py diff --git a/.github/actions/checkout/action.yaml b/.github/actions/checkout/action.yaml deleted file mode 100644 index f2f0dcf1..00000000 --- a/.github/actions/checkout/action.yaml +++ /dev/null @@ -1,52 +0,0 @@ -name: Git Checkout -description: 'Checkout action supporting both github and non github repositories.' - - -inputs: - repository: - description: 'Github repository in the format owner/repo or external http(s) URL' - required: true - ref: - description: 'The branch, tag or SHA to checkout' - default: '' - path: - description: 'Relative path under $GITHUB_WORKSPACE to place the repository' - default: '.' - submodules: - description: 'Whether to checkout submodules. true|false|recursive according to actions/checkout@v4' - default: 'false' - github-server-url: - description: 'The base URL for the GitHub instance that you are trying to clone from' - default: 'https://github.com' - -runs: - using: "composite" - steps: - - name: Checkout - shell: bash - run: | - - # If URL lacks the protocol, assume it is a github repo - if [[ "${{ inputs.repository }}" =~ https?:// ]] - then - git_url="${{ inputs.repository }}" - else - git_url="${{ inputs.github-server-url }}/${{ inputs.repository }}.git" - fi - - # create repo path relative to GITHUB_WORKSPACE as per actions/checkout@v4 - repo_path="$GITHUB_WORKSPACE/${{ inputs.path }}" - - # clone the repo and cd into it - git clone $git_url "$repo_path" - cd "$repo_path" - - # checkout the correct ref - git config advice.detachedHead false - git checkout ${{ inputs.ref }} - - # and update sub modules if required - if ${{ inputs.submodules == 'true' || inputs.submodules == 'recursive' }} - then - git submodule update ${{ inputs.submodules == 'recursive' && '--recursive' || '' }} - fi diff --git a/.github/actions/validate-actor/action.yaml b/.github/actions/validate-actor/action.yaml deleted file mode 100644 index dda8ea34..00000000 --- a/.github/actions/validate-actor/action.yaml +++ /dev/null @@ -1,38 +0,0 @@ -name: Validate Access -description: 'Check if the workflow is triggered by an admin user' - -# This callable workflow checks if the workflow is triggered by -# a code owner or an image maintainer by testing the github.actor -# variable against the CODEOWNERS file and the contacts.yaml file -# under oci/* path - -inputs: - admin-only: - description: 'The protected workflow should only be triggered as a code owner or an image maintainer' - required: true - default: 'false' - image-path: - description: 'The path to the image to be built' - required: true - github-token: - description: 'The GITHUB_TOKEN for the GitHub CLI' - required: true - -runs: - using: "composite" - steps: - - name: Check if the workflow is triggered by an admin user - id: check-if-permitted - shell: bash - env: - GITHUB_TOKEN: ${{ inputs.github-token }} - run: ./.github/actions/validate-actor/validate-actor.sh ${{ github.actor }} ${{ inputs.admin-only }} ${{ github.workspace }} ${{ inputs.image-path }} - - - name: Cancel the remaining workflow if the actor is not permitted - if: ${{ !cancelled() && steps.check-if-permitted.outcome == 'failure' }} - shell: bash - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - echo "The workflow is not triggered by a permitted user. Cancelling the workflow." - gh run cancel ${{ github.run_id }} diff --git a/.github/actions/validate-actor/test-validate-actor.bats b/.github/actions/validate-actor/test-validate-actor.bats deleted file mode 100755 index d7595609..00000000 --- a/.github/actions/validate-actor/test-validate-actor.bats +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bats - -SOURCE_DIR=$(dirname -- "${BASH_SOURCE[0]}") - -setup() { - workdir=$(mktemp -d) - mkdir -p $workdir/img - echo -n "* @code-owner" > $workdir/CODEOWNERS - echo -e "maintainers:\n - maintainer" > $workdir/img/contacts.yaml -} - -@test "blocks non-code-owner-non-maintainer user" { - { - output=$(${BATS_TEST_DIRNAME}/validate-actor.sh "random" "true" $workdir "img" 2>&1) - exit_status=$? - } || true - [[ $exit_status -eq 1 ]] - [[ $(echo "${output}"| tail -n 1) = "The workflow is triggered by a user neither as a code owner nor a maintainer of the image img" ]] -} - -@test "allows code owner" { - output=$(${BATS_TEST_DIRNAME}/validate-actor.sh "code-owner" "true" $workdir "img" 2>&1) - [[ $(echo "${output}"| tail -n 1) = "The workflow is triggered by code-owner as the code owner" ]] -} - -@test "allows image maintainer" { - output=$(${BATS_TEST_DIRNAME}/validate-actor.sh "maintainer" "true" $workdir "img") - [[ $(echo "${output}"| tail -n 1) = "The workflow is triggered by maintainer as a maintainer of the image img" ]] -} - -@test "allows non-code-owner-non-maintainer user" { - output=$(${BATS_TEST_DIRNAME}/validate-actor.sh "random" "false" $workdir "img") - [[ $(echo "${output}"| tail -n 1) = "The workflow is not restricted to non-code-owner or non-maintainer users" ]] -} - -@test "user as both code-owner and maintainer is triggered as code owner" { - echo -n " @maintainer" >> $workdir/CODEOWNERS - output=$(${BATS_TEST_DIRNAME}/validate-actor.sh "maintainer" "true" $workdir "img") - [[ $(echo "${output}"| tail -n 1) = "The workflow is triggered by maintainer as the code owner" ]] -} - -@test "teams are expanded to team members" { - echo -n "@canonical/rocks" >> $workdir/CODEOWNERS - output=$(${BATS_TEST_DIRNAME}/validate-actor.sh "ROCKsBot" "true" $workdir "img") - [[ $(echo "${output}"| tail -n 1) = "The workflow is triggered by ROCKsBot as the code owner" ]] -} diff --git a/.github/actions/validate-actor/validate-actor.sh b/.github/actions/validate-actor/validate-actor.sh deleted file mode 100755 index da2401b4..00000000 --- a/.github/actions/validate-actor/validate-actor.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash -e - -actor=$1 -admin_only=$2 -workspace=$3 -image_path=$4 - -echo "github.actor: ${actor}" -echo "admin-only: ${admin_only}" -if [[ ${admin_only} == true ]]; then - exit_status=0 - echo "Expanding team mentions in the CODEOWNERS file" - cp ${workspace}/CODEOWNERS ${workspace}/CODEOWNERS.bak - teams=$(grep -oE '@[[:alnum:]_.-]+\/[[:alnum:]_.-]+' ${workspace}/CODEOWNERS || true | sort | uniq) - - for team in ${teams}; do - org=$(echo ${team} | cut -d'/' -f1 | sed 's/@//') - team_name=$(echo ${team} | cut -d'/' -f2) - members=$(gh api "/orgs/${org}/teams/${team_name}/members" | jq -r '.[].login') - replacement=$(echo "${members}" | xargs -I {} echo -n "@{} " | awk '{$1=$1};1') - sed -i "s|${team}|${replacement}|g" ${workspace}/CODEOWNERS - done - - if grep -wq "@${actor}" ${workspace}/CODEOWNERS; then - echo "The workflow is triggered by ${actor} as the code owner" - elif cat ${workspace}/${image_path}/contacts.yaml | yq ".maintainers" | grep "\- " | grep -wq "${actor}"; then - echo "The workflow is triggered by ${actor} as a maintainer of the image ${image_path}" - else - echo "The workflow is triggered by a user neither as a code owner nor a maintainer of the image ${image_path}" - exit_status=1 - fi - mv ${workspace}/CODEOWNERS.bak ${workspace}/CODEOWNERS - exit ${exit_status} -else - echo "The workflow is not restricted to non-code-owner or non-maintainer users" -fi diff --git a/.github/base_digests/20.04 b/.github/base_digests/20.04 index b5d44b69..16a0145d 100644 --- a/.github/base_digests/20.04 +++ b/.github/base_digests/20.04 @@ -1 +1 @@ -public.ecr.aws/ubuntu/ubuntu:focal@sha256:4a885c102bc7de9ff2ffc4b11b65f35e46827d608069cd959181718aa7d14731 +public.ecr.aws/ubuntu/ubuntu:focal@sha256:5b2a13b49acb61bc754c03841a405fb6416cbc3a2fd117c120ebaa26d22ccfbf diff --git a/.github/base_digests/22.04 b/.github/base_digests/22.04 index a911a392..cdd8a037 100644 --- a/.github/base_digests/22.04 +++ b/.github/base_digests/22.04 @@ -1,2 +1,2 @@ -public.ecr.aws/ubuntu/ubuntu:jammy@sha256:1582c29f34a48752e406f1a261fe9545fad895da3f6bb4be55bc82485557564e +public.ecr.aws/ubuntu/ubuntu:jammy@sha256:289c8c222907130df84230b590c52fac0489e8337d214c03d8f424668535cce6 diff --git a/.github/base_digests/24.04 b/.github/base_digests/24.04 deleted file mode 100644 index e9f9d674..00000000 --- a/.github/base_digests/24.04 +++ /dev/null @@ -1,2 +0,0 @@ -public.ecr.aws/ubuntu/ubuntu:noble@sha256:5b2fc4131b3c134a019c3ea815811de70e6ad9ee1626f59bf302558a95b436e5 - diff --git a/.github/workflows/Announcements.yaml b/.github/workflows/Announcements.yaml index c379700a..09999dcc 100644 --- a/.github/workflows/Announcements.yaml +++ b/.github/workflows/Announcements.yaml @@ -33,14 +33,6 @@ jobs: fi done - - name: Validate access to triggered image - uses: ./.github/actions/validate-actor - if: ${{ github.repository == 'canonical/oci-factory' }} - with: - admin-only: true - image-path: "oci/${{ steps.get-image-name.outputs.img-name }}" - github-token: ${{ secrets.ROCKSBOT_TOKEN }} - - name: Get contacts for ${{ steps.get-image-name.outputs.img-name }} id: get-contacts working-directory: oci/${{ steps.get-image-name.outputs.img-name }} @@ -112,3 +104,4 @@ jobs: do MM_CHANNEL_ID="${channel}" ./src/notifications/send_to_mattermost.sh done + diff --git a/.github/workflows/Build-Rock.yaml b/.github/workflows/Build-Rock.yaml index 31dc1ef8..495c8650 100644 --- a/.github/workflows/Build-Rock.yaml +++ b/.github/workflows/Build-Rock.yaml @@ -3,231 +3,204 @@ name: Build rock on: workflow_call: inputs: - # build parameters oci-archive-name: - description: "Final filename of the rock OCI archive." + description: "Final filename of the rock's OCI archive" type: string required: true - build-id: - description: "Optional string for identifying workflow jobs in GitHub UI" + oci-factory-path: + description: "Path, in the OCI Factory, to this rock" type: string - - # source parameters + required: true + rock-name: + description: "Name of the rock" + type: string + required: true rock-repo: - description: "Public Git repo where to build the rock from." + description: "Public Git repo where to build the rock from" type: string required: true rock-repo-commit: - description: "Git ref from where to build the rock from." + description: "Git ref from where to build the rock from" type: string required: true rockfile-directory: - description: "Directory in repository where to find the rockcraft.yaml file." + description: "Directory, in 'rock-repo', where to find the rockcraft.yaml file" type: string required: true - # parameters for multi-arch builds - arch-map: - description: "JSON string mapping target architecture to runners." - type: string - default: '{"amd64": ["linux", "X64"], "arm64": ["linux", "ARM64"]}' - lpci-fallback: - description: "Enable fallback to Launchpad build when runners for target arch are not available." - type: boolean - default: false - env: - ROCK_REPO_DIR: rock-repo # path where the image repo is cloned into - ROCK_CI_FOLDER: ci-rocks # path of uploaded/downloaded artifacts + ROCKS_CI_FOLDER: ci-rocks jobs: - configure-build: - # configure-build reads the rockcraft.yaml, creating one or more *-build job runs - # depending on the target architecture. + prepare-multi-arch-matrix: runs-on: ubuntu-22.04 outputs: - runner-build-matrix: ${{ steps.configure.outputs.runner-build-matrix }} - lpci-build-matrix: ${{ steps.configure.outputs.lpci-build-matrix }} - oci-factory-ref: ${{ steps.workflow-version.outputs.sha }} - name: "configure-build ${{ inputs.build-id != '' && format('| {0}', inputs.build-id) || ' '}}" + build-for: ${{ steps.rock-platforms.outputs.build-for }} + build-with-lpci: ${{ steps.rock-platforms.outputs.build-with-lpci }} steps: - - - name: Get Workflow Version - # Note: we may need to pass a github token when working with private repositories. - # https://github.com/canonical/get-workflow-version-action - id: workflow-version - uses: canonical/get-workflow-version-action@v1 - with: - repository-name: canonical/oci-factory - file-name: Build-Rock.yaml - - - name: Cloning OCI Factory + - name: Clone GitHub image repository uses: actions/checkout@v4 - with: - repository: canonical/oci-factory - ref: ${{ steps.workflow-version.outputs.sha }} - fetch-depth: 1 - - - name: Cloning Target Repo - uses: ./.github/actions/checkout + id: clone-image-repo + continue-on-error: true with: repository: ${{ inputs.rock-repo }} - path: ${{ env.ROCK_REPO_DIR }} - ref: ${{ inputs.rock-repo-commit }} - submodules: "recursive" - - - name: Installing Python - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - - name: Installing Python requirements - run: pip install -r src/build_rock/configure/requirements.txt - - # Configure matrices for each *-build job - - name: Configuring Jobs - id: configure + fetch-depth: 0 + - name: Clone generic image repository + if: ${{ steps.clone-image-repo.outcome == 'failure' }} run: | - python3 -m src.build_rock.configure.generate_build_matrix \ - --rockfile-directory "${{ env.ROCK_REPO_DIR }}/${{ inputs.rockfile-directory }}" \ - --lpci-fallback "${{ toJSON(inputs.lpci-fallback) }}" \ - --config ${{ toJSON(inputs.arch-map) }} # important: do not use quotes here - - runner-build: - # runner-build builds rocks per target architecture using pre configured runner images. - needs: [configure-build] - if: fromJSON(needs.configure-build.outputs.runner-build-matrix).include[0] != '' + git clone ${{ inputs.rock-repo }} . + - run: git checkout ${{ inputs.rock-repo-commit }} + - run: sudo snap install yq --channel=v4/stable + - name: Validate image naming and base + working-directory: ${{ inputs.rockfile-directory }} + run: | + rock_name=`cat rockcraft.y*ml | yq -r .name` + if [[ "${{ inputs.oci-factory-path }}" != *"${rock_name}"* ]] + then + echo "ERROR: the rock's name '${rock_name}' must match the OCI folder name!" + exit 1 + fi + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip install pyyaml + - name: Get rock archs + uses: jannekem/run-python-script-action@v1 + id: rock-platforms + with: + script: | + import yaml + import os + + BUILD_WITH_LPCI = 0 + + with open("${{ inputs.rockfile-directory }}/rockcraft.yaml") as rf: + rockcraft_yaml = yaml.safe_load(rf) + + platforms = rockcraft_yaml["platforms"] + + target_archs = [] + for platf, values in platforms.items(): + if isinstance(values, dict) and "build-for" in values: + target_archs += list(values["build-for"]) + continue + target_archs.append(platf) + + print(f"Target architectures: {set(target_archs)}") + + matrix = {"include": []} + gh_supported_archs = {"amd64": "ubuntu-22.04", "arm64": "Ubuntu_ARM64_4C_16G_01"} + if set(target_archs) - set(gh_supported_archs.keys()): + # Then there are other target archs, so we need to build in LP + matrix["include"].append( + {"architecture": "-".join(set(target_archs)), "runner": gh_supported_archs["amd64"]} + ) + BUILD_WITH_LPCI = 1 + else: + for runner_arch, runner_name in gh_supported_archs.items(): + if runner_arch in target_archs: + matrix["include"].append( + {"architecture": runner_arch, "runner": runner_name} + ) + + with open(os.environ["GITHUB_OUTPUT"], "a") as gh_out: + print(f"build-for={matrix}", file=gh_out) + print(f"build-with-lpci={BUILD_WITH_LPCI}", file=gh_out) + + build: + needs: [prepare-multi-arch-matrix] strategy: fail-fast: true - matrix: ${{ fromJSON(needs.configure-build.outputs.runner-build-matrix) }} + matrix: ${{ fromJSON(needs.prepare-multi-arch-matrix.outputs.build-for) }} runs-on: ${{ matrix.runner }} - name: "runner-build | ${{ matrix.architecture }} ${{ inputs.build-id != '' && format('| {0}', inputs.build-id) || ' '}}" + name: 'Build ${{ inputs.rock-name }} | ${{ matrix.architecture }}' steps: - - - name: Cloning OCI Factory + - name: Clone GitHub image repository uses: actions/checkout@v4 - with: - repository: canonical/oci-factory - ref: ${{ needs.configure-build.outputs.oci-factory-ref }} - fetch-depth: 1 - - - name: Cloning Target Repo - uses: ./.github/actions/checkout + id: clone-image-repo + continue-on-error: true with: repository: ${{ inputs.rock-repo }} - path: ${{ env.ROCK_REPO_DIR }} - ref: ${{ inputs.rock-repo-commit }} - submodules: "recursive" - - - name: Building Target + fetch-depth: 0 + - name: Clone generic image repository + if: ${{ steps.clone-image-repo.outcome == 'failure' }} + run: | + git clone ${{ inputs.rock-repo }} . + - run: git checkout ${{ inputs.rock-repo-commit }} + - name: Build rock ${{ inputs.rock-name }} id: rockcraft + if: needs.prepare-multi-arch-matrix.outputs.build-with-lpci == 0 uses: canonical/craft-actions/rockcraft-pack@main with: - path: "${{ env.ROCK_REPO_DIR }}/${{ inputs.rockfile-directory }}" + path: "${{ inputs.rockfile-directory }}" verbosity: debug - - - name: Collecting Artifacts - id: collect-artifacts - run: | - mkdir -p ${{ env.ROCK_CI_FOLDER }} && cp ${{ steps.rockcraft.outputs.rock }} "$_" - echo "filename=$(basename ${{ steps.rockcraft.outputs.rock }})" >> $GITHUB_OUTPUT - - - name: Uploading Artifacts - uses: actions/upload-artifact@v4 + - uses: actions/setup-python@v5 + if: needs.prepare-multi-arch-matrix.outputs.build-with-lpci == 1 with: - name: ${{ inputs.oci-archive-name }}-${{ steps.collect-artifacts.outputs.filename }} - path: ${{ env.ROCK_CI_FOLDER }} - if-no-files-found: error - - lpci-build: - # lpci-build is a fallback for building rocks if no suitable runners are - # configured for the required architecture. Builds in this job will be - # outsourced to Launchpad for completion. - # Note the Secret - needs: [configure-build] - if: fromJSON(needs.configure-build.outputs.lpci-build-matrix).include[0] != '' - strategy: - fail-fast: true - matrix: ${{ fromJSON(needs.configure-build.outputs.lpci-build-matrix) }} - runs-on: ubuntu-22.04 - name: "lpci-build | ${{ matrix.architecture }} ${{ inputs.build-id != '' && format('| {0}', inputs.build-id) || ' '}}" - steps: - - - name: Cloning OCI Factory - uses: actions/checkout@v4 - with: - repository: canonical/oci-factory - ref: ${{ needs.configure-build.outputs.oci-factory-ref }} - fetch-depth: 1 - - - name: Cloning Target Repo - uses: ./.github/actions/checkout - with: - repository: ${{ inputs.rock-repo }} - path: ${{ env.ROCK_REPO_DIR }} - ref: ${{ inputs.rock-repo-commit }} - submodules: "recursive" - - - name: Building Target - # TODO: Replace this retry action with bash equivalent for better testing - uses: nick-fields/retry@v3.0.0 + python-version: '3.x' + - uses: nick-fields/retry@v3.0.0 + name: Build multi-arch ${{ inputs.rock-name }} in Launchpad + if: needs.prepare-multi-arch-matrix.outputs.build-with-lpci == 1 with: timeout_minutes: 180 max_attempts: 4 polling_interval_seconds: 5 retry_wait_seconds: 30 command: | - src/build_rock/lpci_build/lpci_build.sh \ - -c "${{ secrets.LP_CREDENTIALS_B64 }}" \ - -d "${{ env.ROCK_REPO_DIR }}/${{ inputs.rockfile-directory }}" - - - name: Collecting Artifacts - id: collect-artifacts + set -ex + cd ${{ inputs.rockfile-directory }} + rocks_toolbox="$(mktemp -d)" + git clone --depth 1 --branch v1.1.2 https://github.com/canonical/rocks-toolbox $rocks_toolbox + ${rocks_toolbox}/rockcraft_lpci_build/requirements.sh + pip3 install -r ${rocks_toolbox}/rockcraft_lpci_build/requirements.txt + + python3 ${rocks_toolbox}/rockcraft_lpci_build/rockcraft_lpci_build.py \ + --lp-credentials-b64 "${{ secrets.LP_CREDENTIALS_B64 }}" \ + --launchpad-accept-public-upload + - name: Rename rock OCI archive + id: rock run: | - mkdir -p ${{ env.ROCK_CI_FOLDER }} && cp ${{ env.ROCK_REPO_DIR }}/${{ inputs.rockfile-directory }}/*.rock "$_" - echo "filename=${{ matrix.rock-name }}_${{ matrix.architecture }}" >> $GITHUB_OUTPUT - - - name: Uploading Artifacts + mkdir ${{ env.ROCKS_CI_FOLDER }} + if [ ${{ needs.prepare-multi-arch-matrix.outputs.build-with-lpci }} -eq 0 ] + then + cp ${{ steps.rockcraft.outputs.rock }} ${{ env.ROCKS_CI_FOLDER }}/$(basename ${{ steps.rockcraft.outputs.rock }}) + echo "filename=$(basename ${{ steps.rockcraft.outputs.rock }})" >> $GITHUB_OUTPUT + else + cp ${{ inputs.rockfile-directory }}/*.rock ${{ env.ROCKS_CI_FOLDER }} + echo "filename=${{ inputs.rock-name }}_${{ matrix.architecture }}" >> $GITHUB_OUTPUT + fi + - name: Upload ${{ inputs.rock-name }} for ${{ matrix.architecture }} uses: actions/upload-artifact@v4 with: - name: ${{ inputs.oci-archive-name }}-${{ steps.collect-artifacts.outputs.filename }} - path: ${{ env.ROCK_CI_FOLDER }} + name: ${{ inputs.oci-archive-name }}-${{ steps.rock.outputs.filename }} + path: ${{ env.ROCKS_CI_FOLDER }} if-no-files-found: error assemble-rock: - # Assemble individual single-arch rocks into multi-arch rocks - needs: [runner-build, lpci-build, configure-build] + needs: [prepare-multi-arch-matrix, build] runs-on: ubuntu-22.04 - # Always run even if one of the *-build jobs are skipped - # Nice example from benjamin-bergia/github-workflow-patterns... - if: ${{ always() && contains(needs.*.result, 'success') && !(contains(needs.*.result, 'failure')) }} - name: "assemble-rock ${{ inputs.build-id != '' && format('| {0}', inputs.build-id) || ' '}}" steps: - # Job Setup - - name: Cloning OCI Factory - uses: actions/checkout@v4 - with: - repository: canonical/oci-factory - ref: ${{ needs.configure-build.outputs.oci-factory-ref }} - fetch-depth: 1 - - - run: src/build_rock/assemble_rock/requirements.sh - - name: Downloading Single Arch rocks - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v4 id: download - with: - path: ${{ env.ROCK_CI_FOLDER }} - pattern: ${{ inputs.oci-archive-name }}-* - - - name: Assembling Multi Arch rock + - run: sudo apt update && sudo apt install buildah -y + - name: Merge single-arch rocks into multi-arch OCI archive run: | - src/build_rock/assemble_rock/assemble.sh \ - -n "${{ inputs.oci-archive-name }}" \ - -d "${{ env.ROCK_CI_FOLDER }}" - - - name: Uploading Multi Arch rock + set -xe + ls ./${{ inputs.oci-archive-name }}* + buildah manifest create multi-arch-rock + for rock in `find ${{ inputs.oci-archive-name }}*/*.rock` + do + test -f $rock + buildah manifest add multi-arch-rock oci-archive:$rock + done + buildah manifest push --all multi-arch-rock oci-archive:${{ inputs.oci-archive-name }} + - name: Upload multi-arch ${{ inputs.oci-archive-name }} OCI archive uses: actions/upload-artifact@v4 with: name: ${{ inputs.oci-archive-name }} path: ${{ inputs.oci-archive-name }} if-no-files-found: error + - uses: actions/cache/save@v4 + with: + path: ${{ inputs.oci-archive-name }} + key: ${{ github.run_id }}-${{ inputs.oci-archive-name }} diff --git a/.github/workflows/Documentation.yaml b/.github/workflows/Documentation.yaml index a1aa61ec..b9859fa3 100644 --- a/.github/workflows/Documentation.yaml +++ b/.github/workflows/Documentation.yaml @@ -5,14 +5,12 @@ on: push: paths: - "oci/*/documentation.y*ml" - branches: - - main workflow_dispatch: inputs: oci-image-name: description: 'OCI image to generate the documentation for' required: true - external_ref_id: # (1) + external_ref_id: #(1) description: 'Optional ID for unique run detection' required: false type: string @@ -23,10 +21,6 @@ on: description: 'OCI image to generate the documentation for' required: true type: string - release-commit-sha: - description: 'Commit SHA containing the updated _releases.json' - required: true - type: string jobs: validate-documentation-request: @@ -36,20 +30,12 @@ jobs: oci-img-path: ${{ steps.validate-image.outputs.img-path }} oci-img-name: ${{ steps.validate-image.outputs.img-name }} steps: - - name: ${{ inputs.external_ref_id }} # (2) + - name: ${{ inputs.external_ref_id }} #(2) if: ${{ github.event_name == 'workflow_dispatch' }} run: echo 'Started by ${{ inputs.external_ref_id }}' >> "$GITHUB_STEP_SUMMARY" - uses: actions/checkout@v4 - - name: Validate access to triggered image - uses: ./.github/actions/validate-actor - if: ${{ github.repository == 'canonical/oci-factory' }} - with: - admin-only: true - image-path: "oci/${{ inputs.oci-image-name }}" - github-token: ${{ secrets.ROCKSBOT_TOKEN }} - - name: Infer images to document uses: tj-actions/changed-files@v35 id: changed-files @@ -92,8 +78,6 @@ jobs: IS_PROD: ${{ ! startsWith(needs.validate-documentation-request.outputs.oci-img-name, 'mock-') }} steps: - uses: actions/checkout@v4 - with: - ref: ${{ inputs.release-commit-sha }} - uses: actions/setup-python@v5 with: @@ -159,7 +143,7 @@ jobs: runs-on: ubuntu-22.04 name: Notify on failure needs: [validate-documentation-request, do-documentation] - if: ${{ !cancelled() && contains(needs.*.result, 'failure') && github.event_name != 'workflow_dispatch' }} + if: ${{ always() && contains(needs.*.result, 'failure') && github.event_name != 'workflow_dispatch' }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/Image.yaml b/.github/workflows/Image.yaml index 0ba4c7c3..2bd21039 100644 --- a/.github/workflows/Image.yaml +++ b/.github/workflows/Image.yaml @@ -1,5 +1,5 @@ name: Image -run-name: "Image - ${{ inputs.oci-image-name || github.triggering_actor }} - ${{ github.ref }}" +run-name: 'Image - ${{ inputs.oci-image-name || github.triggering_actor }} - ${{ github.ref }}' on: push: @@ -24,8 +24,8 @@ on: required: true type: boolean default: false - external_ref_id: # (1) - description: "Optional ID for unique run detection" + external_ref_id: #(1) + description: 'Optional ID for unique run detection' required: false type: string default: "default-id" @@ -53,8 +53,9 @@ jobs: release-to: ${{ steps.prepare-matrix.outputs.release-to }} oci-img-path: ${{ steps.validate-image.outputs.img-path }} oci-img-name: ${{ steps.validate-image.outputs.img-name }} + revision-data-cache-key: ${{ steps.prepare-matrix.outputs.revision-data-cache-key }} steps: - - name: ${{ inputs.external_ref_id }} # (2) + - name: ${{ inputs.external_ref_id }} #(2) if: ${{ github.event_name == 'workflow_dispatch' }} run: echo 'Started by ${{ inputs.external_ref_id }}' >> "$GITHUB_STEP_SUMMARY" @@ -94,14 +95,6 @@ jobs: echo "img-name=$(basename ${img_path})" >> "$GITHUB_OUTPUT" echo "img-path=${img_path}" >> "$GITHUB_OUTPUT" - - name: Validate access to triggered image - uses: ./.github/actions/validate-actor - if: ${{ github.repository == 'canonical/oci-factory' && !github.event.pull_request.head.repo.fork }} - with: - admin-only: true - image-path: ${{ steps.validate-image.outputs.img-path }} - github-token: ${{ secrets.ROCKSBOT_TOKEN }} - - name: Use custom image trigger if: ${{ inputs.b64-image-trigger != '' }} run: echo ${{ inputs.b64-image-trigger }} | base64 -d > ${{ steps.validate-image.outputs.img-path }}/image.yaml @@ -112,6 +105,18 @@ jobs: - run: pip install -r src/image/requirements.txt + - name: Get next revision number + id: get-next-revision + env: + OS_USERNAME: ${{ secrets.SWIFT_OS_USERNAME }} + OS_TENANT_NAME: ${{ secrets.SWIFT_OS_TENANT_NAME }} + OS_PASSWORD: ${{ secrets.SWIFT_OS_PASSWORD }} + OS_REGION_NAME: ${{ secrets.SWIFT_OS_REGION_NAME }} + OS_STORAGE_URL: ${{ secrets.SWIFT_OS_STORAGE_URL }} + IMAGE_NAME: ${{ steps.validate-image.outputs.img-name }} + SWIFT_CONTAINER_NAME: ${{ vars.SWIFT_CONTAINER_NAME }} + run: ./src/image/define_image_revision.sh + - name: Validate and prepare builds matrix id: prepare-matrix env: @@ -121,188 +126,55 @@ jobs: ./src/image/prepare_single_image_build_matrix.py \ --oci-path ${{ steps.validate-image.outputs.img-path }} \ - --revision-data-dir ${{ env.DATA_DIR }} + --revision-data-dir ${{ env.DATA_DIR }} \ + --next-revision ${{ steps.get-next-revision.outputs.revision }} - validate-matrix: - # validate matrix prepared in previous job before running Build-Rock workflow. - runs-on: ubuntu-22.04 - needs: [prepare-build] - strategy: - fail-fast: true - matrix: ${{ fromJSON(needs.prepare-build.outputs.build-matrix) }} - steps: + echo "revision-data-cache-key=${{ github.run_id }}-${{ env.DATA_DIR }}" >> "$GITHUB_OUTPUT" - - name: Clone GitHub image repository - uses: actions/checkout@v4 + - uses: actions/cache/save@v4 with: - repository: ${{ matrix.source }} - ref: ${{ matrix.commit }} - submodules: "recursive" - fetch-depth: 1 - - - name: Installing yq - run: sudo snap install yq --channel=v4/stable - - - name: Validate image naming and base - run: | - rock_name=`cat "${{ matrix.directory }}"/rockcraft.y*ml | yq -r .name` - folder_name="${{ matrix.path }}" - if [[ "${folder_name}" != *"${rock_name}"* ]] - then - echo "ERROR: the OCI folder name '${folder_name}', must contain the rock's name '${rock_name}'." - exit 1 - fi + path: ${{ steps.prepare-matrix.outputs.revision-data-dir }} + key: ${{ steps.prepare-matrix.outputs.revision-data-cache-key }} run-build: - needs: [prepare-build, validate-matrix] + needs: [prepare-build] strategy: fail-fast: true matrix: ${{ fromJSON(needs.prepare-build.outputs.build-matrix) }} uses: ./.github/workflows/Build-Rock.yaml with: - oci-archive-name: ${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.dir_identifier }} - build-id: ${{ matrix.name }} + oci-archive-name: ${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.revision }} + oci-factory-path: ${{ matrix.path }} + rock-name: ${{ matrix.name }} rock-repo: ${{ matrix.source }} rock-repo-commit: ${{ matrix.commit }} rockfile-directory: ${{ matrix.directory }} - lpci-fallback: true secrets: inherit - tmp-cache-job: - # TODO: This is a temporary job that will be removed when the refactored test job is merged. - # Going forward we download the built rocks from artifacts instead of cache. This job takes - # the uploaded rocks then re-caches them for compatibility. - name: Temporary step to cache rocks - runs-on: ubuntu-22.04 - needs: [prepare-build, run-build] - strategy: - fail-fast: true - matrix: ${{ fromJSON(needs.prepare-build.outputs.build-matrix) }} - steps: - - name: Download rock - uses: actions/download-artifact@v4 - with: - name: ${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.dir_identifier }} - - - uses: actions/cache/save@v4 - with: - key: ${{ github.run_id }}-${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.dir_identifier }} - path: ${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.dir_identifier }} - test: - needs: [prepare-build, run-build, tmp-cache-job] - # TODO: Remove tmp-cache-job when removing the job tmp-cache-job + needs: [prepare-build, run-build] name: Test strategy: fail-fast: true matrix: ${{ fromJSON(needs.prepare-build.outputs.build-matrix) }} uses: ./.github/workflows/Tests.yaml with: - oci-image-name: "${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.dir_identifier }}" + oci-image-name: "${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.revision }}" oci-image-path: "oci/${{ matrix.name }}" test-from: "cache" - cache-key: ${{ github.run_id }}-${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.dir_identifier }} + cache-key: ${{ github.run_id }}-${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.revision }} secrets: inherit - prepare-upload: - runs-on: ubuntu-22.04 - needs: [prepare-build, run-build, test] - name: Prepare upload - if: ${{ inputs.upload || (github.ref_name == 'main' && github.event_name == 'push') }} - env: - OS_USERNAME: ${{ secrets.SWIFT_OS_USERNAME }} - OS_TENANT_NAME: ${{ secrets.SWIFT_OS_TENANT_NAME }} - OS_PASSWORD: ${{ secrets.SWIFT_OS_PASSWORD }} - OS_REGION_NAME: ${{ secrets.SWIFT_OS_REGION_NAME }} - OS_STORAGE_URL: ${{ secrets.SWIFT_OS_STORAGE_URL }} - IMAGE_NAME: ${{ needs.prepare-build.outputs.oci-img-name }} - SWIFT_CONTAINER_NAME: ${{ vars.SWIFT_CONTAINER_NAME }} - DATA_DIR: "revision-data" - outputs: - build-matrix: ${{ steps.prepare-matrix.outputs.build-matrix }} - revision-data-cache-key: ${{ steps.prepare-matrix.outputs.revision-data-cache-key }} - steps: - - uses: actions/checkout@v4 - - - name: Use custom image trigger - if: ${{ inputs.b64-image-trigger != '' }} - run: echo ${{ inputs.b64-image-trigger }} | base64 -d > ${{ needs.prepare-build.outputs.oci-img-path }}/image.yaml - - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - - run: | - ./src/uploads/requirements.sh - pip install -r src/image/requirements.txt -r src/uploads/requirements.txt - - - name: Upload the lockfile for the image - id: swift-lock - run: | - ./src/uploads/swift_lockfile_lock.sh \ - ${{ needs.prepare-build.outputs.oci-img-name }} - - # Here starts the critical section, have to be executed in sequence outside of matrix. - - name: Get next revision number - id: get-next-revision - run: ./src/image/define_image_revision.sh - - - name: Prepare builds matrix for upload - id: prepare-matrix - run: | - set -ex - - mkdir ${{ env.DATA_DIR }} - - ./src/image/prepare_single_image_build_matrix.py \ - --oci-path ${{ needs.prepare-build.outputs.oci-img-path }} \ - --revision-data-dir ${{ env.DATA_DIR }} \ - --next-revision ${{ steps.get-next-revision.outputs.revision }} \ - --infer-image-track - - echo "revision-data-cache-key=${{ github.run_id }}-${{ env.DATA_DIR }}-$(date +%s)" >> "$GITHUB_OUTPUT" - - - name: Preempt Swift slot - run: | - ./src/uploads/preempt_swift_slots.sh ${{ env.DATA_DIR }} - - # Here leaves the critical section. - # The lock will be removed even if the steps above fail, - # or the workflow is cancelled. - - name: Remove the lockfile for the image - # Failing to lock the swift container can mean there are multiple - # workflows trying to upload the same image at the same time. - # Therefore we should not remove the lockfile if the swift lock failed. - if: ${{ always() && steps.swift-lock.outcome != 'failure' }} - run: | - ./src/uploads/swift_lockfile_unlock.sh \ - ${{ needs.prepare-build.outputs.oci-img-name }} - - # The revision files have to be sanitised before merging, - # since the `track` field should not be present. - - name: Sanitise revision files - run: | - set -ex - for revision_file in `ls ${{ env.DATA_DIR }}` - do - jq 'del(.track, .base)' ${{ env.DATA_DIR }}/$revision_file > ${{ env.DATA_DIR }}/$revision_file.tmp - mv ${{ env.DATA_DIR }}/$revision_file.tmp ${{ env.DATA_DIR }}/$revision_file - done - - - uses: actions/cache/save@v4 - with: - path: ${{ steps.prepare-matrix.outputs.revision-data-dir }} - key: ${{ steps.prepare-matrix.outputs.revision-data-cache-key }} - upload: runs-on: ubuntu-22.04 - needs: [prepare-build, prepare-upload] + needs: [prepare-build, run-build, test] name: Upload + if: ${{ inputs.upload || (github.ref_name == 'main' && github.event_name == 'push') }} strategy: fail-fast: true - matrix: ${{ fromJSON(needs.prepare-upload.outputs.build-matrix) }} + matrix: ${{ fromJSON(needs.prepare-build.outputs.build-matrix) }} env: - OCI_ARCHIVE_NAME: ${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.dir_identifier }} + OCI_ARCHIVE_NAME: ${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.revision }} outputs: artefacts-hashes: ${{ steps.artefacts-hashes.outputs.hashes }} steps: @@ -325,17 +197,38 @@ jobs: ./src/uploads/requirements.sh pip install -r src/uploads/requirements.txt -r src/image/requirements.txt + - name: Clone GitHub image repository + uses: actions/checkout@v4 + id: clone-image-repo + continue-on-error: true + with: + repository: ${{ matrix.source }} + fetch-depth: 0 + path: source + + - name: Clone generic image repository + if: ${{ steps.clone-image-repo.outcome == 'failure' }} + run: | + git clone ${{ matrix.source }} source + + - run: cd source && git checkout ${{ matrix.commit }} + - uses: actions/cache/restore@v4 with: path: ${{ env.OCI_ARCHIVE_NAME }} - key: ${{ github.run_id }}-${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.dir_identifier }} + key: ${{ github.run_id }}-${{ matrix.name }}_${{ matrix.commit }}_${{ matrix.revision }} fail-on-cache-miss: true + - name: Infer track name + id: get-track + run: | + ./src/uploads/infer_image_track.py --recipe-dirname source/${{ matrix.directory }} + - name: Name output artefact id: rename-oci-archive run: | # Rename the OCI archive tarball - canonical_tag="${{ matrix.track }}_${{ matrix.revision }}" + canonical_tag="${{ steps.get-track.outputs.track }}_${{ matrix.revision }}" name="${{ matrix.name }}_${canonical_tag}" mv ${{ env.OCI_ARCHIVE_NAME }} $name @@ -463,12 +356,12 @@ jobs: IMAGE_NAME: ${{ matrix.name }} SWIFT_CONTAINER_NAME: ${{ vars.SWIFT_CONTAINER_NAME }} run: | - jq --arg base "${{ matrix.base }}" \ + jq --arg base "${{ steps.get-track.outputs.base }}" \ --arg digest "${{ steps.upload-image.outputs.digest }}" \ '. + {base: $base, digest: $digest}' <<< '${{ toJSON(matrix) }}' > build_metadata.json ./src/uploads/upload_to_swift.sh \ ${{ matrix.name }} \ - ${{ matrix.track }} \ + ${{ steps.get-track.outputs.track }} \ ${{ matrix.revision }} \ build_metadata.json \ ${{ steps.generate-sboms.outputs.sboms }} \ @@ -487,7 +380,7 @@ jobs: # and commit the _releases.json file in a single commit, outside a matrix job prepare-releases: name: Prepare releases - needs: [prepare-build, prepare-upload, upload] + needs: [prepare-build, upload] runs-on: ubuntu-22.04 if: ${{ needs.prepare-build.outputs.release-to != '' }} concurrency: @@ -511,7 +404,7 @@ jobs: - uses: actions/cache/restore@v4 with: path: ${{ env.REVISION_DATA_DIR }} - key: ${{ needs.prepare-upload.outputs.revision-data-cache-key }} + key: ${{ needs.prepare-build.outputs.revision-data-cache-key }} fail-on-cache-miss: true - run: pip install -r src/image/requirements.txt @@ -626,16 +519,15 @@ jobs: notify: runs-on: ubuntu-22.04 name: Notify - needs: - [prepare-build, run-build, upload, prepare-releases, generate-provenance] - if: ${{ !cancelled() && contains(needs.*.result, 'failure') && github.event_name != 'pull_request' }} + needs: [prepare-build, run-build, upload, prepare-releases, generate-provenance] + if: ${{ always() && contains(needs.*.result, 'failure') && github.event_name != 'pull_request' }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.x" - + python-version: '3.x' + - name: Summarize workflow failure message id: get-summary run: | @@ -657,7 +549,7 @@ jobs: URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} SUMMARY: ${{ steps.get-summary.outputs.summary }} FOOTER: "Triggered by ${{ github.triggering_actor }}. Ref: ${{ github.ref }}. Attempts: ${{ github.run_attempt }}" - TITLE: "${{ needs.prepare-build.outputs.oci-img-name }}: failed to build->upload->release" + TITLE: '${{ needs.prepare-build.outputs.oci-img-name }}: failed to build->upload->release' run: | for channel in $(echo ${{ steps.get-contacts.outputs.mattermost-channels }} | tr ',' ' ') do diff --git a/.github/workflows/Release.yaml b/.github/workflows/Release.yaml index 515d345d..42c0799b 100644 --- a/.github/workflows/Release.yaml +++ b/.github/workflows/Release.yaml @@ -11,7 +11,7 @@ on: description: 'Cache key (to fetch image trigger from cache)' required: false type: string - external_ref_id: # (1) + external_ref_id: #(1) description: 'Optional ID for unique run detection' required: false type: string @@ -34,19 +34,11 @@ jobs: outputs: oci-image-name: ${{ steps.get-image-name.outputs.img-name }} steps: - - name: ${{ inputs.external_ref_id }} # (2) + - name: ${{ inputs.external_ref_id }} #(2) run: echo 'Started by ${{ inputs.external_ref_id }}' >> "$GITHUB_STEP_SUMMARY" - uses: actions/checkout@v4 - - name: Validate access to triggered image - uses: ./.github/actions/validate-actor - if: ${{ github.repository == 'canonical/oci-factory' }} - with: - admin-only: true - image-path: "oci/${{ inputs.oci-image-name }}" - github-token: ${{ secrets.ROCKSBOT_TOKEN }} - - name: Infer number of image triggers uses: tj-actions/changed-files@v35 id: changed-files @@ -75,7 +67,6 @@ jobs: needs: [validate-push-release-request] outputs: gh-releases-matrix: ${{ steps.release-image.outputs.gh-releases-matrix }} - release-commit-sha: ${{ steps.release-commit-sha.outputs.release-commit-sha }} env: IS_PROD: ${{ ! startsWith(inputs.oci-image-name, 'mock-') }} steps: @@ -150,17 +141,11 @@ jobs: - name: Commit oci/${{ inputs.oci-image-name }}/_releases.json uses: actions-x/commit@v6 with: - token: ${{ secrets.ROCKSBOT_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} branch: ${{ github.ref }} message: 'ci: automatically update oci/${{ inputs.oci-image-name }}/_releases.json, from ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' files: oci/${{ inputs.oci-image-name }}/_releases.json - - name: Get commit SHA of _release.json update - id: release-commit-sha - run: | - set -ex - echo "release-commit-sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - update-documentation: name: Update documentation @@ -168,7 +153,6 @@ jobs: uses: ./.github/workflows/Documentation.yaml with: oci-image-name: "${{ inputs.oci-image-name }}" - release-commit-sha: "${{ needs.do-releases.outputs.release-commit-sha }}" secrets: inherit @@ -182,8 +166,8 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 - ref: ${{ matrix.canonical-tag }} + fetch-depth: 0 + ref: ${{ matrix.canonical-tag }} - uses: dev-drprasad/delete-tag-and-release@v1.0 # We force delete an existing tag because otherwise we won't get diff --git a/.github/workflows/Tests.yaml b/.github/workflows/Tests.yaml index 09be6e99..e2a3fce0 100644 --- a/.github/workflows/Tests.yaml +++ b/.github/workflows/Tests.yaml @@ -40,8 +40,8 @@ on: default: 'cache' type: choice options: - - cache - - registry + - cache + - registry cache-key: description: 'Cache key (when fetching from cache)' required: false @@ -51,7 +51,7 @@ on: required: true type: string default: '.vulnerability-report.json' - external_ref_id: # (1) + external_ref_id: #(1) description: 'Optional ID for unique run detection' required: false type: string @@ -66,26 +66,13 @@ env: DIVE_IMAGE: 'wagoodman/dive:v0.12' jobs: - access-check: - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - name: Validate access to triggered image - uses: ./.github/actions/validate-actor - if: ${{ github.repository == 'canonical/oci-factory' && !github.event.pull_request.head.repo.fork }} - with: - admin-only: true - image-path: ${{ inputs.oci-image-path }} - github-token: ${{ secrets.ROCKSBOT_TOKEN }} - fetch-oci-image: runs-on: ubuntu-22.04 name: Fetch OCI image for testing - needs: [access-check] outputs: test-cache-key: ${{ steps.cache.outputs.key }} steps: - - name: ${{ inputs.external_ref_id }} # (2) + - name: ${{ inputs.external_ref_id }} #(2) run: echo 'Started by ${{ inputs.external_ref_id }}' >> "$GITHUB_STEP_SUMMARY" - uses: actions/cache/restore@v4 diff --git a/.github/workflows/Vulnerability-Scan.yaml b/.github/workflows/Vulnerability-Scan.yaml index 99adb42c..fb94c993 100644 --- a/.github/workflows/Vulnerability-Scan.yaml +++ b/.github/workflows/Vulnerability-Scan.yaml @@ -40,18 +40,9 @@ jobs: outputs: vulnerability-report: ${{ steps.vulnerability-report.outputs.name }} notify: ${{ steps.check-report.outputs.notify }} - vulnerabilities: ${{ steps.check-report.outputs.vulnerabilities }} steps: - uses: actions/checkout@v4 - - name: Validate access to triggered image - uses: ./.github/actions/validate-actor - if: ${{ github.repository == 'canonical/oci-factory' && !github.event.pull_request.head.repo.fork }} - with: - admin-only: true - image-path: ${{ inputs.oci-image-path }} - github-token: ${{ secrets.ROCKSBOT_TOKEN }} - - id: vulnerability-report run: | full_name="${{ inputs.oci-image-name }}${{ inputs.vulnerability-report-suffix }}" @@ -112,24 +103,18 @@ jobs: image-ref: '${{ steps.to-docker-daemon.outputs.name }}' - name: Process report - if: ${{ !cancelled() }} + if: ${{ always() }} id: check-report run: | report="${{ steps.vulnerability-report.outputs.name }}" + cat $report echo "notify=false" >> "$GITHUB_OUTPUT" set -x - vulnerabilities="$(jq -r -c '[ - try(.scanner.result.Results[]) - | .Target as $target - | .Vulnerabilities - | select(. != null) - | .[] - | {Target: $target, LastModifiedDate: .LastModifiedDate, VulnerabilityID: .VulnerabilityID, - PkgName: .PkgName, Severity: .Severity} - ]' < $report)" - echo "vulnerabilities=$vulnerabilities" >> "$GITHUB_OUTPUT" - last_modified_dates="$(echo "$vulnerabilities" | jq -r '.[] | select(.LastModifiedDate != null) | .LastModifiedDate')" - cat "$GITHUB_OUTPUT" + last_modified_dates="$(jq -r 'try(.scanner.result.Results[].Vulnerabilities) + | select(. != null) + | .[].LastModifiedDate + | select(. != null)' < $report)" + # We want to notify only if the CVEs have been updated since the last # time this scan ran for cve_updated in $last_modified_dates @@ -142,13 +127,13 @@ jobs: done - uses: actions/cache/save@v4 - if: ${{ !cancelled() }} + if: ${{ always() }} with: path: ${{ steps.vulnerability-report.outputs.name }} key: ${{ github.run_id }}-${{ steps.vulnerability-report.outputs.name }} - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} + if: ${{ always() }} with: name: ${{ steps.vulnerability-report.outputs.name }} path: ${{ steps.vulnerability-report.outputs.name }} @@ -161,7 +146,7 @@ jobs: name: Notify on failure needs: - test-vulnerabilities - if: ${{ !cancelled() && needs.test-vulnerabilities.outputs.notify == 'true' }} + if: ${{ always() && needs.test-vulnerabilities.outputs.notify == 'true' }} steps: - uses: actions/checkout@v4 @@ -184,90 +169,9 @@ jobs: URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} SUMMARY: '' FOOTER: '' - TITLE: 'Vulnerabilities found for ${{ inputs.oci-image-name }}' + TITLE: 'CVEs found for ${{ inputs.oci-image-name }}' run: | for channel in $(echo ${{ steps.get-contacts.outputs.mattermost-channels }} | tr ',' ' ') do MM_CHANNEL_ID="${channel}" ./src/notifications/send_to_mattermost.sh done - - issue: - runs-on: ubuntu-22.04 - name: Create issue - needs: - - test-vulnerabilities - env: - GITHUB_TOKEN: ${{ secrets.ROCKSBOT_TOKEN }} - if: ${{ !cancelled() && github.event_name != 'pull_request' }} - steps: - - uses: actions/checkout@v4 - - - id: simplify-image-name - run: | - img_name=$(echo "${{ inputs.oci-image-name }}" | sed -r 's|.*/([a-zA-Z0-9-]+:[0-9.-]+)_[0-9]+|\1|') - echo "img_name=$img_name" >> "$GITHUB_OUTPUT" - - # We assume that the sources within image.yaml are the same - - name: Get image repo - id: get-image-repo - run: | - img_repo=$(yq -r '.upload.[].source' ${{ github.workspace }}/${{ inputs.oci-image-path }}/image.yaml | head -n 1) - echo "img-repo=$img_repo" >> "$GITHUB_OUTPUT" - - # We have to walk through the vulnerabilities since trivy does not support outputting the results as Markdown - - name: Create Markdown Content - id: create-markdown - run: | - set -x - title="Vulnerabilities found for ${{ steps.simplify-image-name.outputs.img_name }}" - echo "## $title" > issue.md - echo "| ID | Target | Severity | Package |" >> issue.md - echo "| -- | ----- | -------- | ------- |" >> issue.md - echo '${{ needs.test-vulnerabilities.outputs.vulnerabilities }}' | jq -r '.[] | "| \(.VulnerabilityID) | /\(.Target) | \(.Severity) | \(.PkgName) |"' >> issue.md - echo -e "\nDetails: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> issue.md - num_vulns=$(echo '${{ needs.test-vulnerabilities.outputs.vulnerabilities }}' | jq -r 'length') - echo "issue-title=$title" >> "$GITHUB_OUTPUT" - echo "issue-body-file=issue.md" >> "$GITHUB_OUTPUT" - echo "vulnerability-exists=$([[ $num_vulns -gt 0 ]] && echo 'true' || echo 'false')" >> "$GITHUB_OUTPUT" - - - id: issue-exists - run: | - issue_number=$(gh issue list --repo ${{ steps.get-image-repo.outputs.img-repo }} --json "number,title" \ - | jq -r '.[] | select(.title == "${{ steps.create-markdown.outputs.issue-title }}") | .number') - echo "issue-exists=$([[ -n "$issue_number" ]] && echo 'true' || echo 'false')" >> "$GITHUB_OUTPUT" - echo "issue-number=$issue_number" >> "$GITHUB_OUTPUT" - - - # Truth table for issue creation - # | issue-exists | notify | vulnerability-exists | op | - # |--------------|--------|----------------------|--------| - # | T | T | T | update | - # | T | T | F | never | - # | T | F | T | nop | - # | T | F | F | close | - # | F | T | T | create | - # | F | T | F | never | - # | F | F | T | create | - # | F | F | F | nop | - - - name: Notify via GitHub issue - if: ${{ steps.create-markdown.outputs.vulnerability-exists == 'true' }} - run: | - set -x - op=nop - if [[ ${{ steps.issue-exists.outputs.issue-exists }} == 'false' ]]; then - op="create" - elif [[ ${{ steps.issue-exists.outputs.issue-exists }} == 'true' \ - && ${{ needs.test-vulnerabilities.outputs.notify }} == 'true' ]]; then - op="edit ${{ steps.issue-exists.outputs.issue-number }}" - fi - if [[ $op != 'nop' ]]; then - gh issue $op --repo ${{ steps.get-image-repo.outputs.img-repo }} \ - --title "Vulnerabilities found for ${{ steps.simplify-image-name.outputs.img_name }}" \ - --body-file "${{ steps.create-markdown.outputs.issue-body-file }}" - fi - - - name: Close issue - if: ${{ needs.test-vulnerabilities.result == 'success' && steps.issue-exists.outputs.issue-exists == 'true' && steps.create-markdown.outputs.vulnerability-exists == 'false' }} - run: | - gh issue close ${{ steps.issue-exists.outputs.issue-number }} --repo ${{ steps.get-image-repo.outputs.img-repo }} diff --git a/.github/workflows/_Auto-updates.yaml b/.github/workflows/_Auto-updates.yaml index 6eea9edb..a522d22b 100644 --- a/.github/workflows/_Auto-updates.yaml +++ b/.github/workflows/_Auto-updates.yaml @@ -48,11 +48,11 @@ jobs: - name: Install dependencies continue-on-error: true - working-directory: tools/workflow-engine/charms/temporal-worker + working-directory: src/workflow-engine/charms/temporal-worker run: poetry install - name: Trigger auto-updates - working-directory: tools/workflow-engine/charms/temporal-worker/oci_factory/activities/ + working-directory: src/workflow-engine/charms/temporal-worker/oci_factory/activities/ env: OS_AUTH_URL: ${{ secrets.SWIFT_OS_AUTH_URL }} OS_USERNAME: ${{ secrets.SWIFT_OS_USERNAME }} diff --git a/.github/workflows/_CLI-Client.yaml b/.github/workflows/_CLI-Client.yaml deleted file mode 100644 index e09df58f..00000000 --- a/.github/workflows/_CLI-Client.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: CLI-Client -on: - push: - paths: - - "tools/cli-client/**" - pull_request: - paths: - - "tools/cli-client/**" - -jobs: - test: - runs-on: ubuntu-latest - name: Test - steps: - - uses: actions/checkout@v4 - - name: Set up Go 1.22 - uses: actions/setup-go@v2 - with: - go-version: 1.22 - - name: Test - working-directory: tools/cli-client - run: | - go test ./... - - build: - runs-on: ubuntu-latest - name: Snap build - steps: - - uses: actions/checkout@v4 - - uses: snapcore/action-build@v1 - id: snapcraft - with: - path: tools/cli-client - - uses: actions/upload-artifact@v3 - with: - name: snap - path: ${{ steps.snapcraft.outputs.snap }} diff --git a/.github/workflows/_Test-OCI-Factory.yaml b/.github/workflows/_Test-OCI-Factory.yaml index e11c18eb..dab257f9 100644 --- a/.github/workflows/_Test-OCI-Factory.yaml +++ b/.github/workflows/_Test-OCI-Factory.yaml @@ -4,101 +4,18 @@ on: push: paths: - ".github/workflows/*" - - ".github/actions/**" - "!.github/workflows/CLA-Check.yaml" - "!.github/workflows/PR-Validator.yaml" - "!.github/workflows/_Auto-updates.yaml" - "!.github/workflows/Continuous-Testing.yaml" - - "!.github/workflows/CLI-Client.yaml" - "examples/**" - "oci/mock*" - "src/**" - - "tools/**" - - "tests/**" - - "!tools/workflow-engine/**" - - "!tools/cli-client/**" - -env: - # local path to clone the oci-factory to - - # path of pytest junit output - PYTEST_RESULT_PATH: pytest_results.xml + - "!src/workflow-engine/**" jobs: - access-check: - name: Validate access to mock-rock - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/validate-actor - with: - admin-only: true - image-path: "oci/mock-rock" - github-token: ${{ secrets.ROCKSBOT_TOKEN }} - - pytest: - # Trigger python unit tests across the repository - name: pytest - runs-on: ubuntu-22.04 - steps: - # Job Setup - - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - # Note: Add additional dependency installation lines as required below - # test-oci-factory/pytest requirements - - run: pip install -r tests/etc/requirements.txt - - # build_rock/configure requirements - - run: pip install -r src/build_rock/configure/requirements.txt - - - name: Run pytest - continue-on-error: true - run: | - python3 -m pytest --junit-xml "${{ env.PYTEST_RESULT_PATH }}" - - - name: Generate Summary - if: ${{ !cancelled() }} - run: | - python3 -m tools.junit_to_markdown --input-junit "${{ env.PYTEST_RESULT_PATH }}" >> $GITHUB_STEP_SUMMARY - - - name: Upload pytest Result - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v4 - with: - name: ${{ env.PYTEST_RESULT_PATH }} - path: ${{ env.PYTEST_RESULT_PATH }} - if-no-files-found: error - - bats-test: - # Trigger bash unit tests across the repository - name: bats - runs-on: ubuntu-22.04 - steps: - # Job Setup - - uses: actions/checkout@v4 - with: - fetch-depth: 1 - - - name: Install bats - run: | - sudo apt-get update - sudo apt-get install -y bats - - - name: Run bats - env: - GITHUB_TOKEN: ${{ secrets.ROCKSBOT_TOKEN }} - run: | - find ${{ github.workspace }} -name 'test-*.bats' | xargs bats - test-workflows: name: Trigger internal tests for mock-rock - needs: [access-check] uses: ./.github/workflows/Image.yaml with: oci-image-name: "mock-rock" diff --git a/.gitignore b/.gitignore index 90072ac8..5532c25d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ __pycache__ .env -tools/workflow-engine/charms/temporal-worker/config.yml -tools/workflow-engine/charms/temporal-worker/dist +src/workflow-engine/charms/temporal-worker/config.yml +src/workflow-engine/charms/temporal-worker/dist *.rock .terraform *terraform.tfstate* diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 0afe0e53..00000000 --- a/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @canonical/rocks diff --git a/README.md b/README.md index 10b4c170..369c13bf 100644 --- a/README.md +++ b/README.md @@ -70,12 +70,7 @@ Pull Requests 🚀. ### As a **Maintainer** ⛏ 🪨 -Maintainers can request new image builds and releases by either creating -Pull Requests (PRs) or via the OCI Factory CLI Client. - -#### 1. With Pull Requests - -Maintainers can use Pull Requests (PRs) as an interface for asking the OCI +Maintainers shall use Pull Requests (PRs) as an interface for asking the OCI Factory to build their images. Here's what you should do and know before you make your first PR as a Maintainer: @@ -89,25 +84,6 @@ multiple images; - that means one PR can only propose changes to [Maintainer files](#maintainer-files) within a single `oci` folder. -#### 2. With the CLI Client - -Maintainers can use the OCI Factory's CLI Client to interact with the OCI -Factory. The CLI Client is a Go module at [tools/cli-client](tools/cli-client). - -Here's what you should do and know before you use the CLI Client: - -1. you **must** already be an onboarded Maintainer and have had significant -interactions with the OCI Factory (via PRs). Why is that? To use this client -you'll be granted write permissions to the repository, and thus you'll have -escalated rights when compared to a regular Maintainer - with great power -comes great maintainability, -2. each use of the CLI Client **must** only target one version of an OCI image, -i.e. you can ask the OCI Factory to build one version of the image, upload and -release it to GHCR, Docker Hub, and ECR with different tracks, risks and EOL with -a single command, but **not** multiple versions of the same image. - -Further documentation regarding the CLI Client can be found [here](tools/cli-client/README.md). - Refer to the diagram below to understand the oci-factory workflow: ![OCI Factory Workflow](assets/img/oci-factory-workflow.png) diff --git a/assets/img/oci-factory-workflow.png b/assets/img/oci-factory-workflow.png index 8e274932bf3caa1ff28b302f578ebde77953c77c..4ea09a0bb502e4cfc2520c70bd55cc3ab8e8ed8f 100644 GIT binary patch literal 111218 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 z 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 diff --git a/tests/data/rockcraft.yaml b/examples/mock-rock/1.0/rockcraft.yaml similarity index 75% rename from tests/data/rockcraft.yaml rename to examples/mock-rock/1.0/rockcraft.yaml index a26aeabe..08607349 100644 --- a/tests/data/rockcraft.yaml +++ b/examples/mock-rock/1.0/rockcraft.yaml @@ -1,18 +1,16 @@ -# Metadata section - -name: hello +name: mock-rock summary: Hello World description: The most basic example of a rock. -version: "latest" -license: Apache-2.0 - +version: "1.0" base: bare build-base: ubuntu@22.04 +license: Apache-2.0 platforms: amd64: + arm64: parts: hello: plugin: nil stage-packages: - - hello + - hello \ No newline at end of file diff --git a/examples/mock-rock/1.1/rockcraft.yaml b/examples/mock-rock/1.1/rockcraft.yaml new file mode 100644 index 00000000..8ab6e150 --- /dev/null +++ b/examples/mock-rock/1.1/rockcraft.yaml @@ -0,0 +1,15 @@ +name: mock-rock +summary: Hello World +description: The most basic example of a rock. +version: "1.1" +base: bare +build-base: ubuntu@22.04 +license: Apache-2.0 +platforms: + amd64: + +parts: + hello: + plugin: nil + stage-packages: + - hello \ No newline at end of file diff --git a/examples/mock-rock/1.2/rockcraft.yaml b/examples/mock-rock/1.2/rockcraft.yaml new file mode 100644 index 00000000..4045518f --- /dev/null +++ b/examples/mock-rock/1.2/rockcraft.yaml @@ -0,0 +1,16 @@ +name: mock-rock +summary: Hello World +description: The most basic example of a rock. +version: "1.2" +base: bare +build-base: ubuntu@22.04 +license: Apache-2.0 +platforms: + ppc64el: + amd64: + +parts: + hello: + plugin: nil + stage-packages: + - hello \ No newline at end of file diff --git a/oci/alertmanager/_releases.json b/oci/alertmanager/_releases.json index ae39e36d..7963bdc8 100644 --- a/oci/alertmanager/_releases.json +++ b/oci/alertmanager/_releases.json @@ -1,107 +1,107 @@ { "0.25.0-22.04": { "stable": { - "target": "59" + "target": "44" }, "candidate": { - "target": "59" + "target": "44" }, "beta": { - "target": "59" + "target": "44" }, "edge": { - "target": "59" + "target": "44" }, "end-of-life": "2025-03-13T00:00:00Z" }, "0.25-22.04": { "stable": { - "target": "59" + "target": "44" }, "candidate": { - "target": "59" + "target": "44" }, "beta": { - "target": "59" + "target": "44" }, "edge": { - "target": "59" + "target": "44" }, "end-of-life": "2025-03-13T00:00:00Z" }, "0-22.04": { "stable": { - "target": "61" + "target": "46" }, "candidate": { - "target": "61" + "target": "46" }, "beta": { - "target": "61" + "target": "46" }, "edge": { - "target": "61" + "target": "46" }, "end-of-life": "2025-03-13T00:00:00Z" }, "0.26.0-22.04": { "stable": { - "target": "60" + "target": "45" }, "candidate": { - "target": "60" + "target": "45" }, "beta": { - "target": "60" + "target": "45" }, "edge": { - "target": "60" + "target": "45" }, "end-of-life": "2025-03-13T00:00:00Z" }, "0.26-22.04": { "stable": { - "target": "60" + "target": "45" }, "candidate": { - "target": "60" + "target": "45" }, "beta": { - "target": "60" + "target": "45" }, "edge": { - "target": "60" + "target": "45" }, "end-of-life": "2025-03-13T00:00:00Z" }, "0.27.0-22.04": { "end-of-life": "2025-03-13T00:00:00Z", "stable": { - "target": "61" + "target": "46" }, "candidate": { - "target": "61" + "target": "46" }, "beta": { - "target": "61" + "target": "46" }, "edge": { - "target": "61" + "target": "46" } }, "0.27-22.04": { "end-of-life": "2025-03-13T00:00:00Z", "stable": { - "target": "61" + "target": "46" }, "candidate": { - "target": "61" + "target": "46" }, "beta": { - "target": "61" + "target": "46" }, "edge": { - "target": "61" + "target": "46" } } } \ No newline at end of file diff --git a/oci/hydra/.trivyignore b/oci/hydra/.trivyignore deleted file mode 100644 index 6a1ac181..00000000 --- a/oci/hydra/.trivyignore +++ /dev/null @@ -1,8 +0,0 @@ -# Upstream CVEs - -# github.com/jackc/pgproto3/v2 - pgproto3 SQL Injection via Protocol Message Size Overflow -GHSA-7jwh-3vrq-q3m8 -# github.com/jackc/pgx/v4 - pgx SQL Injection via Line Comment Creation -CVE-2024-27289 -# github.com/jackc/pgx/v4 - pgx SQL Injection via Protocol Message Size Overflow -CVE-2024-27304 \ No newline at end of file diff --git a/oci/hydra/_releases.json b/oci/hydra/_releases.json deleted file mode 100644 index c6073e04..00000000 --- a/oci/hydra/_releases.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "2.2.0-22.04": { - "end-of-life": "2025-05-01T00:00:00Z", - "stable": { - "target": "1" - }, - "candidate": { - "target": "1" - }, - "beta": { - "target": "2.2.0-22.04_candidate" - }, - "edge": { - "target": "1" - } - } -} \ No newline at end of file diff --git a/oci/hydra/contacts.yaml b/oci/hydra/contacts.yaml deleted file mode 100644 index 8daeadeb..00000000 --- a/oci/hydra/contacts.yaml +++ /dev/null @@ -1,7 +0,0 @@ -notify: - emails: - - identity.charmers@lists.launchpad.net - mattermost-channels: - - ofi4for9obfq8m978h318x56ar -maintainers: - - canonical-iam \ No newline at end of file diff --git a/oci/hydra/documentation.yaml b/oci/hydra/documentation.yaml deleted file mode 100644 index 7f14835d..00000000 --- a/oci/hydra/documentation.yaml +++ /dev/null @@ -1,48 +0,0 @@ -version: 1 -application: hydra -is_chiselled: True -description: | - Ory Hydra is a hardened, OpenID Certified OAuth 2.0 Server and OpenID Connect Provider - optimized for low-latency, high throughput, and low resource consumption. - Ory Hydra enables you to become an OAuth 2.0 and OpenID Connect provider. - If you're not writing a basic web app but something that has to work on different devices, - that has machine-2-machine interaction, or enables third-party developers to use your API - (and pay for it), then this is what you're looking for. -docker: - parameters: - - -p 4444:4444 - - -p 4445:4445 - access: Access your Hydra Public API at `http://localhost:4444`, Admin API at `http://localhost:4445`. -parameters: - - type: -e - value: 'TRACING_ENABLED=true' - description: Tracing enablement. - - type: -e - value: 'TRACING_PROVIDER=otel' - description: Tracing protocol to be used. - - type: -e - value: 'TRACING_PROVIDERS_OTLP_INSECURE=true' - description: Allow Tracing via non TLS/insecure communication. - - type: -e - value: 'TRACING_PROVIDERS_OTLP_SAMPLING_SAMPLING_RATIO=1.0' - description: Tracing sampling ratio. - - type: -e - value: 'TRACING_PROVIDERS_OTLP_SERVER_URL=tempo.server.io:4318' - description: Tracing server url and port. - - type: -p - value: '4444:4444' - description: Hydra Public API port. - - type: -p - value: '4445:4445' - description: Hydra Admin API port. - - type: -v - value: "/path/to/hydra/config.yaml:/hydra.yaml" - description: > - Hydra config contains all the information needed to successfully configure it as an OIDC - Provider, see https://github.com/ory/hydra/blob/master/internal/config/config.yaml as a reference - - type: CMD - value: "hydra serve all --config /hydra.yaml" - description: > - Launch Hydra web server(s) using a mix of environment variables and the config mounted via volume. -debug: - text: "" diff --git a/oci/hydra/image.yaml b/oci/hydra/image.yaml deleted file mode 100644 index 2a2d27d2..00000000 --- a/oci/hydra/image.yaml +++ /dev/null @@ -1,12 +0,0 @@ -version: 1 -upload: - - source: "canonical/hydra-rock" - commit: 3c27fb428fad0a339c39355b2f8cd5a477d32014 - directory: . - release: - 2.2.0-22.04: - risks: - - stable - - candidate - - edge - end-of-life: "2025-05-01T00:00:00Z" \ No newline at end of file diff --git a/oci/identity-platform-admin-ui/_releases.json b/oci/identity-platform-admin-ui/_releases.json deleted file mode 100644 index e1b831e8..00000000 --- a/oci/identity-platform-admin-ui/_releases.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "1.19.0-22.04": { - "end-of-life": "2025-03-01T00:00:00Z", - "stable": { - "target": "1" - }, - "candidate": { - "target": "1" - }, - "beta": { - "target": "1.19.0-22.04_candidate" - }, - "edge": { - "target": "1" - } - }, - "1-22.04": { - "end-of-life": "2025-04-24T00:00:00Z", - "stable": { - "target": "2" - }, - "candidate": { - "target": "1-22.04_stable" - }, - "beta": { - "target": "1-22.04_candidate" - }, - "edge": { - "target": "1-22.04_beta" - } - }, - "1.21.0-22.04": { - "end-of-life": "2024-11-07T00:00:00Z", - "candidate": { - "target": "3" - }, - "beta": { - "target": "1.21.0-22.04_candidate" - }, - "edge": { - "target": "3" - } - } -} \ No newline at end of file diff --git a/oci/identity-platform-admin-ui/contacts.yaml b/oci/identity-platform-admin-ui/contacts.yaml deleted file mode 100644 index 8daeadeb..00000000 --- a/oci/identity-platform-admin-ui/contacts.yaml +++ /dev/null @@ -1,7 +0,0 @@ -notify: - emails: - - identity.charmers@lists.launchpad.net - mattermost-channels: - - ofi4for9obfq8m978h318x56ar -maintainers: - - canonical-iam \ No newline at end of file diff --git a/oci/identity-platform-admin-ui/documentation.yaml b/oci/identity-platform-admin-ui/documentation.yaml deleted file mode 100644 index b73b391f..00000000 --- a/oci/identity-platform-admin-ui/documentation.yaml +++ /dev/null @@ -1,139 +0,0 @@ -version: 1 -application: identity-platform-admin-ui -is_chiselled: True -description: | - Canonical IAM Admin UI is a component that allows you to interact with the components - that are part of the Identity Platform solution. - - It provides a set of API to view,modify and delete resources on Ory Kratos, Ory Hydra - Ory Oathkeeper and OpenFGA - - For further information check our repository on Github https://github.com/canonical/identity-platform-admin-ui -docker: - parameters: - - -p 8080:8080 - access: Access the API at `http://localhost:8080`. -parameters: - - type: -e - value: 'TRACING_ENABLED=true' - description: Tracing enablement. - - type: -e - value: 'OTEL_GRPC_ENDPOINT=tempo-0.tempo-endpoints.stg-identity-jaas-dev.svc.cluster.local:4317' - description: Tracing server GRPC endpoint, has priority on OTEL_HTTP_ENDPOINT. - - type: -e - value: 'OTEL_HTTP_ENDPOINT=http://tempo-0.tempo-endpoints.stg-identity-jaas-dev.svc.cluster.local:4318' - description: Tracing server HTTP endpoint. - - type: -e - value: 'MFA_ENABLED="true"' - description: Enable MFA validation on logins. - - type: -e - value: 'HYDRA_ADMIN_URL=http://hydra.io:4445' - description: Hydra Admin API URL, used to manage clients - - type: -e - value: 'KRATOS_ADMIN_URL=http://kratos.io:4434' - description: Kratos Admin API URL, used to manage identities - - type: -e - value: 'KRATOS_PUBLIC_URL=http://kratos.io:4433' - description: Kratos Public API URL, used to manage identities - - type: -e - value: 'OATHKEEPER_PUBLIC_URL=http://oathkeeper.io:4455' - description: Oathkeeper Public API URL, used to manage rules - - type: -e - value: 'BASE_URL=https://iam.io/dev/path' - description: Public URL Login UI will be served from. - - type: -e - value: 'ACCESS_TOKEN_VERIFICATION_STRATEGY=jwks' - description: Strategy used to verify JWT tokens. - - type: -e - value: 'AUTHENTICATION_ENABLED="true"' - description: Authentication enable flag. - - type: -e - value: 'AUTHORIZATION_ENABLED="true"' - description: Authorization enable flag. - - type: -e - value: 'CONTEXT_PATH=/dev/path' - description: Path needed by the UI to work behind an ingress proxy. - - type: -e - value: 'IDP_CONFIGMAP_NAME=providers' - description: Name of kubernetes configmap where Kratos IDP are configured. - - type: -e - value: 'IDP_CONFIGMAP_NAMESPACE=default' - description: Namespace of kubernetes configmap where Kratos IDP are configured. - - type: -e - value: 'RULES_CONFIGMAP_NAME=rules' - description: Name of kubernetes configmap where Oathkeeper rules are configured. - - type: -e - value: 'RULES_CONFIGMAP_NAMESPACE=default' - description: Namespace of kubernetes configmap where Oathkeeper rules are configured. - - type: -e - value: 'RULES_CONFIGMAP_FILENAME=rules.yaml' - description: Name of the file where Oathkeeper rules are configured. - - type: -e - value: 'SCHEMAS_CONFIGMAP_NAME=schemas' - description: Name of kubernetes configmap where Kratos identity schemas are configured. - - type: -e - value: 'SCHEMAS_CONFIGMAP_NAMESPACE=default' - description: Namespace of kubernetes configmap where Kratos identity schemas are configured. - - type: -e - value: 'MAIL_FROM_ADDRESS=iam@canonical.com' - description: Email sender - - type: -e - value: 'MAIL_HOST=smtp.io' - description: SMPT server host - - type: -e - value: 'MAIL_PASSWORD="***********************************"' - description: SMTP password - - type: -e - value: 'MAIL_PORT="1025"' - description: SMTP server port - - type: -e - value: 'MAIL_USERNAME="***********************************"' - description: SMTP password - - type: -e - value: 'OAUTH2_AUTH_COOKIES_ENCRYPTION_KEY="***********************************"' - description: Key used to encrypt authentication cookies - - type: -e - value: 'OAUTH2_CLIENT_ID=***********************************' - description: OAuth2 client ID, needed for OIDC authentication - - type: -e - value: 'OAUTH2_CLIENT_SECRET=***********************************' - description: OAuth2 client secret, needed for OIDC authentication - - type: -e - value: 'OAUTH2_CODEGRANT_SCOPES=openid,email,profile,offline_access' - description: OAuth2 scopes needed by the application, needed for OIDC authentication - - type: -e - value: 'OAUTH2_REDIRECT_URI=https://iam..io/dev/api/v0/auth/callback' - description: OAuth2 redirect uri where /api/v0/auth/callback is the endpoint used by the application, needed for OIDC authentication - - type: -e - value: 'OIDC_ISSUER=https://iam.dev.canonical.com/stg-identity-jaas-dev-hydra' - description: OAuth2 server issuer - - type: -e - value: 'OPENFGA_API_HOST=openfga:8443' - description: OpenFGA server address - - type: -e - value: 'OPENFGA_API_SCHEME=http' - description: OpenFGA server scheme - - type: -e - value: 'OPENFGA_API_TOKEN=***********************************' - description: OpenFGA server API token, needed for authentication to the server - - type: -e - value: 'OPENFGA_AUTHORIZATION_MODEL_ID=***********************************' - description: OpenFGA model ID - - type: -e - value: 'OPENFGA_STORE_ID=***********************************' - description: OpenFGA store ID - - type: -e - value: 'LOG_FILE=log.txt' - description: Destination file for logs. - - type: -e - value: 'LOG_LEVEL=error' - description: Log level. - - type: -p - value: '8080:8080' - description: Server API port. - - type: CMD - value: '/usr/bin/identity-platform-admin-ui serve' - description: > - Launch Admin UI web server(s) using environment variables. -debug: - text: "" \ No newline at end of file diff --git a/oci/identity-platform-admin-ui/image.yaml b/oci/identity-platform-admin-ui/image.yaml deleted file mode 100644 index dab8f656..00000000 --- a/oci/identity-platform-admin-ui/image.yaml +++ /dev/null @@ -1,12 +0,0 @@ -version: 1 -upload: - - source: "canonical/identity-platform-admin-ui" - commit: c46a9568f9be665f86aa5a274d8ac9d90054ba6b - directory: . - release: - 1.19.0-22.04: - risks: - - stable - - candidate - - edge - end-of-life: "2025-03-01T00:00:00Z" \ No newline at end of file diff --git a/oci/identity-platform-login-ui/contacts.yaml b/oci/identity-platform-login-ui/contacts.yaml deleted file mode 100644 index 8daeadeb..00000000 --- a/oci/identity-platform-login-ui/contacts.yaml +++ /dev/null @@ -1,7 +0,0 @@ -notify: - emails: - - identity.charmers@lists.launchpad.net - mattermost-channels: - - ofi4for9obfq8m978h318x56ar -maintainers: - - canonical-iam \ No newline at end of file diff --git a/oci/identity-platform-login-ui/documentation.yaml b/oci/identity-platform-login-ui/documentation.yaml deleted file mode 100644 index e68b6629..00000000 --- a/oci/identity-platform-login-ui/documentation.yaml +++ /dev/null @@ -1,54 +0,0 @@ -version: 1 -application: identity-platform-login-ui -is_chiselled: True -description: | - Canonical IAM Login UI is a core components of the Identity Platform solution. - - It provides a way to login using OIDC via interactions with Ory Kratos and Ory Hydra, also allows - you to manage self service functionalities for everything related to authentication - - For further information check our repository on Github https://github.com/canonical/identity-platform-login-ui -docker: - parameters: - - -p 8080:8080 - access: Access the API at `http://localhost:8080`. -parameters: - - type: -e - value: 'TRACING_ENABLED=true' - description: Tracing enablement. - - type: -e - value: 'OTEL_GRPC_ENDPOINT=tempo-0.tempo-endpoints.stg-identity-jaas-dev.svc.cluster.local:4317' - description: Tracing server GRPC endpoint, has priority on OTEL_HTTP_ENDPOINT. - - type: -e - value: 'OTEL_HTTP_ENDPOINT=http://tempo-0.tempo-endpoints.stg-identity-jaas-dev.svc.cluster.local:4318' - description: Tracing server HTTP endpoint. - - type: -e - value: 'MFA_ENABLED="true"' - description: Enable MFA validation on logins. - - type: -e - value: 'HYDRA_ADMIN_URL=http://hydra.io:4445' - description: Hydra Admin API URL, used to validate logins - - type: -e - value: 'KRATOS_ADMIN_URL=http://kratos.io:4434' - description: Kratos Admin API URL, used to manage identities - - type: -e - value: 'KRATOS_PUBLIC_URL=http://kratos.io:4433' - description: Kratos Public API URL, used to manage identities - - type: -e - value: 'BASE_URL=https://iam.io/dev/path' - description: Public URL Login UI will be served from. - - type: -e - value: 'LOG_FILE=log.txt' - description: Destination file for logs. - - type: -e - value: 'LOG_LEVEL=error' - description: Log level. - - type: -p - value: '8080:8080' - description: Server API port. - - type: CMD - value: '/usr/bin/identity-platform-login-ui serve' - description: > - Launch Login UI web server(s) using environment variables. -debug: - text: "" \ No newline at end of file diff --git a/oci/identity-platform-login-ui/image.yaml b/oci/identity-platform-login-ui/image.yaml deleted file mode 100644 index e59f8822..00000000 --- a/oci/identity-platform-login-ui/image.yaml +++ /dev/null @@ -1,12 +0,0 @@ -version: 1 -upload: - - source: "canonical/identity-platform-login-ui" - commit: 3c03717429801d1334ca7feb4dd2a2e2793ca4ff - directory: . - release: - 0.18.3-22.04: - risks: - - stable - - candidate - - edge - end-of-life: "2025-03-01T00:00:00Z" diff --git a/oci/kafka/_releases.json b/oci/kafka/_releases.json index ec0e59b4..f3f01c2e 100644 --- a/oci/kafka/_releases.json +++ b/oci/kafka/_releases.json @@ -2,7 +2,7 @@ "3.6-22.04": { "end-of-life": "2024-09-01T00:00:00Z", "edge": { - "target": "10" + "target": "7" } } } \ No newline at end of file diff --git a/oci/kratos/.trivyignore b/oci/kratos/.trivyignore deleted file mode 100644 index 9ebdf9c1..00000000 --- a/oci/kratos/.trivyignore +++ /dev/null @@ -1,10 +0,0 @@ -# Upstream CVEs - -# github.com/jackc/pgproto3/v2 - pgproto3 SQL Injection via Protocol Message Size Overflow -GHSA-7jwh-3vrq-q3m8 -# github.com/jackc/pgx/v4 - pgx SQL Injection via Line Comment Creation -CVE-2024-27289 -# github.com/jackc/pgx/v4 - pgx SQL Injection via Protocol Message Size Overflow -CVE-2024-27304 -# github.com/docker/docker - Authz zero length regression -CVE-2024-41110 diff --git a/oci/kratos/_releases.json b/oci/kratos/_releases.json deleted file mode 100644 index 834e571a..00000000 --- a/oci/kratos/_releases.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "1.1.0-22.04": { - "end-of-life": "2025-05-01T00:00:00Z", - "stable": { - "target": "1" - }, - "candidate": { - "target": "1" - }, - "beta": { - "target": "1.1.0-22.04_candidate" - }, - "edge": { - "target": "1" - } - } -} \ No newline at end of file diff --git a/oci/kratos/contacts.yaml b/oci/kratos/contacts.yaml deleted file mode 100644 index 8daeadeb..00000000 --- a/oci/kratos/contacts.yaml +++ /dev/null @@ -1,7 +0,0 @@ -notify: - emails: - - identity.charmers@lists.launchpad.net - mattermost-channels: - - ofi4for9obfq8m978h318x56ar -maintainers: - - canonical-iam \ No newline at end of file diff --git a/oci/kratos/documentation.yaml b/oci/kratos/documentation.yaml deleted file mode 100644 index c4ca5ebe..00000000 --- a/oci/kratos/documentation.yaml +++ /dev/null @@ -1,67 +0,0 @@ -version: 1 -application: kratos -is_chiselled: True -description: | - Ory Kratos is the developer-friendly, security-hardened and battle-tested Identity, - User Management and Authentication system for the Cloud. - - The identity management server Ory Kratos enables you to implement user management, - login and registration in a secure and straightforward way. - Don't rewrite every aspect of identity management yourself. - Ory Kratos implements all common flows such as login and logout, account activation, - mfa/2fa, profile and session management, user facing errors and account recovery methods. - Just spin up a docker image and write a simple UI for it in the language or framework of - your choice. -docker: - parameters: - - -p 4433:4433 - - -p 4434:4434 - access: Access your Kratos Public API at `http://localhost:4433`, Admin API at `http://localhost:4434`. -parameters: - - type: -e - value: 'TRACING_ENABLED=true' - description: Tracing enablement. - - type: -e - value: 'TRACING_PROVIDER=otel' - description: Tracing protocol to be used. - - type: -e - value: 'TRACING_PROVIDERS_OTLP_INSECURE=true' - description: Allow Tracing via non TLS/insecure communication. - - type: -e - value: 'TRACING_PROVIDERS_OTLP_SAMPLING_SAMPLING_RATIO=1.0' - description: Tracing sampling ratio. - - type: -e - value: 'TRACING_PROVIDERS_OTLP_SERVER_URL=tempo.server.io:4318' - description: Tracing server url and port. - - type: -e - value: 'SERVE_PUBLIC_BASE_URL=https://kratos.io/dev/path' - description: Public URL kratos will be served from. - - type: -e - value: 'DSN=postgres://user:pass@postgresql:5432/db' - description: Database connection string for postgresql database. - - type: -e - value: 'HTTPS_PROXY=http://proxy.internal' - description: HTTPS proxy used in air gapped environments. - - type: -e - value: 'HTTP_PROXY=http://proxy.internal' - description: HTTP proxy used in air gapped environments. - - type: -e - value: 'NO_PROXY=*.canonical.com' - description: Domain that needs to be exluded from the proxy, used in air gapped environments. - - type: -p - value: '4433:4433' - description: Kratos Public API port. - - type: -p - value: '4434:4434' - description: Kratos Admin API port. - - type: -v - value: '/path/to/Kratos/config.yaml:/kratos.yaml' - description: > - Kratos config contains all the information needed to successfully configure it as an OIDC - Provider, see https://www.ory.sh/docs/kratos/reference/configuration as a reference - - type: CMD - value: 'kratos serve all --config /kratos.yaml' - description: > - Launch Kratos web server(s) using a mix of environment variables and the config mounted via volume. -debug: - text: "" diff --git a/oci/kratos/image.yaml b/oci/kratos/image.yaml deleted file mode 100644 index 230eb0ad..00000000 --- a/oci/kratos/image.yaml +++ /dev/null @@ -1,12 +0,0 @@ -version: 1 -upload: - - source: "canonical/kratos-rock" - commit: 396bf3a71cb65f97cf853540117858d6859ef43b - directory: . - release: - 1.1.0-22.04: - risks: - - stable - - candidate - - edge - end-of-life: "2025-05-01T00:00:00Z" \ No newline at end of file diff --git a/oci/loki/_releases.json b/oci/loki/_releases.json index 680c5caa..67d24a06 100644 --- a/oci/loki/_releases.json +++ b/oci/loki/_releases.json @@ -1,182 +1,182 @@ { "2.8.4-22.04": { "stable": { - "target": "84" + "target": "77" }, "candidate": { - "target": "84" + "target": "77" }, "beta": { - "target": "84" + "target": "77" }, "edge": { - "target": "84" + "target": "77" }, "end-of-life": "2025-03-14T00:00:00Z" }, "2.8-22.04": { "stable": { - "target": "84" + "target": "77" }, "candidate": { - "target": "84" + "target": "77" }, "beta": { - "target": "84" + "target": "77" }, "edge": { - "target": "84" + "target": "77" }, "end-of-life": "2025-03-14T00:00:00Z" }, "2-22.04": { "stable": { - "target": "89" + "target": "82" }, "candidate": { - "target": "89" + "target": "82" }, "beta": { - "target": "89" + "target": "82" }, "edge": { - "target": "89" + "target": "82" }, "end-of-life": "2025-05-28T00:00:00Z" }, "2.9.2-22.04": { "stable": { - "target": "85" + "target": "78" }, "candidate": { - "target": "85" + "target": "78" }, "beta": { - "target": "85" + "target": "78" }, "edge": { - "target": "85" + "target": "78" }, "end-of-life": "2025-03-14T00:00:00Z" }, "2.9-22.04": { "stable": { - "target": "89" + "target": "82" }, "candidate": { - "target": "89" + "target": "82" }, "beta": { - "target": "89" + "target": "82" }, "edge": { - "target": "89" + "target": "82" }, "end-of-life": "2025-05-28T00:00:00Z" }, "2.9.3-22.04": { "stable": { - "target": "86" + "target": "79" }, "candidate": { - "target": "86" + "target": "79" }, "beta": { - "target": "86" + "target": "79" }, "edge": { - "target": "86" + "target": "79" }, "end-of-life": "2025-03-14T00:00:00Z" }, "2.9.4-22.04": { "stable": { - "target": "87" + "target": "80" }, "candidate": { - "target": "87" + "target": "80" }, "beta": { - "target": "87" + "target": "80" }, "edge": { - "target": "87" + "target": "80" }, "end-of-life": "2025-03-14T00:00:00Z" }, "2.9.5-22.04": { "end-of-life": "2025-03-14T00:00:00Z", "stable": { - "target": "88" + "target": "81" }, "candidate": { - "target": "88" + "target": "81" }, "beta": { - "target": "88" + "target": "81" }, "edge": { - "target": "88" + "target": "81" } }, "2.9.6-22.04": { "end-of-life": "2025-05-28T00:00:00Z", "stable": { - "target": "89" + "target": "82" }, "candidate": { - "target": "89" + "target": "82" }, "beta": { - "target": "89" + "target": "82" }, "edge": { - "target": "89" + "target": "82" } }, "3.0.0-22.04": { "end-of-life": "2025-05-28T00:00:00Z", "stable": { - "target": "90" + "target": "83" }, "candidate": { - "target": "90" + "target": "83" }, "beta": { - "target": "90" + "target": "83" }, "edge": { - "target": "90" + "target": "83" } }, "3.0-22.04": { "end-of-life": "2025-05-28T00:00:00Z", "stable": { - "target": "90" + "target": "83" }, "candidate": { - "target": "90" + "target": "83" }, "beta": { - "target": "90" + "target": "83" }, "edge": { - "target": "90" + "target": "83" } }, "3-22.04": { "end-of-life": "2025-05-28T00:00:00Z", "stable": { - "target": "90" + "target": "83" }, "candidate": { - "target": "90" + "target": "83" }, "beta": { - "target": "90" + "target": "83" }, "edge": { - "target": "90" + "target": "83" } } } \ No newline at end of file diff --git a/oci/mimir/_releases.json b/oci/mimir/_releases.json index e2fc31f3..09afa446 100644 --- a/oci/mimir/_releases.json +++ b/oci/mimir/_releases.json @@ -1,182 +1,152 @@ { "2.6.0-22.04": { "stable": { - "target": "106" + "target": "71" }, "candidate": { - "target": "106" + "target": "71" }, "beta": { - "target": "106" + "target": "71" }, "edge": { - "target": "106" + "target": "71" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2.6-22.04": { "stable": { - "target": "106" + "target": "71" }, "candidate": { - "target": "106" + "target": "71" }, "beta": { - "target": "106" + "target": "71" }, "edge": { - "target": "106" + "target": "71" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2-22.04": { "stable": { - "target": "107" + "target": "70" }, "candidate": { - "target": "2-22.04_stable" + "target": "70" }, "beta": { - "target": "2-22.04_candidate" + "target": "70" }, "edge": { - "target": "2-22.04_beta" + "target": "70" }, - "end-of-life": "2025-01-08T00:00:00Z" + "end-of-life": "2025-05-28T00:00:00Z" }, "2.10.4-22.04": { "stable": { - "target": "102" + "target": "67" }, "candidate": { - "target": "102" + "target": "67" }, "beta": { - "target": "102" + "target": "67" }, "edge": { - "target": "102" + "target": "67" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2.10-22.04": { "stable": { - "target": "101" + "target": "66" }, "candidate": { - "target": "101" + "target": "66" }, "beta": { - "target": "101" + "target": "66" }, "edge": { - "target": "101" + "target": "66" }, "end-of-life": "2025-03-14T00:00:00Z" }, "2.10.0-22.04": { "stable": { - "target": "100" + "target": "65" }, "candidate": { - "target": "100" + "target": "65" }, "beta": { - "target": "100" + "target": "65" }, "edge": { - "target": "100" + "target": "65" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2.11.0-22.04": { "stable": { - "target": "104" + "target": "69" }, "candidate": { - "target": "104" + "target": "69" }, "beta": { - "target": "104" + "target": "69" }, "edge": { - "target": "104" + "target": "69" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2.11-22.04": { "stable": { - "target": "103" + "target": "68" }, "candidate": { - "target": "103" + "target": "68" }, "beta": { - "target": "103" + "target": "68" }, "edge": { - "target": "103" + "target": "68" }, "end-of-life": "2025-03-14T00:00:00Z" }, "2.12.0-22.04": { "end-of-life": "2025-05-28T00:00:00Z", "stable": { - "target": "105" + "target": "70" }, "candidate": { - "target": "105" + "target": "70" }, "beta": { - "target": "105" + "target": "70" }, "edge": { - "target": "105" + "target": "70" } }, "2.12-22.04": { "end-of-life": "2025-05-28T00:00:00Z", "stable": { - "target": "105" + "target": "70" }, "candidate": { - "target": "105" + "target": "70" }, "beta": { - "target": "105" + "target": "70" }, "edge": { - "target": "105" - } - }, - "2.13.0-22.04": { - "end-of-life": "2025-01-08T00:00:00Z", - "stable": { - "target": "107" - }, - "candidate": { - "target": "2.13.0-22.04_stable" - }, - "beta": { - "target": "2.13.0-22.04_candidate" - }, - "edge": { - "target": "2.13.0-22.04_beta" - } - }, - "2.13-22.04": { - "end-of-life": "2025-01-08T00:00:00Z", - "stable": { - "target": "107" - }, - "candidate": { - "target": "2.13-22.04_stable" - }, - "beta": { - "target": "2.13-22.04_candidate" - }, - "edge": { - "target": "2.13-22.04_beta" + "target": "70" } } } \ No newline at end of file diff --git a/oci/mimir/image.yaml b/oci/mimir/image.yaml index dc664416..31597273 100644 --- a/oci/mimir/image.yaml +++ b/oci/mimir/image.yaml @@ -1,18 +1,18 @@ version: 1 upload: - source: canonical/mimir-rock - commit: 130a427f31db55028183aa9842f7fdd6778ad733 - directory: 2.13.0 + commit: 2d5de24d16bb5909204614314536ea855d2defea + directory: 2.12.0 release: - 2.13.0-22.04: - end-of-life: "2025-01-08T00:00:00Z" + 2.12.0-22.04: + end-of-life: "2025-05-28T00:00:00Z" risks: - stable - 2.13-22.04: - end-of-life: "2025-01-08T00:00:00Z" + 2.12-22.04: + end-of-life: "2025-05-28T00:00:00Z" risks: - stable 2-22.04: - end-of-life: "2025-01-08T00:00:00Z" + end-of-life: "2025-05-28T00:00:00Z" risks: - stable diff --git a/oci/mock-rock/_releases.json b/oci/mock-rock/_releases.json index 1f7deab1..dcbeeee0 100644 --- a/oci/mock-rock/_releases.json +++ b/oci/mock-rock/_releases.json @@ -13,13 +13,13 @@ }, "1.0-22.04": { "candidate": { - "target": "583" + "target": "316" }, "beta": { - "target": "583" + "target": "316" }, "edge": { - "target": "583" + "target": "316" }, "end-of-life": "2025-05-01T00:00:00Z" }, @@ -35,31 +35,31 @@ "1.1-22.04": { "end-of-life": "2025-05-01T00:00:00Z", "candidate": { - "target": "641" + "target": "317" }, "beta": { - "target": "641" + "target": "317" }, "edge": { - "target": "641" + "target": "317" } }, "1-22.04": { "end-of-life": "2025-05-01T00:00:00Z", "candidate": { - "target": "641" + "target": "317" }, "beta": { - "target": "641" + "target": "317" }, "edge": { - "target": "641" + "target": "317" } }, "1.2-22.04": { "end-of-life": "2025-05-01T00:00:00Z", "beta": { - "target": "642" + "target": "318" }, "edge": { "target": "1.2-22.04_beta" diff --git a/oci/mock-rock/image.yaml b/oci/mock-rock/image.yaml index 75e22f84..d8670ad5 100644 --- a/oci/mock-rock/image.yaml +++ b/oci/mock-rock/image.yaml @@ -9,9 +9,9 @@ release: beta: 1.0-22.04_beta upload: - - source: "canonical/rocks-toolbox" - commit: 17916dd5de270e61a6a3fd3f4661a6413a50fd6f - directory: mock_rock/1.0 + - source: "canonical/oci-factory" + commit: 7f080b50ba656538aee3819889090c173f06debd + directory: examples/mock-rock/1.0 release: 1.0-22.04: end-of-life: "2024-05-01T00:00:00Z" @@ -19,9 +19,9 @@ upload: - candidate - edge - beta - - source: "canonical/rocks-toolbox" - commit: 17916dd5de270e61a6a3fd3f4661a6413a50fd6f - directory: mock_rock/1.1 + - source: "canonical/oci-factory" + commit: 7f080b50ba656538aee3819889090c173f06debd + directory: examples/mock-rock/1.1 release: 1.1-22.04: end-of-life: "2025-05-01T00:00:00Z" @@ -35,9 +35,9 @@ upload: - candidate - edge - beta - - source: "canonical/rocks-toolbox" - commit: 17916dd5de270e61a6a3fd3f4661a6413a50fd6f - directory: mock_rock/1.2 + - source: "canonical/oci-factory" + commit: 486799e24328410678c3534381a5473a46b07d06 + directory: examples/mock-rock/1.2 release: 1.2-22.04: end-of-life: "2025-05-01T00:00:00Z" diff --git a/oci/prometheus-pushgateway/_releases.json b/oci/prometheus-pushgateway/_releases.json index 96748829..d6852e79 100644 --- a/oci/prometheus-pushgateway/_releases.json +++ b/oci/prometheus-pushgateway/_releases.json @@ -1,137 +1,137 @@ { "1.6.2-22.04": { "stable": { - "target": "101" + "target": "66" }, "candidate": { - "target": "101" + "target": "66" }, "beta": { - "target": "101" + "target": "66" }, "edge": { - "target": "101" + "target": "66" }, "end-of-life": "2025-05-24T00:00:00Z" }, "1.6-22.04": { "stable": { - "target": "100" + "target": "65" }, "candidate": { - "target": "100" + "target": "65" }, "beta": { - "target": "100" + "target": "65" }, "edge": { - "target": "100" + "target": "65" }, "end-of-life": "2025-03-14T00:00:00Z" }, "1-22.04": { "stable": { - "target": "104" + "target": "69" }, "candidate": { - "target": "104" + "target": "69" }, "beta": { - "target": "104" + "target": "69" }, "edge": { - "target": "104" + "target": "69" }, "end-of-life": "2025-05-28T00:00:00Z" }, "1.6.1-22.04": { "stable": { - "target": "99" + "target": "64" }, "candidate": { - "target": "99" + "target": "64" }, "beta": { - "target": "99" + "target": "64" }, "edge": { - "target": "99" + "target": "64" }, "end-of-life": "2025-05-24T00:00:00Z" }, "1.6.0-22.04": { "stable": { - "target": "98" + "target": "63" }, "candidate": { - "target": "98" + "target": "63" }, "beta": { - "target": "98" + "target": "63" }, "edge": { - "target": "98" + "target": "63" }, "end-of-life": "2025-05-24T00:00:00Z" }, "1.7.0-22.04": { "stable": { - "target": "103" + "target": "68" }, "candidate": { - "target": "103" + "target": "68" }, "beta": { - "target": "103" + "target": "68" }, "edge": { - "target": "103" + "target": "68" }, "end-of-life": "2025-05-24T00:00:00Z" }, "1.7-22.04": { "stable": { - "target": "102" + "target": "67" }, "candidate": { - "target": "102" + "target": "67" }, "beta": { - "target": "102" + "target": "67" }, "edge": { - "target": "102" + "target": "67" }, "end-of-life": "2025-03-14T00:00:00Z" }, "1.8.0-22.04": { "end-of-life": "2025-05-28T00:00:00Z", "stable": { - "target": "104" + "target": "69" }, "candidate": { - "target": "104" + "target": "69" }, "beta": { - "target": "104" + "target": "69" }, "edge": { - "target": "104" + "target": "69" } }, "1.8-22.04": { "end-of-life": "2025-05-28T00:00:00Z", "stable": { - "target": "104" + "target": "69" }, "candidate": { - "target": "104" + "target": "69" }, "beta": { - "target": "104" + "target": "69" }, "edge": { - "target": "104" + "target": "69" } } } \ No newline at end of file diff --git a/oci/prometheus/.trivyignore b/oci/prometheus/.trivyignore index fcc7b4be..979472c3 100644 --- a/oci/prometheus/.trivyignore +++ b/oci/prometheus/.trivyignore @@ -6,17 +6,3 @@ CVE-2023-45142 CVE-2023-39325 # google.golang.org/grpc - gRPC-Go HTTP/2 Rapid Reset vulnerability GHSA-m425-mq94-257g -# github.com/docker/distribution - distribution/distribution: DoS from malicious API request -CVE-2023-2253 -# github.com/docker/docker - moby: Encrypted overlay network may be unauthenticated -CVE-2023-28840 -# github.com/emicklei/go-restful - go-restful: Authorization Bypass Through User-Controlled Key -CVE-2022-1996 -# golang.org/x/net - golang: net/http: handle server errors after sending GOAWAY -CVE-2022-27664 -# golang.org/x/net - x/net/http2/h2c: request smuggling -CVE-2022-41721 -# golang.org/x/net - net/http, golang.org/x/net/http2: avoid quadratic complexity in HPACK decoding -CVE-2022-41723 -# golang.org/x/text - golang: golang.org/x/text/language: ParseAcceptLanguage takes a long time to parse complex tags -CVE-2022-32149 diff --git a/oci/prometheus/_releases.json b/oci/prometheus/_releases.json index aec8fe42..ad0c81b7 100644 --- a/oci/prometheus/_releases.json +++ b/oci/prometheus/_releases.json @@ -1,272 +1,212 @@ { "2.46.0-22.04": { "stable": { - "target": "97" + "target": "88" }, "candidate": { - "target": "97" + "target": "88" }, "beta": { - "target": "97" + "target": "88" }, "edge": { - "target": "97" + "target": "88" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2.46-22.04": { "stable": { - "target": "96" + "target": "87" }, "candidate": { - "target": "96" + "target": "87" }, "beta": { - "target": "96" + "target": "87" }, "edge": { - "target": "96" + "target": "87" }, "end-of-life": "2025-03-14T00:00:00Z" }, "2-22.04": { "stable": { - "target": "104" + "target": "95" }, "candidate": { - "target": "104" + "target": "95" }, "beta": { - "target": "104" + "target": "95" }, "edge": { - "target": "104" + "target": "95" }, "end-of-life": "2025-05-28T00:00:00Z" }, "2.48.0-22.04": { "stable": { - "target": "99" + "target": "90" }, "candidate": { - "target": "99" + "target": "90" }, "beta": { - "target": "99" + "target": "90" }, "edge": { - "target": "99" + "target": "90" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2.48-22.04": { "stable": { - "target": "99" + "target": "90" }, "candidate": { - "target": "99" + "target": "90" }, "beta": { - "target": "99" + "target": "90" }, "edge": { - "target": "99" + "target": "90" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2.47.2-22.04": { "stable": { - "target": "98" + "target": "89" }, "candidate": { - "target": "98" + "target": "89" }, "beta": { - "target": "98" + "target": "89" }, "edge": { - "target": "98" + "target": "89" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2.47-22.04": { "stable": { - "target": "98" + "target": "89" }, "candidate": { - "target": "98" + "target": "89" }, "beta": { - "target": "98" + "target": "89" }, "edge": { - "target": "98" + "target": "89" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2.49.1-22.04": { "stable": { - "target": "100" + "target": "91" }, "candidate": { - "target": "100" + "target": "91" }, "beta": { - "target": "100" + "target": "91" }, "edge": { - "target": "100" + "target": "91" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2.49-22.04": { "stable": { - "target": "100" + "target": "91" }, "candidate": { - "target": "100" + "target": "91" }, "beta": { - "target": "100" + "target": "91" }, "edge": { - "target": "100" + "target": "91" }, "end-of-life": "2025-05-24T00:00:00Z" }, "2.50.0-22.04": { "end-of-life": "2025-05-24T00:00:00Z", "stable": { - "target": "101" + "target": "92" }, "candidate": { - "target": "101" + "target": "92" }, "beta": { - "target": "101" + "target": "92" }, "edge": { - "target": "101" + "target": "92" } }, "2.50.1-22.04": { "end-of-life": "2025-05-24T00:00:00Z", "stable": { - "target": "103" + "target": "94" }, "candidate": { - "target": "103" + "target": "94" }, "beta": { - "target": "103" + "target": "94" }, "edge": { - "target": "103" + "target": "94" } }, "2.50-22.04": { "end-of-life": "2025-03-14T00:00:00Z", "stable": { - "target": "102" + "target": "93" }, "candidate": { - "target": "102" + "target": "93" }, "beta": { - "target": "102" + "target": "93" }, "edge": { - "target": "102" + "target": "93" } }, "2.52.0-22.04": { "end-of-life": "2025-05-28T00:00:00Z", "stable": { - "target": "104" + "target": "95" }, "candidate": { - "target": "104" + "target": "95" }, "beta": { - "target": "104" + "target": "95" }, "edge": { - "target": "104" + "target": "95" } }, "2.52-22.04": { "end-of-life": "2025-05-28T00:00:00Z", "stable": { - "target": "104" + "target": "95" }, "candidate": { - "target": "104" + "target": "95" }, "beta": { - "target": "104" + "target": "95" }, "edge": { - "target": "104" - } - }, - "2.37.0-22.04": { - "end-of-life": "2024-10-04T00:00:00Z", - "stable": { - "target": "105" - }, - "candidate": { - "target": "2.37.0-22.04_stable" - }, - "beta": { - "target": "2.37.0-22.04_candidate" - }, - "edge": { - "target": "2.37.0-22.04_beta" - } - }, - "2.37-22.04": { - "end-of-life": "2024-10-04T00:00:00Z", - "stable": { - "target": "105" - }, - "candidate": { - "target": "2.37-22.04_stable" - }, - "beta": { - "target": "2.37-22.04_candidate" - }, - "edge": { - "target": "2.37-22.04_beta" - } - }, - "2.45.0-22.04": { - "end-of-life": "2024-10-04T00:00:00Z", - "stable": { - "target": "106" - }, - "candidate": { - "target": "2.45.0-22.04_stable" - }, - "beta": { - "target": "2.45.0-22.04_candidate" - }, - "edge": { - "target": "2.45.0-22.04_beta" - } - }, - "2.45-22.04": { - "end-of-life": "2024-10-04T00:00:00Z", - "stable": { - "target": "106" - }, - "candidate": { - "target": "2.45-22.04_stable" - }, - "beta": { - "target": "2.45-22.04_candidate" - }, - "edge": { - "target": "2.45-22.04_beta" + "target": "95" } } } \ No newline at end of file diff --git a/oci/prometheus/image.yaml b/oci/prometheus/image.yaml index 31c956dc..199b27e2 100644 --- a/oci/prometheus/image.yaml +++ b/oci/prometheus/image.yaml @@ -1,26 +1,18 @@ version: 1 upload: - source: canonical/prometheus-rock - commit: ebe3742f58628c2be2c385b1c300d33a5d519e0b - directory: 2.37.0 + commit: c817c7a768d7a19e127cce0cfc7a6cd9d54f584c + directory: 2.52.0 release: - 2.37.0-22.04: - end-of-life: "2024-10-04T00:00:00Z" + 2.52.0-22.04: + end-of-life: "2025-05-28T00:00:00Z" risks: - stable - 2.37-22.04: - end-of-life: "2024-10-04T00:00:00Z" + 2.52-22.04: + end-of-life: "2025-05-28T00:00:00Z" risks: - stable - - source: canonical/prometheus-rock - commit: ebe3742f58628c2be2c385b1c300d33a5d519e0b - directory: 2.45.0 - release: - 2.45.0-22.04: - end-of-life: "2024-10-04T00:00:00Z" - risks: - - stable - 2.45-22.04: - end-of-life: "2024-10-04T00:00:00Z" + 2-22.04: + end-of-life: "2025-05-28T00:00:00Z" risks: - stable diff --git a/oci/python/_releases.json b/oci/python/_releases.json index c53e77ff..8c3cf9a5 100644 --- a/oci/python/_releases.json +++ b/oci/python/_releases.json @@ -1,47 +1,23 @@ { "3.8-20.04": { "edge": { - "target": "43" + "target": "17" }, "end-of-life": "2025-03-31T00:00:00Z", "stable": { - "target": "43" + "target": "17" }, "candidate": { - "target": "43" + "target": "3.8-20.04_stable" }, "beta": { - "target": "43" + "target": "3.8-20.04_candidate" } }, "3.12-24.04": { "end-of-life": "2029-03-31T00:00:00Z", "edge": { - "target": "48" - }, - "stable": { - "target": "48" - }, - "candidate": { - "target": "48" - }, - "beta": { - "target": "48" - } - }, - "3.10-22.04": { - "end-of-life": "2027-03-31T00:00:00Z", - "stable": { - "target": "38" - }, - "candidate": { - "target": "38" - }, - "beta": { - "target": "38" - }, - "edge": { - "target": "38" + "target": "18" } } } \ No newline at end of file diff --git a/oci/python/image.yaml b/oci/python/image.yaml index 9d71e805..a63c4c92 100644 --- a/oci/python/image.yaml +++ b/oci/python/image.yaml @@ -2,35 +2,19 @@ version: 1 upload: - source: canonical/chiselled-python - commit: e0943bf2923ef50c9117ac58cd02a86146ece1fb + commit: 26d72f441aa2ad3bcd830cee6d6996b384be223c directory: python3.8/ release: 3.8-20.04: end-of-life: "2025-03-31T00:00:00Z" risks: - stable - - candidate - - beta - edge - source: canonical/chiselled-python - commit: e0943bf2923ef50c9117ac58cd02a86146ece1fb - directory: python3.10/ - release: - 3.10-22.04: - end-of-life: "2027-03-31T00:00:00Z" - risks: - - stable - - candidate - - beta - - edge - - source: canonical/chiselled-python - commit: e0943bf2923ef50c9117ac58cd02a86146ece1fb + commit: 26d72f441aa2ad3bcd830cee6d6996b384be223c directory: python3.12/ release: 3.12-24.04: end-of-life: "2029-03-31T00:00:00Z" risks: - - stable - - candidate - - beta - edge diff --git a/oci/tempo/_releases.json b/oci/tempo/_releases.json index a2b2f8ef..192fe6a4 100644 --- a/oci/tempo/_releases.json +++ b/oci/tempo/_releases.json @@ -30,9 +30,9 @@ } }, "2-22.04": { - "end-of-life": "2025-01-18T00:00:00Z", + "end-of-life": "2025-05-28T00:00:00Z", "stable": { - "target": "3" + "target": "2" }, "candidate": { "target": "2-22.04_stable" @@ -58,35 +58,5 @@ "edge": { "target": "2.4.2-22.04_beta" } - }, - "2.6.1-22.04": { - "end-of-life": "2025-01-18T00:00:00Z", - "stable": { - "target": "3" - }, - "candidate": { - "target": "2.6.1-22.04_stable" - }, - "beta": { - "target": "2.6.1-22.04_candidate" - }, - "edge": { - "target": "2.6.1-22.04_beta" - } - }, - "2.6-22.04": { - "end-of-life": "2025-01-18T00:00:00Z", - "stable": { - "target": "3" - }, - "candidate": { - "target": "2.6-22.04_stable" - }, - "beta": { - "target": "2.6-22.04_candidate" - }, - "edge": { - "target": "2.6-22.04_beta" - } } } \ No newline at end of file diff --git a/oci/tempo/image.yaml b/oci/tempo/image.yaml index 57b46703..913d0b02 100644 --- a/oci/tempo/image.yaml +++ b/oci/tempo/image.yaml @@ -1,18 +1,18 @@ version: 1 upload: - source: canonical/tempo-rock - commit: 61866670957aecbb67481f8c5250b72aa82fc7f4 - directory: 2.6.1 + commit: 791b11401656f10dd447b2385644bf847737ae6e + directory: 2.4.2 release: - 2.6.1-22.04: - end-of-life: "2025-01-18T00:00:00Z" + 2.4.2-22.04: + end-of-life: "2025-05-28T00:00:00Z" risks: - stable - 2.6-22.04: - end-of-life: "2025-01-18T00:00:00Z" + 2.4-22.04: + end-of-life: "2025-05-28T00:00:00Z" risks: - stable 2-22.04: - end-of-life: "2025-01-18T00:00:00Z" + end-of-life: "2025-05-28T00:00:00Z" risks: - stable diff --git a/oci/zookeeper/_releases.json b/oci/zookeeper/_releases.json index aa551951..a48f7ca8 100644 --- a/oci/zookeeper/_releases.json +++ b/oci/zookeeper/_releases.json @@ -2,7 +2,7 @@ "3.8-22.04": { "end-of-life": "2024-09-01T00:00:00Z", "edge": { - "target": "15" + "target": "9" } } } \ No newline at end of file diff --git a/src/build_rock/assemble_rock/assemble.sh b/src/build_rock/assemble_rock/assemble.sh deleted file mode 100755 index f2c3d8dd..00000000 --- a/src/build_rock/assemble_rock/assemble.sh +++ /dev/null @@ -1,54 +0,0 @@ -#! /bin/bash - -set -e - - -function usage(){ - echo - echo "$(basename "$0") -d -n " - echo - echo "Merge multiple OCI rock images into one multi arch image." - echo - echo -e "-d \\t Directory to search for rock OCI images in." - echo -e "-n \\t Final output archive name. " -} - -while getopts "d:n:" opt -do - case $opt in - d) - ROCK_DIR="$OPTARG" - ;; - n) - ARCHIVE_NAME="$OPTARG" - ;; - ?) - usage - exit 1 - ;; - esac -done - -if [ -z "$ROCK_DIR" ] -then - echo "Error: Missing rock search directory argument (-d)" - usage - exit 1 -fi - -if [ -z "$ARCHIVE_NAME" ] -then - echo "Error: Missing final archive name (-n)" - usage - exit 1 -fi - -buildah manifest create multi-arch-rock - -for rock in `find "$ROCK_DIR" -name "*.rock" -type f` -do - buildah manifest add multi-arch-rock oci-archive:$rock -done - -buildah manifest push --all multi-arch-rock "oci-archive:$ARCHIVE_NAME" - diff --git a/src/build_rock/assemble_rock/requirements.sh b/src/build_rock/assemble_rock/requirements.sh deleted file mode 100755 index 1ff6d0d0..00000000 --- a/src/build_rock/assemble_rock/requirements.sh +++ /dev/null @@ -1,6 +0,0 @@ -#! /bin/bash - -set -e - -sudo apt update -sudo apt install buildah -y diff --git a/src/build_rock/configure/generate_build_matrix.py b/src/build_rock/configure/generate_build_matrix.py deleted file mode 100755 index 8c1dc9cf..00000000 --- a/src/build_rock/configure/generate_build_matrix.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 - -import yaml -import os -import argparse -import json -from enum import Enum -from ...shared.github_output import GithubOutput -from pydantic import TypeAdapter - - -class MATRIX_NAMES(Enum): - RUNNER = "runner-build-matrix" - LPCI = "lpci-build-matrix" - - -class MissingArchSupport(Exception): - pass - - -def get_target_archs(rockcraft: dict) -> list: - """get list of target architectures from rockcraft project definition""" - - rock_platforms = rockcraft["platforms"] - - target_archs = set() - - for platf, values in rock_platforms.items(): - - if isinstance(values, dict) and "build-for" in values: - if isinstance(arches := values["build-for"], list): - target_archs.update(arches) - elif isinstance(values, str): - target_archs.add(arches) - else: - target_archs.add(platf) - - return target_archs - - -def configure_matrices(target_archs: list, arch_map: dict, lp_fallback: bool) -> dict: - """Sort build into appropriate build matrices""" - - # map configuration to individual job matrices - build_matrices = {name.value: {"include": []} for name in MATRIX_NAMES} - - # Check if we have runners for all supported architectures - if missing_archs := set(target_archs) - set(arch_map): - - # raise exception if we cannot fallback to LP builds - if not lp_fallback: - raise MissingArchSupport( - f"Missing support for runner arches: {missing_archs}" - ) - - # configure LP build - build_matrices[MATRIX_NAMES.LPCI.value]["include"].append( - {"architecture": "-".join(set(target_archs))} - ) - - else: - # configure runner matrix for list of supported runners - for runner_arch, runner_name in arch_map.items(): - if runner_arch in target_archs: - build_matrices[MATRIX_NAMES.RUNNER.value]["include"].append( - {"architecture": runner_arch, "runner": runner_name} - ) - - return build_matrices - - -def set_build_config_outputs(rock_name: str, build_matrices: dict): - """Update GITHUB_OUTPUT with build configuration.""" - - outputs = {"rock-name": rock_name, **build_matrices} - - with GithubOutput() as github_output: - github_output.write(**outputs) - - -def main(): - parser = argparse.ArgumentParser() - - parser.add_argument( - "--rockfile-directory", - help="Path where to directory containing rockcraft.yaml.", - required=True, - ) - - parser.add_argument( - "--lpci-fallback", - help="Revert to lpci if architectures are not supported. ", - required=True, - type=TypeAdapter(bool).validate_python, - ) - - parser.add_argument( - "--config", - help="JSON mapping arch to runner for matrix generation.", - required=True, - ) - - args = parser.parse_args() - - # get configuration form rockcraft yaml - with open(f"{args.rockfile_directory}/rockcraft.yaml") as rf: - rockcraft_yaml = yaml.safe_load(rf) - - # load config - arch_map = json.loads(args.config) - - target_archs = get_target_archs(rockcraft_yaml) - build_matrices = configure_matrices(target_archs, arch_map, args.lpci_fallback) - - # set github outputs for use in later steps - set_build_config_outputs(rockcraft_yaml["name"], build_matrices) - - -if __name__ == "__main__": - main() diff --git a/src/build_rock/configure/requirements.txt b/src/build_rock/configure/requirements.txt deleted file mode 100644 index d04c0606..00000000 --- a/src/build_rock/configure/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pyyaml==6.0.2 -pydantic==2.8.2 diff --git a/src/build_rock/lpci_build/lpci_build.sh b/src/build_rock/lpci_build/lpci_build.sh deleted file mode 100755 index f1900694..00000000 --- a/src/build_rock/lpci_build/lpci_build.sh +++ /dev/null @@ -1,58 +0,0 @@ -#! /bin/bash - - -set -e - - -function usage(){ - echo - echo "$(basename "$0") -d -c " - echo - echo "Build local rockcraft project on Launchpad." - echo - echo -e "-d \\t Directory to rockcraft project file. " - echo -e "-c \\t Launchpad credentials. " -} - -while getopts "c:d:" opt -do - case $opt in - d) - ROCKCRAFT_DIR="$OPTARG" - ;; - c) - LP_CREDENTIALS_B64="$OPTARG" - ;; - ?) - usage - exit 1 - ;; - esac -done - -if [ -z "$ROCKCRAFT_DIR" ] -then - echo "Error: Missing rockcraft project directory argument (-d)" - usage - exit 1 -fi - -if [ -z "$LP_CREDENTIALS_B64" ] -then - echo "Error: Missing launchpad credentials argument (-c)" - usage - exit 1 -fi - - -cd "$ROCKCRAFT_DIR" -rocks_toolbox="$(mktemp -d)" - -# install dependencies -git clone --depth 1 --branch v1.1.2 https://github.com/canonical/rocks-toolbox $rocks_toolbox -${rocks_toolbox}/rockcraft_lpci_build/requirements.sh -pip3 install -r ${rocks_toolbox}/rockcraft_lpci_build/requirements.txt - -python3 ${rocks_toolbox}/rockcraft_lpci_build/rockcraft_lpci_build.py \ - --lp-credentials-b64 "$LP_CREDENTIALS_B64" \ - --launchpad-accept-public-upload diff --git a/src/docs/generate_oci_doc_yaml.py b/src/docs/generate_oci_doc_yaml.py index 398eb5b2..9a1ba959 100755 --- a/src/docs/generate_oci_doc_yaml.py +++ b/src/docs/generate_oci_doc_yaml.py @@ -267,7 +267,9 @@ def build_releases_data( # Set the support date if all_tracks.get(track_base): eol = parser.parse(all_tracks[track_base]) - release_data["support"] = {"until": eol.strftime("%m/%Y")} + release_data["support"] = { + "until": eol.strftime("%m/%Y") + } if eol > datetime.now(timezone.utc): release_data["deprecated"] = { diff --git a/src/docs/schema/triggers.py b/src/docs/schema/triggers.py index 7cae1b3f..d9de4900 100755 --- a/src/docs/schema/triggers.py +++ b/src/docs/schema/triggers.py @@ -2,7 +2,6 @@ this module is the pydantic version of the documentation.yaml schema. """ - from typing import Optional from pydantic import BaseModel, Extra, constr, conlist diff --git a/src/image/define_image_revision.sh b/src/image/define_image_revision.sh index 00dad985..b6a966be 100755 --- a/src/image/define_image_revision.sh +++ b/src/image/define_image_revision.sh @@ -17,4 +17,4 @@ highest_revision=$(swift list $SWIFT_CONTAINER_NAME -p $IMAGE_NAME \ | awk -F'/' '{print $3}') REVISION=$(( $highest_revision + 1 )) -echo "revision=${REVISION}" >> "$GITHUB_OUTPUT" +echo "revision=${REVISION}" >> "$GITHUB_OUTPUT" \ No newline at end of file diff --git a/src/image/prepare_single_image_build_matrix.py b/src/image/prepare_single_image_build_matrix.py index 09cecc66..9047d0bd 100755 --- a/src/image/prepare_single_image_build_matrix.py +++ b/src/image/prepare_single_image_build_matrix.py @@ -36,13 +36,7 @@ def validate_image_trigger(data: dict) -> None: parser.add_argument( "--next-revision", help="Next revision number", - default=1, - ) - parser.add_argument( - "--infer-image-track", - help="Infer the track corresponding to the releases", - action="store_true", - default=False, + required=True, ) args = parser.parse_args() @@ -66,50 +60,21 @@ def validate_image_trigger(data: dict) -> None: while img_number < len(builds): builds[img_number]["name"] = args.oci_path.rstrip("/").split("/")[-1] builds[img_number]["path"] = args.oci_path + # make sure every build of this image has a unique identifier + # within the execution of the workflow - use revision number builds[img_number]["revision"] = img_number + int(args.next_revision) - if args.infer_image_track: - import sys - - sys.path.append("src/") - from git import Repo - from tempfile import TemporaryDirectory as tempdir - from uploads.infer_image_track import get_base_and_track - - with tempdir() as d: - url = f"https://github.com/{builds[img_number]['source']}.git" - repo = Repo.clone_from(url, d) - repo.git.checkout(builds[img_number]["commit"]) - # get the base image from the rockcraft.yaml file - with open( - f"{d}/{builds[img_number]['directory']}/rockcraft.yaml", - encoding="UTF-8", - ) as rockcraft_file: - rockcraft_yaml = yaml.load(rockcraft_file, Loader=yaml.BaseLoader) - - base_release, track = get_base_and_track(rockcraft_yaml) - builds[img_number]["track"] = track - builds[img_number]["base"] = f"ubuntu:{base_release}" - with open( f"{args.revision_data_dir}/{builds[img_number]['revision']}", "w", - encoding="UTF-8", ) as data_file: json.dump(builds[img_number], data_file) - # Add dir_identifier to assemble the cache key and artefact path - # No need to write it to rev data file since it's only used in matrix - builds[img_number]["dir_identifier"] = ( - builds[img_number]["directory"].rstrip("/").replace("/", "_") - ) - # set an output as a marker for later knowing if we need to release if "release" in builds[img_number]: - min_eol = datetime.strptime( - min(v["end-of-life"] for v in builds[img_number]["release"].values()), - "%Y-%m-%dT%H:%M:%SZ", - ).replace(tzinfo=timezone.utc) + min_eol = datetime.strptime(min( + v["end-of-life"] for v in builds[img_number]["release"].values() + ), "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) if min_eol < datetime.now(timezone.utc): print("Track skipped because it reached its end of life") del builds[img_number] diff --git a/src/image/requirements.sh b/src/image/requirements.sh index 85e7fc75..9f7f1966 100755 --- a/src/image/requirements.sh +++ b/src/image/requirements.sh @@ -22,9 +22,8 @@ git remote add origin git+ssh://${ROCKS_DEV_LP_USERNAME}@${CPC_BUILD_TOOLS_REPO} git fetch --depth 1 origin main # ${CPC_BUILD_TOOLS_REPO_REF} git checkout FETCH_HEAD -sudo mv /tmp/cpc-build-tools/* /usr/local/bin/ -sudo chmod +x /usr/local/bin/oci_registry_upload.py -ln -s oci_registry_upload.py /usr/local/bin/cpc-build-tools.oci-registry-upload +sudo mv oci_registry_upload.py /usr/local/bin/cpc-build-tools.oci-registry-upload +sudo chmod +x /usr/local/bin/cpc-build-tools.oci-registry-upload popd ## @@ -37,4 +36,4 @@ docker run -v $PWD:/src -w /src -e DISABLE_DOCS=1 \ sudo mv bin/skopeo /usr/local/bin/ sudo chmod +x /usr/local/bin/skopeo -popd +popd \ No newline at end of file diff --git a/src/image/requirements.txt b/src/image/requirements.txt index 6ea9d920..57743760 100644 --- a/src/image/requirements.txt +++ b/src/image/requirements.txt @@ -1,6 +1,4 @@ -GitPython PyYAML pydantic==1.9.0 -pytest python-swiftclient -python-keystoneclient +python-keystoneclient \ No newline at end of file diff --git a/src/image/utils/schema/triggers.py b/src/image/utils/schema/triggers.py index 74fcf487..e9e6a4d2 100644 --- a/src/image/utils/schema/triggers.py +++ b/src/image/utils/schema/triggers.py @@ -69,20 +69,3 @@ class ImageSchema(pydantic.BaseModel): class Config: extra = pydantic.Extra.forbid - - @pydantic.validator("upload") - def ensure_unique_triggers( - cls, v: Optional[List[ImageUploadSchema]] - ) -> Optional[List[ImageUploadSchema]]: - """Ensure that the triggers are unique.""" - if not v: - return v - unique_triggers = set() - for upload in v: - trigger = f"{upload.source}_{upload.commit}_{upload.directory}" - if trigger in unique_triggers: - raise ImageTriggerValidationError( - f"Image trigger {trigger} is not unique." - ) - unique_triggers.add(trigger) - return v diff --git a/src/shared/github_output.py b/src/shared/github_output.py deleted file mode 100755 index ab9fe70f..00000000 --- a/src/shared/github_output.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 - -import json -from os import environ - -"""This module provides support for writing Github Outputs.""" - -# locate -GITHUB_OUTPUT = environ.get("GITHUB_OUTPUT", None) - - -class GithubOutput: - - def __init__(self): - - self.output_path = environ["GITHUB_OUTPUT"] - - def __enter__(self): - - self.file_handler = open(self.output_path, "a") - - return self - - def __exit__(self, exc_type, exc_value, traceback): - - self.file_handler.close() - del self.file_handler - - def write(self, **kwargs): - """Format kwargs for Github Outputs and write to `output` File Object""" - - if not getattr(self, "file_handler", None): - raise AttributeError( - "file_handler not available. Please use in context block." - ) - - for key, value in kwargs.items(): - - formatted_value = self.format_value(value) - print(f"{key}={formatted_value}", file=self.file_handler) - - @staticmethod - def format_value(value): - """Format `value` such that it can be stored as a github output""" - - if isinstance(value, str): - # str is an exception to casting with json.dumps as we do - # not need to represent the string itself, but just the data - return value - else: - json_value = json.dumps(value) - return json_value diff --git a/src/shared/release_info.py b/src/shared/release_info.py old mode 100644 new mode 100755 index d07a06f6..b879d68d --- a/src/shared/release_info.py +++ b/src/shared/release_info.py @@ -6,7 +6,7 @@ """ import json -from ..image.utils.schema.triggers import KNOWN_RISKS_ORDERED +from src.image.utils.schema.triggers import KNOWN_RISKS_ORDERED class BadChannel(Exception): @@ -71,14 +71,13 @@ def get_revision_to_track(all_revisions_tags: list) -> dict: revision_track = {} for track_revision in all_revisions_tags: track, revision = track_revision.rsplit("_", 1) - revision = int(revision) if revision in revision_track: msg = ( "Each revision can only have 1 canonical tag, " f"but revision {revision} is associated with tracks " - f"{track} and {revision_track[revision]}!" + f"{track} and {revision_track['revision']}!" ) raise BadChannel(msg) - revision_track[revision] = track + revision_track[int(revision)] = track return revision_track diff --git a/src/tests/get_released_revisions.py b/src/tests/get_released_revisions.py index 083861d8..43613563 100755 --- a/src/tests/get_released_revisions.py +++ b/src/tests/get_released_revisions.py @@ -94,11 +94,11 @@ def get_image_name_in_registry(img_name: str, revision: str) -> str: ) continue elif not risks.get("end-of-life"): - logging.warning(f"Track {track} is missing its end-of-life field") + logging.warning( + f"Track {track} is missing its end-of-life field" + ) - for key, targets in risks.items(): - if key == "end-of-life": - continue + for targets in risks.values(): try: if int(targets["target"]) in released_revisions[img]: continue diff --git a/src/uploads/infer_image_track.py b/src/uploads/infer_image_track.py index 14087d81..e74101ad 100755 --- a/src/uploads/infer_image_track.py +++ b/src/uploads/infer_image_track.py @@ -21,46 +21,40 @@ def get_release_from_codename(codename: str) -> str: ].split()[1] -def get_base_and_track(rockcraft_yaml) -> tuple[str, str]: - rock_base = ( - rockcraft_yaml["base"] - if rockcraft_yaml["base"] != "bare" - else rockcraft_yaml["build-base"] +parser = argparse.ArgumentParser() +parser.add_argument( + "--recipe-dirname", + help="Path to the directory where rockcraft.yaml is", + required=True, +) +args = parser.parse_args() + +with open( + f"{args.recipe_dirname.rstrip('/')}/rockcraft.yaml", encoding="UTF-8" +) as rockcraft_file: + rockcraft_yaml = yaml.load(rockcraft_file, Loader=yaml.BaseLoader) + +rock_base = ( + rockcraft_yaml["base"] + if rockcraft_yaml["base"] != "bare" + else rockcraft_yaml["build-base"] +) + +try: + base_release = float(rock_base.replace(":", "@").split("@")[-1]) +except ValueError: + logging.warning( + f"Could not infer ROCK's base release from {rock_base}. Trying with codename." ) - - try: - base_release = float(rock_base.replace(":", "@").split("@")[-1]) - except ValueError: - logging.warning( - f"Could not infer ROCK's base release from {rock_base}. Trying with codename." - ) - base_release = float( - get_release_from_codename(rock_base.replace(":", "@").split("@")[-1]) - ) - - version = rockcraft_yaml["version"] - - return base_release, f"{version}-{base_release}" - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--recipe-dirname", - help="Path to the directory where rockcraft.yaml is", - required=True, + base_release = float( + get_release_from_codename(rock_base.replace(":", "@").split("@")[-1]) ) - args = parser.parse_args() - - with open( - f"{args.recipe_dirname.rstrip('/')}/rockcraft.yaml", encoding="UTF-8" - ) as rockcraft_file: - rockcraft_yaml = yaml.load(rockcraft_file, Loader=yaml.BaseLoader) - base_release, track = get_base_and_track(rockcraft_yaml) +version = rockcraft_yaml["version"] - print(f"rock track: {track}") +track = f"{version}-{base_release}" +print(f"rock track: {track}") - with open(os.environ["GITHUB_OUTPUT"], "a") as gh_out: - print(f"track={track}", file=gh_out) - print(f"base=ubuntu:{base_release}", file=gh_out) +with open(os.environ["GITHUB_OUTPUT"], "a") as gh_out: + print(f"track={track}", file=gh_out) + print(f"base=ubuntu:{base_release}", file=gh_out) diff --git a/src/uploads/preempt_swift_slots.sh b/src/uploads/preempt_swift_slots.sh deleted file mode 100755 index 87226e72..00000000 --- a/src/uploads/preempt_swift_slots.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -e - -# Source Swift config -source $(dirname $0)/../configs/swift.public.novarc - -set -x - -REVISION_DATA_DIR=$1 - -staging_area=$(mktemp -d) - -for revision in $(ls ${REVISION_DATA_DIR}); do - image_name=$(jq -r '.name' "${REVISION_DATA_DIR}/${revision}") - track=$(jq -r '.track' "${REVISION_DATA_DIR}/${revision}") - mkdir -p "${staging_area}/${image_name}/${track}/${revision}" - touch "${staging_area}/${image_name}/${track}/${revision}/dummy.txt" -done - -pushd "${staging_area}" - -# SWIFT_CONTAINER_NAME comes from env -swift upload "$SWIFT_CONTAINER_NAME" "${IMAGE_NAME}" diff --git a/src/uploads/swift_lockfile_lock.sh b/src/uploads/swift_lockfile_lock.sh deleted file mode 100755 index e3da5d4f..00000000 --- a/src/uploads/swift_lockfile_lock.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash -e - -# Source Swift config -source $(dirname $0)/../configs/swift.public.novarc - -set -x - -IMAGE_NAME=$1 -# Timeout and sleep time in seconds -TIMEOUT=${2:-300} -SLEEP_TIME=5 - -staging_area=$(mktemp -d) - -mkdir -p "${staging_area}/${IMAGE_NAME}" - -touch "${staging_area}/${IMAGE_NAME}/lockfile.lock" - -pushd "${staging_area}" - -# Check if the ${IMAGE_NAME}/lockfile.lock exists in the swift container -# if it does, wait until the timeout is reached and emit an error -# if it does not, upload the lockfile.lock to the swift container -# and exit. -# There's still the unlikely corner case where 2 concurrent jobs -# are waiting for the lockfile to get removed, and they may exit -# the while loop at the same time, getting into a race condition. -while [ $TIMEOUT -gt 0 ]; do - swift list $SWIFT_CONTAINER_NAME -p $IMAGE_NAME | grep "lockfile.lock" && sleep $SLEEP_TIME || break - TIMEOUT=$(( $TIMEOUT - $SLEEP_TIME )) - if [ $TIMEOUT -lt 1 ]; then - echo "Timeout reached while waiting to write lockfile into the Swift container for ${IMAGE_NAME}." - exit 1 - fi -done - -# SWIFT_CONTAINER_NAME comes from env -swift upload "$SWIFT_CONTAINER_NAME" "${IMAGE_NAME}" diff --git a/src/uploads/swift_lockfile_unlock.sh b/src/uploads/swift_lockfile_unlock.sh deleted file mode 100755 index 629c1f62..00000000 --- a/src/uploads/swift_lockfile_unlock.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -e - -# Source Swift config -source $(dirname $0)/../configs/swift.public.novarc - -set -x - -IMAGE_NAME=$1 - -# check if the ${IMAGE_NAME}/lockfile.lock exists in the swift container -# if it does, remove it -# if it does not, emit an error -# SWIFT_CONTAINER_NAME comes from env -LOCKFILE="${IMAGE_NAME}/lockfile.lock" -swift list $SWIFT_CONTAINER_NAME -p $IMAGE_NAME | grep "$LOCKFILE" && \ - (swift delete $SWIFT_CONTAINER_NAME "$LOCKFILE" && echo "Lock file removed successfully.") || \ - echo "Lock file does not exist." diff --git a/src/uploads/upload_to_swift.sh b/src/uploads/upload_to_swift.sh index 2f83c2e3..e713e992 100755 --- a/src/uploads/upload_to_swift.sh +++ b/src/uploads/upload_to_swift.sh @@ -22,6 +22,4 @@ cp "$BUILD_METADATA_FILE" "$SBOM_FILE" "$VULN_REPORT_FILE" \ pushd "${staging_area}" # SWIFT_CONTAINER_NAME comes from env -swift upload "$SWIFT_CONTAINER_NAME" "${IMAGE_NAME}" -# Remove the dummy file uploaded by `upload_dummy_to_swift.sh` in `prepare-upload` -swift delete "$SWIFT_CONTAINER_NAME" "${IMAGE_NAME}/${TRACK}/${REVISION}/dummy.txt" +swift upload "$SWIFT_CONTAINER_NAME" "${IMAGE_NAME}" diff --git a/tools/workflow-engine/README.md b/src/workflow-engine/README.md similarity index 100% rename from tools/workflow-engine/README.md rename to src/workflow-engine/README.md diff --git a/tools/workflow-engine/charms/temporal-worker/README.md b/src/workflow-engine/charms/temporal-worker/README.md similarity index 99% rename from tools/workflow-engine/charms/temporal-worker/README.md rename to src/workflow-engine/charms/temporal-worker/README.md index 3af60a3a..c9835e8a 100644 --- a/tools/workflow-engine/charms/temporal-worker/README.md +++ b/src/workflow-engine/charms/temporal-worker/README.md @@ -167,7 +167,7 @@ process. ```bash # Only set the https_proxy when running inside the cloud environment https_proxy="http://squid.internal:3128" git clone https://github.com/canonical/oci-factory - cd oci-factory/tools/workflow-engine/charms/temporal-worker/ + cd oci-factory/src/workflow-engine/charms/temporal-worker/ `````` 4. use the [existing Python env](../../README.md) and generate a wheel file diff --git a/tools/workflow-engine/charms/temporal-worker/charm.tf b/src/workflow-engine/charms/temporal-worker/charm.tf similarity index 100% rename from tools/workflow-engine/charms/temporal-worker/charm.tf rename to src/workflow-engine/charms/temporal-worker/charm.tf diff --git a/tools/workflow-engine/charms/temporal-worker/config.yml.template b/src/workflow-engine/charms/temporal-worker/config.yml.template similarity index 99% rename from tools/workflow-engine/charms/temporal-worker/config.yml.template rename to src/workflow-engine/charms/temporal-worker/config.yml.template index db603906..54f8dcab 100644 --- a/tools/workflow-engine/charms/temporal-worker/config.yml.template +++ b/src/workflow-engine/charms/temporal-worker/config.yml.template @@ -49,7 +49,6 @@ temporal-worker-k8s: # Only needed if deploying under a proxy # http-proxy: "http://squid.internal:3128" # https-proxy: "http://squid.internal:3128" - # no-proxy: ".canonical.com" # Charmed workflows # The "workflows-file-name" must match the wheel file created with poetry diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/.env.template b/src/workflow-engine/charms/temporal-worker/oci_factory/.env.template similarity index 83% rename from tools/workflow-engine/charms/temporal-worker/oci_factory/.env.template rename to src/workflow-engine/charms/temporal-worker/oci_factory/.env.template index 7294370b..a0708736 100644 --- a/tools/workflow-engine/charms/temporal-worker/oci_factory/.env.template +++ b/src/workflow-engine/charms/temporal-worker/oci_factory/.env.template @@ -7,6 +7,3 @@ OS_USERNAME= OS_PASSWORD= OS_PROJECT_NAME= OS_STORAGE_URL= -MATTERMOST_SERVER= -MATTERMOST_TOKEN= -MATTERMOST_CHANNEL_ID= diff --git a/tests/fixtures/__init__.py b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/__init__.py similarity index 100% rename from tests/fixtures/__init__.py rename to src/workflow-engine/charms/temporal-worker/oci_factory/activities/__init__.py diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/activity_consume_events.py b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/activity_consume_events.py similarity index 64% rename from tools/workflow-engine/charms/temporal-worker/oci_factory/activities/activity_consume_events.py rename to src/workflow-engine/charms/temporal-worker/oci_factory/activities/activity_consume_events.py index c3b860a4..6f128c85 100644 --- a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/activity_consume_events.py +++ b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/activity_consume_events.py @@ -1,27 +1,12 @@ -import logging -import os -import subprocess - from confluent_kafka import Consumer, KafkaException from temporalio import activity from oci_factory.activities.consumer.config import Config from oci_factory.activities.consumer.schema import SchemaClient -from oci_factory.notification.mattermost_notifier import ( - send_message, - update_status_and_message, -) - -TWC_HOST = os.environ.get("TWC_HOST") -TWC_NAMESPACE = os.environ.get("TWC_NAMESPACE") -FILE_NAME = os.path.basename(__file__) -TEMPORAL_WEB_UI = ( - f"https://web.{TWC_HOST}/namespaces/{TWC_NAMESPACE}/workflows/{{}}/{{}}/history" -) -MM_MESSAGE_TITLE = f"[OCI Factory Temporal Workflow]: {FILE_NAME}: Rebuild rocks" -MM_MESSAGE_BODY = "**Release:** {}\n**Status:** {}\n" -MM_MESSAGE_BODY += f"[More details]({TEMPORAL_WEB_UI})" +import logging +import os +import subprocess # `TWC_LOG_LEVEL` is the mapped value of `log-level` in the charm config logging.basicConfig(level=os.environ.get("TWC_LOG_LEVEL", "info").upper()) @@ -39,9 +24,6 @@ async def consume(topic: str, consumer_group: str) -> dict: f"with consumer group {consumer_group}" ) - activity_info = activity.info() - activity_info_formatter = (activity_info.workflow_id, activity_info.workflow_run_id) - with Config() as config: consumer_config = config.get_consumer_config(consumer_group) registry_config = config.get_registry_config() @@ -70,10 +52,6 @@ async def consume(topic: str, consumer_group: str) -> dict: consumer.close() logging.info("Release: {}".format(value["release"])) - message_id = send_message( - MM_MESSAGE_TITLE, - MM_MESSAGE_BODY.format(value["release"], "Triggered", *activity_info_formatter), - ) # TODO: This part of code should be refactored once Renovate is dropped # Details see ROCKS-1197 @@ -81,19 +59,8 @@ async def consume(topic: str, consumer_group: str) -> dict: script_full_path = os.path.join(curr_file_path, "find_images_to_update.py") proc = subprocess.Popen( - ["python3", script_full_path, "{}".format(value.get("release"))], - stderr=subprocess.PIPE, - ) - success = proc.wait() == 0 - - status = "Success" if success else "Failed" - update_status_and_message( - message_id, - success, - MM_MESSAGE_BODY.format(value["release"], status, *activity_info_formatter), + ["python3", script_full_path, "{}".format(value.get("release"))] ) + proc.wait() - return { - "eventbus_message": value, - "find_images_to_update.py": proc.stderr.read().decode("utf-8"), - } + return value diff --git a/tests/integration/__init__.py b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/__init__.py similarity index 100% rename from tests/integration/__init__.py rename to src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/__init__.py diff --git a/src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/ca.crt b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/ca.crt new file mode 100644 index 00000000..d66b3274 --- /dev/null +++ b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/ca.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFLTCCAxWgAwIBAgIUPlDHrCDf61mA+trI3ifo4H8b7wswDQYJKoZIhvcNAQEN +BQAwLTETMBEGA1UECgwKaW8uc3RyaW16aTEWMBQGA1UEAwwNY2x1c3Rlci1jYSB2 +MDAeFw0yMzExMDIxODMzMDZaFw0yNDExMDExODMzMDZaMC0xEzARBgNVBAoMCmlv +LnN0cmltemkxFjAUBgNVBAMMDWNsdXN0ZXItY2EgdjAwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCcD2hg0ns2wznfSHQ8ifWqMbqJ51jvb+GckH2F5zbk +C/C9NtSvo18b/JDWiMRBGdR7T/Od6d9VreC69iF6qsJ4AsNmZg/qcW4FbfztzBMD +byDay9TBK10r6hBvM2xlLj0h1c80L0emx52zbjAwnPwuprcHIcr0cxwrx42xpqXW +aFiE57q/SlTfh2nlHmNZ4vz9IEBB4QSZN3zwT+o7ZM0jxjm/vBrlzdO34euW31Mz +5/RMJP4NAjelC/VOakgA58UlitG8SbMhhXmZv1yXSoOGGXz94kZphB5pbBXbpOd8 +SR66QvVcytI4Babj7M5vgWeDKduIAGTdRogW5iZQkTQ85q64QczYOzS1nG4znPKx +eAXCUOptZ5Kr7VHxvlKFf5bP7fNCF4OaboHh/LtEgJEHYte9q6Ptvgbxyed5epGO +byn1PYPnTDClDf5VGs5bhdMdpkUMFI2RlFT7lI7XAXqLxRgjxdmuDV9cZb+WKc9Y +gZFLzu5J8CcBiwUzaSqqfZkcqvA3Crpyw3/UXl2NEMrNkoHR6GrnqTRtp8EdAd7N +bSkTzH1HMLOLX+Hufbta92gUgGj4d9tfxBIBC1fmMp9LhGJbpb41DKLeNcRDTBKt +pRaon5zOmVb6OEPd+YwByoQoXV52IA8J2omXu0yTR5sTFSm6pPGvBPAS70vrPBnI +RwIDAQABo0UwQzAdBgNVHQ4EFgQUVi88Rg6+46CLPKlcdVc54t64NZswEgYDVR0T +AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQADggIB +ADhewoDt85CqmV3U75tuEvoQovPYKsGsZFbgQxU31KjgFban9q8PV/lSuxuNGrnH +jhFYOlNIy595o9R/ZBv/KG3u1LupSIoQYzwmaktPq3/ISS/lMT9aymvcxRoMQsN5 +tYxAEd4E3qPBkdbk99TjQTFcbDgx0htvcU2IieC6QRpj7Ro5ZMJYSTygOVQiaRNy +ewcgJ7RAtIw9zgWoebfdggfKLNXwobQavYymsNyNoMhjzNp5JKp343wtXFEPANT4 +PQ0LSIUTP1YzeJX2eLlnN2Qq1cyURNSMzLcN7euoUgnBa+OZftvsidDH9JBQ+fF2 +9DKE/Bn2MANW6JXI+lwhjNDo4AA6y+EHW0UxqRCVwONJNxoMyM+5M/UgM9nUJpRO +S2v6FStxYSV9xTTbgLp3bxhNWsfXlU/+GymRReltq3WqysF3GIJeIiJ7/L8oI/jy +l4KnuyPKVS4XNk/XiV29sEXy7q7PlOgOD/UlDk+X2KOumNG5dYumHciAXxLK6alk +RJJDUu3nuFMahUbXRgg0nGYYk++zTXsjGWdVYx+y7fKkXM/LMBagAkUc43ASU4Pb +kmwLUFYKKdL5UzKxaTaw3sFjycnzwfoZkr+bszMctgZd92WuWJiQaiA9TKmZTk3N +kJktK0q0qU6Z1ownhXXFGR/2V8j1nAuFZHPiuuToHjky +-----END CERTIFICATE----- diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/config.py b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/config.py similarity index 100% rename from tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/config.py rename to src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/config.py diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schema.py b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schema.py similarity index 100% rename from tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schema.py rename to src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schema.py diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Architecture.avsc b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Architecture.avsc similarity index 100% rename from tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Architecture.avsc rename to src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Architecture.avsc diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Artefact.avsc b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Artefact.avsc similarity index 100% rename from tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Artefact.avsc rename to src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Artefact.avsc diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Layer.avsc b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Layer.avsc similarity index 100% rename from tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Layer.avsc rename to src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Layer.avsc diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Oci.avsc b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Oci.avsc similarity index 100% rename from tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Oci.avsc rename to src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/bo.schema.avro.Oci.avsc diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/example.json b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/example.json similarity index 100% rename from tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/example.json rename to src/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/schemas/example.json diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/find_images_to_update.py b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/find_images_to_update.py similarity index 96% rename from tools/workflow-engine/charms/temporal-worker/oci_factory/activities/find_images_to_update.py rename to src/workflow-engine/charms/temporal-worker/oci_factory/activities/find_images_to_update.py index 5e6f10dc..25745fd8 100755 --- a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/find_images_to_update.py +++ b/src/workflow-engine/charms/temporal-worker/oci_factory/activities/find_images_to_update.py @@ -198,6 +198,12 @@ def find_released_revisions(releases_json: dict) -> list: logging.exception(f"Unrecognized tag {tag['imageTag']}") continue + if to_track not in release_to: + release_to[str(to_track)] = {"risks": [to_risk]} + else: + release_to[to_track]["risks"].append(to_risk) + + # add end-of-life field to each track if releases[to_track].get("end-of-life"): if releases[to_track]["end-of-life"] < datetime.now( timezone.utc @@ -207,18 +213,14 @@ def find_released_revisions(releases_json: dict) -> list: f"end of life: {releases[to_track]['end-of-life']}" ) continue - else: - if to_track not in release_to: - release_to[str(to_track)] = {"risks": [to_risk]} - else: - release_to[to_track]["risks"].append(to_risk) - release_to[to_track]["end-of-life"] = releases[to_track][ - "end-of-life" - ] + release_to[to_track]["end-of-life"] = releases[to_track][ + "end-of-life" + ] else: logging.warning( f"Track {to_track} is missing its end-of-" "life field" ) + if release_to: build_and_upload_data["release"] = release_to diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/workflows/consume_events_workflow.py b/src/workflow-engine/charms/temporal-worker/oci_factory/workflows/consume_events_workflow.py similarity index 100% rename from tools/workflow-engine/charms/temporal-worker/oci_factory/workflows/consume_events_workflow.py rename to src/workflow-engine/charms/temporal-worker/oci_factory/workflows/consume_events_workflow.py diff --git a/tools/workflow-engine/charms/temporal-worker/pyproject.toml b/src/workflow-engine/charms/temporal-worker/pyproject.toml similarity index 98% rename from tools/workflow-engine/charms/temporal-worker/pyproject.toml rename to src/workflow-engine/charms/temporal-worker/pyproject.toml index 27abc052..c6d5d7f3 100644 --- a/tools/workflow-engine/charms/temporal-worker/pyproject.toml +++ b/src/workflow-engine/charms/temporal-worker/pyproject.toml @@ -28,7 +28,7 @@ importlib-resources = "^6.0.1" pytest = "^7.1.3" black = "^22.8.0" isort = "^5.10.1" -poethepoet = "^0.29.0" +poethepoet = "^0.16.2" pytest-asyncio = "^0.19.0" mypy = "^0.971" diff --git a/tools/workflow-engine/charms/temporal-worker/variables.tf b/src/workflow-engine/charms/temporal-worker/variables.tf similarity index 100% rename from tools/workflow-engine/charms/temporal-worker/variables.tf rename to src/workflow-engine/charms/temporal-worker/variables.tf diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 0b7f7c68..00000000 --- a/tests/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from pathlib import Path - -DATA_DIR = Path(__file__).parent / "data" diff --git a/tests/data/junit_xml_failure.xml b/tests/data/junit_xml_failure.xml deleted file mode 100644 index fb0ebe93..00000000 --- a/tests/data/junit_xml_failure.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - def test_example_failure(): - > assert False, "This is to exemplify the output of a failed unit test" - E AssertionError: This is to exemplify the output of a failed unit test - E assert False - oci-factory/src/docs/test/test_generate_oci_doc_yaml.py:8: AssertionError - - - - \ No newline at end of file diff --git a/tests/etc/requirements.txt b/tests/etc/requirements.txt deleted file mode 100644 index 21465760..00000000 --- a/tests/etc/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest==8.3.2 --r ../../src/build_rock/configure/requirements.txt \ No newline at end of file diff --git a/tests/fixtures/buffers.py b/tests/fixtures/buffers.py deleted file mode 100644 index 30993191..00000000 --- a/tests/fixtures/buffers.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest -from io import StringIO -import os -from pathlib import Path - - -@pytest.fixture -def str_buff(): - """String IO fixture for simulating a file object""" - with StringIO() as buffer: - yield buffer - - -@pytest.fixture -def github_output(monkeypatch, tmp_path): - - env_path = tmp_path / "env" - env_path.touch() - - monkeypatch.setitem(os.environ, "GITHUB_OUTPUT", str(env_path)) - - yield env_path diff --git a/tests/fixtures/sample_data.py b/tests/fixtures/sample_data.py deleted file mode 100644 index c3df9af1..00000000 --- a/tests/fixtures/sample_data.py +++ /dev/null @@ -1,26 +0,0 @@ -import pytest -import xml.etree.ElementTree as ET -import yaml -from .. import DATA_DIR - - -@pytest.fixture -def junit_with_failure(): - """Load ET of junit xml report with failure.""" - sample_file = DATA_DIR / "junit_xml_failure.xml" - - tree = ET.parse(sample_file) - root = tree.getroot() - return root - - -@pytest.fixture -def rockcraft_project(): - """Get sample rockcraft project file for testing.""" - - sample = DATA_DIR / "rockcraft.yaml" - - with open(sample) as rf: - project = yaml.safe_load(rf) - - return project diff --git a/tests/integration/test_convert_junit_xml_to_markdown.py b/tests/integration/test_convert_junit_xml_to_markdown.py deleted file mode 100644 index 9305985c..00000000 --- a/tests/integration/test_convert_junit_xml_to_markdown.py +++ /dev/null @@ -1,18 +0,0 @@ -from ..fixtures.buffers import str_buff -from ..fixtures.sample_data import junit_with_failure -import tools.junit_to_markdown.convert as report - - -def test_print_redirection(junit_with_failure, str_buff, capsys): - """Ensure that the report is entirely redirected when needed""" - - report.print_junit_report(junit_with_failure, str_buff) - report.print_junit_report(junit_with_failure, None) # print report to stdout - - str_buff.seek(0) - str_buff_content = str_buff.read() - - captured = capsys.readouterr() - stdout_content = captured.out - - assert stdout_content == str_buff_content, "Printing to multiple locations." diff --git a/tests/integration/test_junit_to_markdown_output.py b/tests/integration/test_junit_to_markdown_output.py deleted file mode 100644 index 9305985c..00000000 --- a/tests/integration/test_junit_to_markdown_output.py +++ /dev/null @@ -1,18 +0,0 @@ -from ..fixtures.buffers import str_buff -from ..fixtures.sample_data import junit_with_failure -import tools.junit_to_markdown.convert as report - - -def test_print_redirection(junit_with_failure, str_buff, capsys): - """Ensure that the report is entirely redirected when needed""" - - report.print_junit_report(junit_with_failure, str_buff) - report.print_junit_report(junit_with_failure, None) # print report to stdout - - str_buff.seek(0) - str_buff_content = str_buff.read() - - captured = capsys.readouterr() - stdout_content = captured.out - - assert stdout_content == str_buff_content, "Printing to multiple locations." diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/unit/test_generate_build_matrix.py b/tests/unit/test_generate_build_matrix.py deleted file mode 100644 index 7fca208d..00000000 --- a/tests/unit/test_generate_build_matrix.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 - -from src.build_rock.configure.generate_build_matrix import ( - get_target_archs, - configure_matrices, - MissingArchSupport, - set_build_config_outputs, -) -import pytest -from ..fixtures.buffers import github_output -from ..fixtures.sample_data import rockcraft_project - - -def test_get_target_archs(rockcraft_project): - """Test extraction of target architectures from rockcraft project configuration""" - - rockcraft_project["platforms"] = { - "amd64": None, - "armhf": {"build-for": ["armhf", "arm64"]}, - "ibm": {"build-on": ["s390x"], "build-for": "s390x"}, - } - - arches = get_target_archs(rockcraft_project) - assert arches == {"arm64", "armhf", "amd64"} - - -def test_configure_matrices(): - """Test correct configuration of build matrices from project's target arches""" - - build_matrices = configure_matrices(["amd64"], {"amd64": "ubuntu-22.04"}, False) - expected_result = { - "runner-build-matrix": { - "include": [{"architecture": "amd64", "runner": "ubuntu-22.04"}] - }, - "lpci-build-matrix": {"include": []}, - } - - assert build_matrices == expected_result - - -def test_configure_matrices_fallback_exception(): - """Test proper exception is raised when target arch is not buildable""" - with pytest.raises(MissingArchSupport): - configure_matrices(["arm64"], {"amd64": "ubuntu-22.04"}, False) - - -def test_configure_matrices_lpci_fallback(): - """Test lpci fallback logic when target cannot be built on a runner""" - build_matrices = configure_matrices(["arm64"], {"amd64": "ubuntu-22.04"}, True) - expected_result = { - "runner-build-matrix": {"include": []}, - "lpci-build-matrix": {"include": [{"architecture": "arm64"}]}, - } - - assert build_matrices == expected_result - - -def test_set_build_config_outputs(github_output): - """Test correct generation of build matrices.""" - - test_build_matrices = { - "runner-build-matrix": { - "include": [{"architecture": "amd64", "runner": "ubuntu-22.04"}] - }, - "lpci-build-matrix": {"include": []}, - } - - set_build_config_outputs("test", test_build_matrices) - - with open(github_output, "r") as fh: - gh_output = fh.read() - - expected_result = """rock-name=test -runner-build-matrix={"include": [{"architecture": "amd64", "runner": "ubuntu-22.04"}]} -lpci-build-matrix={"include": []} -""" - - assert gh_output == expected_result diff --git a/tests/unit/test_github_output.py b/tests/unit/test_github_output.py deleted file mode 100755 index 5cf11445..00000000 --- a/tests/unit/test_github_output.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 - -from src.shared.github_output import GithubOutput -from ..fixtures.buffers import github_output - - -def test_write(github_output): - """Test github_output write function""" - - outputs = { - "hello-world": 42, - } - expected_result = "hello-world=42\n" - - with GithubOutput() as output: - - output.write(**outputs) - - with open(github_output, "r") as fh: - result = fh.read() - - assert result == expected_result - - -def test_format_value_string(): - """Test formatting of string for outputs""" - - expected_result = "foo" - result = GithubOutput.format_value("foo") - - assert expected_result == result - - -def test_format_value_number(): - """Test formatting of number for outputs""" - - expected_result = "1" - result = GithubOutput.format_value(1) - - assert expected_result == result - - -def test_format_value_json(): - """Test formatting of JSON for outputs""" - - expected_result = '{"foo": "bar"}' - result = GithubOutput.format_value({"foo": "bar"}) - - assert expected_result == result diff --git a/tests/unit/test_junit_to_markdown_formatting.py b/tests/unit/test_junit_to_markdown_formatting.py deleted file mode 100644 index d010c669..00000000 --- a/tests/unit/test_junit_to_markdown_formatting.py +++ /dev/null @@ -1,148 +0,0 @@ -from ..fixtures.buffers import str_buff -import tools.junit_to_markdown.convert as report - -import xml.etree.ElementTree as ET - - -def test_print_element(str_buff): - """Ensure printed elements match expected result""" - - input_xml = """ - - This is example content. - """ - - expected_result = """
-message: This is an example attr
-text: 
-This is example content.
-
-""" - - root = ET.fromstring(input_xml) - - report.print_element(root, str_buff) - - str_buff.seek(0) - result = str_buff.read() - - assert result == expected_result - - -def test_get_chart_data_order(): - """Ensure chart wedges are ordered correctly""" - - input_xml = """ - - This is example content. - - - """ - - # fmt: off - # name, value, colour, default_order - expected_result = [ - ('pass', 3, '#0f0', 4), - ('error', 3, '#fa0', 2), - ('failed', 3, '#f00', 1), - ('skipped', 1, '#ff0', 3) - ] - # fmt: on - - root = ET.fromstring(input_xml) - - result = report.get_chart_data(root) - assert result == expected_result - - -def test_get_chart_data_removal(): - """Ensure zero width chart wedges are removed""" - - input_xml = """ - - """ - - # fmt: off - # name, value, colour, default_order - expected_result = [ - ('pass', 10, '#0f0', 4), - ] - # fmt: on - - root = ET.fromstring(input_xml) - - result = report.get_chart_data(root) - assert result == expected_result - - -def test_get_testcase_status_not_pass(): - """Test correct status icon selection""" - - for status, expected_result in report.STATUS_ICONS.items(): - - input_xml = f""" - <{status}> - """ - - root = ET.fromstring(input_xml) - result = report.get_testcase_status(root) - - assert result == expected_result - - -def test_get_testcase_status_default(): - """Test default status icon selection""" - - input_xml = f""" - - """ - - root = ET.fromstring(input_xml) - result = report.get_testcase_status(root) - - assert result == report.DEFAULT_STATUS_ICON - - -def test_print_header(str_buff): - """Ensure header is printed correctly""" - - input_xml = """ - - """ - - root = ET.fromstring(input_xml) - report.print_header(root, str_buff) - str_buff.seek(0) - result = str_buff.read() - - result_split = result.split() - - assert "#" == result_split[0], "result is not formatted as a level 1 header" - assert ":white_check_mark:" in result_split, "result is missing icon" - assert "pytest" in result_split, "result is missing name" - - -def print_testsuite_report(str_buff): - - input_xml = """ - - - - - - - """ - - root = ET.fromstring(input_xml) - report.print_header(root, str_buff) - str_buff.seek(0) - result = str_buff.read() - result_lines = result.splitlines() - - assert "pytest" in result_lines[0], "result is missing header" - assert any("```mermaid" in line for line in result_lines), "result is missing chart" - - # this may change if
is used for any other purpose than testcases - assert ( - sum("
" in line for line in result_lines) == 2 - ), "result has incorrect testcase test count" diff --git a/tools/cli-client/README.md b/tools/cli-client/README.md deleted file mode 100644 index 6975e08e..00000000 --- a/tools/cli-client/README.md +++ /dev/null @@ -1,68 +0,0 @@ - -# The OCI Factory CLI Client - -A CLI client that triggers GitHub workflows for building, uploading and releasing images in -[OCI Factory](https://github.com/canonical/oci-factory). - -## How to use - -### New User - -See ["How to Contribute as a Maintainer"](https://github.com/canonical/oci-factory?tab=readme-ov-file#as-a-maintainer--). - -Upon finishing the onboarding, the user should receive a GitHub Personal Access Token. This token grants you proper -permissions to trigger the build, upload and release of your rocks. This token should never be shared with -third-parties, nor put anywhere that is publicly available. - -The user will be asked to input the GitHub Personal Access Token upon triggering a workflow. For a non-interactive -terminal, it is possible to assign the token to the environmental variable `export GITHUB_TOKEN=`, and pass -`-y` to confirm the triggering by default. - -### Install using Snap - -```bash -sudo snap install oci-factory -``` - -### Install using Go - -#### Dependencies - -```bash -# install git -sudo apt update && sudo apt install -y git -``` - -Golang-go can be installed either with APT or Snap: - -```bash -# install golang-go with apt -sudo apt update && sudo apt install -y golang-go -``` - -```bash -# install golang-go with snap -sudo snap install go --classic -``` - -#### Install the CLI client -```bash -go install github.com/canonical/oci-factory/tools/cli-client/cmd/oci-factory -``` - -# Triggering workflows -Note: The workflow can only be triggered for the rocks owned by Canonical, i.e., whose repository -belong to the organization Canonical in GitHub. -```bash -cd -# Run `oci-factory upload --help` for more help -oci-factory upload --release track=,risks=[,...],eol=yyyy-mm-dd -``` - -## How to debug the CLI client - -The debug log can be enabled with the following command - -```bash -export LOGGER_DEBUG=1 -``` diff --git a/tools/cli-client/cmd/oci-factory/main.go b/tools/cli-client/cmd/oci-factory/main.go deleted file mode 100644 index 0ccd966d..00000000 --- a/tools/cli-client/cmd/oci-factory/main.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/canonical/oci-factory/tools/cli-client/internals/cli" - "github.com/canonical/oci-factory/tools/cli-client/internals/logger" -) - -func main() { - logger.SetLogger(logger.New(os.Stderr, fmt.Sprintf("[%s] ", "oci-factory"))) - cli.CliMain() -} diff --git a/tools/cli-client/go.mod b/tools/cli-client/go.mod deleted file mode 100644 index f31b9ffa..00000000 --- a/tools/cli-client/go.mod +++ /dev/null @@ -1,43 +0,0 @@ -module github.com/canonical/oci-factory/tools/cli-client - -go 1.22 - -require ( - github.com/briandowns/spinner v1.23.0 - github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8 - github.com/go-git/go-git/v5 v5.12.0 - golang.org/x/term v0.25.0 - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - dario.cat/mergo v1.0.0 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v1.0.0 // indirect - github.com/cloudflare/circl v1.3.7 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect - github.com/emirpasic/gods v1.18.1 // indirect - github.com/fatih/color v1.7.0 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-isatty v0.0.8 // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect - github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/skeema/knownhosts v1.2.2 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/tools v0.22.0 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect -) diff --git a/tools/cli-client/go.sum b/tools/cli-client/go.sum deleted file mode 100644 index cba37a87..00000000 --- a/tools/cli-client/go.sum +++ /dev/null @@ -1,165 +0,0 @@ -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= -github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= -github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8 h1:zGaJEJI9qPVyM+QKFJagiyrM91Ke5S9htoL1D470g6E= -github.com/canonical/go-flags v0.0.0-20230403090104-105d09a091b8/go.mod h1:ZZFeR9K9iGgpwOaLYF9PdT44/+lfSJ9sQz3B+SsGsYU= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= -github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= -github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= -github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/cli-client/internals/cli/cli_main.go b/tools/cli-client/internals/cli/cli_main.go deleted file mode 100644 index d6db002e..00000000 --- a/tools/cli-client/internals/cli/cli_main.go +++ /dev/null @@ -1,79 +0,0 @@ -package cli - -import ( - "bufio" - "fmt" - "io" - "os" - "strings" - - "github.com/canonical/go-flags" -) - -type CmdRelease struct { - Revision int `long:"revision" description:"The revision number of the image release"` -} - -const longDescription = ` -The OCI Factory CLI client is a tool that builds, tests, and releases the OCI -images owned by Canonical using the Github workflow in the OCI Factory repository. -` - -var opts struct { - // TODO: Leave `release` here for now. Should be moved into a `cmd_release.go` in phase 2. - // CmdRelease CmdRelease `command:"release" description:"Release (re-tag) the image into the container registries"` - SkipConfirmation bool `short:"y" description:"Skip the confirmation to upload/release an image"` -} - -var parser = flags.NewParser(&opts, flags.Default) - -func CliMain() error { - addHelp(parser) - if len(os.Args) == 1 || os.Args[1] == "help" { - parser.WriteHelp(os.Stdout) - os.Exit(0) - } - if _, err := parser.Parse(); err != nil { - switch flagsErr := err.(type) { - case flags.ErrorType: - if flagsErr == flags.ErrHelp { - os.Exit(0) - } - os.Exit(1) - default: - os.Exit(1) - } - } - return nil -} - -func addHelp(p *flags.Parser) { - p.ShortDescription = "CLI client to build, test and release OCI images" - p.LongDescription = longDescription -} - -func blockForConfirm(s string) error { - // check if is a tty - fi, err := os.Stdin.Stat() - if err != nil && fi.Mode()&os.ModeNamedPipe != 0 { - return fmt.Errorf("non-interactive terminal detected, run with -y option") - } else if err != nil { - return err - } - - r := bufio.NewReader(os.Stdin) - fmt.Printf("%s [y/N]: ", s) - res, err := r.ReadString('\n') - if err == io.ErrUnexpectedEOF || err == io.EOF { - return fmt.Errorf("cancelled") - } else if err != nil { - return err - } - - res = strings.ToLower(strings.TrimSpace(res)) - - if res != "y" && res != "yes" { - return fmt.Errorf("cancelled") - } - return nil -} diff --git a/tools/cli-client/internals/cli/cli_upload.go b/tools/cli-client/internals/cli/cli_upload.go deleted file mode 100644 index 81bc52c3..00000000 --- a/tools/cli-client/internals/cli/cli_upload.go +++ /dev/null @@ -1,157 +0,0 @@ -package cli - -import ( - "fmt" - "os" - "regexp" - "slices" - "strings" - - "github.com/canonical/oci-factory/tools/cli-client/internals/client" - "github.com/canonical/oci-factory/tools/cli-client/internals/trigger" -) - -type UploadRelease struct { - Track string - Risks []string - EndOfLife string -} - -type CmdUpload struct { - UploadRelease []string `long:"release" description:"Release images to container registries.\nSyntax: --release track=,risks=[,...],eol=yyyy-mm-dd"` -} - -var riskOptions = []string{"stable", "candidate", "beta", "edge"} - -func init() { - parser.AddCommand("upload", "Trigger the build and release for a rock", - `Trigger the build of a rock with the rockcraft.yaml in the current working - directory and the release with --release`, - &CmdUpload{}) -} - -func (c *CmdUpload) Execute(args []string) error { - releases, err := parseUploadReleases(c.UploadRelease) - if err != nil { - return fmt.Errorf("error parsing release arguments: %v", err) - } - triggerUploadReleases(releases) - return nil -} - -func checkMissingFields(release UploadRelease) []string { - var missing []string - if release.Track == "" { - missing = append(missing, "track") - } - if len(release.Risks) == 0 { - missing = append(missing, "risks") - } - if release.EndOfLife == "" { - missing = append(missing, "eol") - } - return missing -} - -// parseUploadReleases parses the release arguments and returns a list of UploadRelease structs. -// The release arguments are expected to be in the format "track=,risks=[,...],eol=yyyy-mm-dd". -// Multiple release arguments can be passed, separated by spaces. -func parseUploadReleases(args []string) ([]UploadRelease, error) { - releases := make([]UploadRelease, 0) - regex := regexp.MustCompile(`(\w+)=(([^,=]*?,)*)`) - - for _, origArgStr := range args { - // Append a "," to the end of the string to enable a simpler regex matching key-value pairs - argStr := origArgStr + "," - var release UploadRelease - matches := regex.FindAllStringSubmatch(argStr, -1) - if matches == nil { - return nil, fmt.Errorf("invalid argument: %s", origArgStr) - } - - for _, part := range matches { - if part == nil || len(part) < 3 { - return nil, fmt.Errorf("invalid argument: %s", origArgStr) - } - key := part[1] - valueString := strings.TrimRight(part[2], ",") - values := strings.Split(valueString, ",") - - switch key { - case "track": - if len(values) != 1 || values[0] == "" { - return nil, fmt.Errorf("invalid track value: %s", valueString) - } - if release.Track != "" { - return nil, fmt.Errorf("duplicated value for track") - } - release.Track = values[0] - case "risks": - for _, risk := range values { - if !slices.Contains(riskOptions, risk) { - return nil, fmt.Errorf("invalid risk value: %s", risk) - } - } - if len(release.Risks) > 0 { - return nil, fmt.Errorf("duplicated value for risks") - } - release.Risks = values - case "eol": - if len(values) != 1 || values[0] == "" { - return nil, fmt.Errorf("invalid eol value: %s", valueString) - } - eol, err := validateAndFormatDate(values[0]) - if err != nil { - return nil, fmt.Errorf("EOL with wrong data formats: %v", err) - } - if release.EndOfLife != "" { - return nil, fmt.Errorf("duplicated value for eol") - } - release.EndOfLife = eol - default: - return nil, fmt.Errorf("invalid key-value pair: %s=%s", key, valueString) - } - } - - // Check if all required fields are present - if missing := checkMissingFields(release); missing != nil { - return nil, fmt.Errorf("missing fields: %s", strings.Join(missing, ", ")) - } - - releases = append(releases, release) - } - if len(releases) == 0 { - return nil, fmt.Errorf("no release track specified, no build will be triggered") - } - return releases, nil -} - -func triggerUploadReleases(releases []UploadRelease) { - // really builds - buildMetadata := trigger.InferBuildMetadata() - var uploadReleaseTrack = make(trigger.UploadReleaseTrack) - for _, release := range releases { - uploadReleaseTrack[release.Track] = trigger.UploadRelease{ - EndOfLife: release.EndOfLife, - UploadReleaseRisks: trigger.UploadReleaseRisks{ - Risks: release.Risks, - }, - } - } - imageTrigger := trigger.NewUploadImageTrigger(buildMetadata, uploadReleaseTrack) - uploadTrigger := trigger.NewUploadTrigger([]trigger.UploadImageTrigger{imageTrigger}) - imageName := trigger.GetRockcraftImageName() - payload := client.NewPayload(imageName, uploadTrigger.ToYamlString()) - fmt.Printf("The %s image will be built and released with following triggers:\n", imageName) - fmt.Println(uploadTrigger.ToYamlString()) - if !opts.SkipConfirmation { - err := blockForConfirm("Do you want to continue?") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - } - externalRefID := payload.Inputs.ExternalRefID - client.DispatchWorkflow(payload) - client.WorkflowPolling(externalRefID) -} diff --git a/tools/cli-client/internals/cli/cli_upload_test.go b/tools/cli-client/internals/cli/cli_upload_test.go deleted file mode 100644 index c7deaad9..00000000 --- a/tools/cli-client/internals/cli/cli_upload_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package cli_test - -import ( - . "gopkg.in/check.v1" - - "github.com/canonical/oci-factory/tools/cli-client/internals/cli" -) - -// `func Test(t *testing.T) { TestingT(t) }` defined in validator_test.go - -type CmdUploadSuite struct{} - -var _ = Suite(&CmdUploadSuite{}) - -func (s *CmdUploadSuite) TestParseUploadReleases(c *C) { - for _, test := range []struct { - in []string - out []cli.UploadRelease - errMsg string - err bool - }{ - { - []string{"track=1.0.0,risks=beta,stable,eol=2023-01-01"}, - []cli.UploadRelease{ - { - Track: "1.0.0", - Risks: []string{"beta", "stable"}, - EndOfLife: "2023-01-01T00:00:00Z", - }, - }, - "", - false, - }, - { - []string{"risks=high,eol=2023-01-02,track=1.0.0"}, - []cli.UploadRelease{ - {}, - }, - "invalid risk value: high", - true, - }, - { - []string{"track=some-track_22.04"}, - []cli.UploadRelease{ - {}, - }, - "missing fields: risks, eol", - true, - }, - { // TODO this should fail when proper regex is implemented - []string{"track=1.0.0,risks=stable,candidate,eol=2023-01-03,extra=field"}, - // TODO change to empty cli.UploadRelease - []cli.UploadRelease{ - { - Track: "1.0.0", - Risks: []string{"stable", "candidate"}, - EndOfLife: "2023-01-03T00:00:00Z", - }, - }, - "invalid key-value pair: extra=field", - // TODO change to true - true, - }, - { - []string{"noname=1.0.0,risks=high,low,eol=2023-01-04"}, - []cli.UploadRelease{ - {}, - }, - "invalid key-value pair: noname=1.0.0", - true, - }, - { - []string{"track==1.0.0,risks=beta,eol=2023-01-05"}, - []cli.UploadRelease{ - {}, - }, - "invalid track value: ", - true, - }, - { - []string{"track=1.0.0,risks=beta,eol=,2023-01-05"}, - []cli.UploadRelease{ - {}, - }, - "invalid eol value: ,2023-01-05", - true, - }, - { - []string{"track=1.0.0,risks=stable,eol=2023-01-06,,,"}, - []cli.UploadRelease{ - { - Track: "1.0.0", - Risks: []string{"stable"}, - EndOfLife: "2023-01-06T00:00:00Z", - }, - }, - "", - false, - }, - { - []string{"track=aaa23345,,,,,,risks=edge,,,,,eol=2023-01-07,,,,,,"}, - []cli.UploadRelease{ - { - Track: "aaa23345", - Risks: []string{"edge"}, - EndOfLife: "2023-01-07T00:00:00Z", - }, - }, - "", - false, - }, - { - []string{"track=1.0.0,risks=stable,eol=2023-01-08", - "track=1.0.1,risks=stable,eol=2023-01-09"}, - []cli.UploadRelease{ - { - Track: "1.0.0", - Risks: []string{"stable"}, - EndOfLife: "2023-01-08T00:00:00Z", - }, - { - Track: "1.0.1", - Risks: []string{"stable"}, - EndOfLife: "2023-01-09T00:00:00Z", - }, - }, - "", - false, - }, - { - []string{"track=1.0.0,risks=stable,eol=2023-01-10,track=2.0.0"}, - []cli.UploadRelease{ - {}, - }, - "duplicated value for track", - true, - }, - { - []string{"track=1.0.0,risks=stable,eol=2023-01-10,risks=candidate"}, - []cli.UploadRelease{ - {}, - }, - "duplicated value for risks", - true, - }, - { - []string{"eol=2024-05-31,track=1.0.0,risks=stable,eol=2023-01-10"}, - []cli.UploadRelease{ - {}, - }, - "duplicated value for eol", - true, - }, - { - []string{"track=1.0.0,eol=2024-05-31,risks=stable,eol=2023-01-10,track=2.0.0"}, - []cli.UploadRelease{ - {}, - }, - "duplicated value for eol", - true, - }, - } { - releases, err := cli.ParseUploadReleases(test.in) - if test.err { - c.Assert(err, ErrorMatches, test.errMsg) - continue - } - c.Assert(err, IsNil) - c.Assert(releases, DeepEquals, test.out) - } -} diff --git a/tools/cli-client/internals/cli/export_test.go b/tools/cli-client/internals/cli/export_test.go deleted file mode 100644 index 63775a3b..00000000 --- a/tools/cli-client/internals/cli/export_test.go +++ /dev/null @@ -1,6 +0,0 @@ -package cli - -var ( - ValidateAndFormatDate = validateAndFormatDate - ParseUploadReleases = parseUploadReleases -) diff --git a/tools/cli-client/internals/cli/validator.go b/tools/cli-client/internals/cli/validator.go deleted file mode 100644 index e44730cc..00000000 --- a/tools/cli-client/internals/cli/validator.go +++ /dev/null @@ -1,18 +0,0 @@ -package cli - -import ( - "fmt" - "time" -) - -func validateAndFormatDate(dateStr string) (string, error) { - // Parse the date string to check its validity - parsedDate, err := time.Parse("2006-01-02", dateStr) - if err != nil { - return "", fmt.Errorf("invalid date format: %w", err) - } - - // Add the time component to be 00:00:00Z - formattedDate := parsedDate.Format("2006-01-02T15:04:05Z") - return formattedDate, nil -} diff --git a/tools/cli-client/internals/cli/validator_test.go b/tools/cli-client/internals/cli/validator_test.go deleted file mode 100644 index 18fdd8fc..00000000 --- a/tools/cli-client/internals/cli/validator_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package cli_test - -import ( - "testing" - - . "gopkg.in/check.v1" - - "github.com/canonical/oci-factory/tools/cli-client/internals/cli" -) - -func Test(t *testing.T) { TestingT(t) } - -type ValidatorSuite struct{} - -func (vs *ValidatorSuite) TestValidateAndFormatDateInput(c *C) { - for _, date := range []struct { - in string - err bool - }{ - {"2006-07-08", false}, - {"2008-09-10", false}, - {"2024-05-01", false}, - {"2001-02-29", true}, - {"2024-05-32", true}, - {"01-01-2023", true}, - } { - _, err := cli.ValidateAndFormatDate(date.in) - if date.err { - c.Assert(err, Equals, date.err) - break - } - c.Assert(err, IsNil) - } -} diff --git a/tools/cli-client/internals/client/client.go b/tools/cli-client/internals/client/client.go deleted file mode 100644 index 6945979e..00000000 --- a/tools/cli-client/internals/client/client.go +++ /dev/null @@ -1,62 +0,0 @@ -package client - -import ( - "bytes" - "io" - "net/http" - "os" - "time" - - "github.com/canonical/oci-factory/tools/cli-client/internals/logger" -) - -const NumRetries = 2 -const RetryInterval = 5 * time.Second - -func SendRequest(requestType, url string, payload []byte, expectedStatusCode int) []byte { - client := &http.Client{} - - req, err := http.NewRequest(requestType, url, bytes.NewBuffer(payload)) - if err != nil { - logger.Panicf("failed to create request: %v", err) - } - header := NewGithubAuthHeaderMap() - SetHeaderWithMap(req, header) - - // Send the request, and retries if the response returns 503 - var respBody []byte - for i := 0; i < NumRetries; i++ { - resp, err := client.Do(req) - if err != nil { - logger.Panicf("failed to send request: %v", err) - } - defer resp.Body.Close() - - respBody, err = io.ReadAll(resp.Body) - if err != nil { - logger.Panicf("failed to read response body: %v", err) - } - - if resp.StatusCode == expectedStatusCode { - break - } - - if resp.StatusCode == 503 { - logger.Noticef("Request failed: %s", resp.Status) - logger.Noticef("Retrying request %d/%d", i+1, NumRetries) - time.Sleep(RetryInterval) - continue - } - - if resp.StatusCode == 401 { - logger.Noticef("Request failed: %s", resp.Status) - logger.Noticef("Please check if your Github token is correct") - os.Exit(1) - } - - logger.Noticef("Request failed: %s", resp.Status) - logger.Panicf("Response: %s", respBody) - } - - return respBody -} diff --git a/tools/cli-client/internals/client/client_test.go b/tools/cli-client/internals/client/client_test.go deleted file mode 100644 index 1d503907..00000000 --- a/tools/cli-client/internals/client/client_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package client_test - -import ( - "io" - "net/http" - "net/http/httptest" - "testing" - - . "gopkg.in/check.v1" - - "github.com/canonical/oci-factory/tools/cli-client/internals/client" - "github.com/canonical/oci-factory/tools/cli-client/internals/token" -) - -type ClientSuite struct{} - -func Test(t *testing.T) { TestingT(t) } - -var _ = Suite(&ClientSuite{}) - -func (s *ClientSuite) TestSendRequest(c *C) { - mockPayload := []byte(`{"mock":"payload"}`) - expectedStatusCode := http.StatusOK - - // Create a mock server - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify the request method, URL, and payload - c.Assert(r.Method, Equals, http.MethodPost) - body, _ := io.ReadAll(r.Body) - c.Assert(body, DeepEquals, mockPayload) - // Set the response status code and body - w.WriteHeader(expectedStatusCode) - w.Write([]byte(`{"mock":"response"}`)) - })) - defer mockServer.Close() - - restoreEnvToken := token.SetEnvToken("ghp_AAAAAAAA") - defer restoreEnvToken() - // Call the SendRequest function - response := client.SendRequest(http.MethodPost, mockServer.URL, mockPayload, expectedStatusCode) - - // Verify the response - expectedResponse := []byte(`{"mock":"response"}`) - - c.Assert(response, DeepEquals, expectedResponse) -} - -func (s *ClientSuite) TestSendRequestWithServerBusy(c *C) { - mockPayload := []byte(`{"mock":"payload"}`) - expectedStatusCode := http.StatusOK - cnt := 0 - - // Create a mock server - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify the request method, URL, and payload - c.Assert(r.Method, Equals, http.MethodPost) - body, _ := io.ReadAll(r.Body) - c.Assert(body, DeepEquals, mockPayload) - // Set the response status code and body - if cnt == 0 { - w.WriteHeader(http.StatusServiceUnavailable) - w.Write([]byte(`{"mock":"response"}`)) - } else { - w.WriteHeader(expectedStatusCode) - w.Write([]byte(`{"mock":"response"}`)) - } - cnt++ - })) - defer mockServer.Close() - - restoreEnvToken := token.SetEnvToken("ghp_AAAAAAAA") - defer restoreEnvToken() - // Call the SendRequest function - response := client.SendRequest(http.MethodPost, mockServer.URL, mockPayload, expectedStatusCode) - - // Verify the response - expectedResponse := []byte(`{"mock":"response"}`) - - c.Assert(response, DeepEquals, expectedResponse) -} diff --git a/tools/cli-client/internals/client/export_test.go b/tools/cli-client/internals/client/export_test.go deleted file mode 100644 index 57f8ca8d..00000000 --- a/tools/cli-client/internals/client/export_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package client - -var ( - GetWorkflowRunID = getWorkflowRunID - GetWorkflowRunStatusFromResp = getWorkflowRunStatusFromResp - GetWorkflowJobsProgressFromResp = getWorkflowJobsProgressFromResp -) - -func SetWorkflowDispatchURL(url string) (restore func()) { - orig := workflowDispatchURL - workflowDispatchURL = url - return func() { workflowDispatchURL = orig } -} diff --git a/tools/cli-client/internals/client/wf_dispatcher.go b/tools/cli-client/internals/client/wf_dispatcher.go deleted file mode 100644 index 43a0defd..00000000 --- a/tools/cli-client/internals/client/wf_dispatcher.go +++ /dev/null @@ -1,66 +0,0 @@ -package client - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/canonical/oci-factory/tools/cli-client/internals/logger" - "github.com/canonical/oci-factory/tools/cli-client/internals/token" -) - -var workflowDispatchURL = "https://api.github.com/repos/canonical/oci-factory/actions/workflows/Image.yaml/dispatches" - -type Inputs struct { - OciImageName string `json:"oci-image-name"` - B64ImageTrigger string `json:"b64-image-trigger"` - Upload bool `json:"upload"` - ExternalRefID string `json:"external_ref_id"` -} - -type Payload struct { - Ref string `json:"ref"` - Inputs Inputs `json:"inputs"` -} - -func NewGithubAuthHeaderMap() map[string]string { - accessToken := token.GetAccessToken() - return map[string]string{ - "Accept": "application/vnd.github+json", - "Authorization": fmt.Sprintf("Bearer %s", accessToken), - "X-GitHub-Api-Version": "2022-11-28", - } -} - -func SetHeaderWithMap(request *http.Request, headerMap map[string]string) { - for key, value := range headerMap { - request.Header.Set(key, value) - } -} - -// Don't forget to keep the ExternalRefID to track the workflow -func NewPayload(imageName string, uberImageTrigger string) Payload { - uberImageTriggerB64 := base64.StdEncoding.EncodeToString([]byte(uberImageTrigger)) - payload := Payload{ - Ref: "main", - Inputs: Inputs{ - OciImageName: imageName, - B64ImageTrigger: uberImageTriggerB64, - Upload: true, - ExternalRefID: fmt.Sprintf("cli-client-%s-%d", imageName, time.Now().Unix()), - }, - } - return payload -} - -// Dispatch GitHub workflow with http request -func DispatchWorkflow(payload Payload) { - payloadJSON, err := json.Marshal(payload) - if err != nil { - logger.Panicf("Unable to marshal payload: %s", err) - } - - SendRequest(http.MethodPost, workflowDispatchURL, payloadJSON, http.StatusNoContent) -} diff --git a/tools/cli-client/internals/client/wf_dispatcher_test.go b/tools/cli-client/internals/client/wf_dispatcher_test.go deleted file mode 100644 index affadcf2..00000000 --- a/tools/cli-client/internals/client/wf_dispatcher_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package client_test - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "os" - - . "gopkg.in/check.v1" - - "github.com/canonical/oci-factory/tools/cli-client/internals/client" - "github.com/canonical/oci-factory/tools/cli-client/internals/token" -) - -type DispatcherSuite struct{} - -// `func Test(t *testing.T) { TestingT(t) }` defined in client_test.go - -var _ = Suite(&DispatcherSuite{}) - -func (s *DispatcherSuite) TestSetHeaderWithMap(c *C) { - mockUrl := "https://mock.url" - mockJson := []byte(`{"mock":"json"}`) - os.Setenv(token.TokenVarName, "ghp_AAAAAAAA") - header := client.NewGithubAuthHeaderMap() - request1, _ := http.NewRequest("POST", mockUrl, bytes.NewBuffer(mockJson)) - client.SetHeaderWithMap(request1, header) - request2, _ := http.NewRequest("POST", mockUrl, bytes.NewBuffer(mockJson)) - request2.Header.Set("Accept", "application/vnd.github+json") - request2.Header.Set("Authorization", "Bearer "+token.GetAccessToken()) - request2.Header.Set("X-GitHub-Api-Version", "2022-11-28") - - c.Assert(fmt.Sprintf("%+v", request1), Equals, fmt.Sprintf("%+v", request2)) -} - -func (s *DispatcherSuite) TestDispatchWorkflow(c *C) { - mockJson := []byte(`{"mock":"json"}`) - restoreEnvToken := token.SetEnvToken("ghp_AAAAAAAA") - defer restoreEnvToken() - header := client.NewGithubAuthHeaderMap() - - expectedPayload := client.NewPayload("image-name", "image-trigger") - expectedPayloadJSON, _ := json.Marshal(expectedPayload) - - // Create a mock server - mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Verify the request method, URL, and payload - c.Assert(r.Method, Equals, http.MethodPost) - c.Assert(r.Header.Get("Accept"), Equals, "application/vnd.github+json") - c.Assert(r.Header.Get("Authorization"), Equals, "Bearer "+token.GetAccessToken()) - c.Assert(r.Header.Get("X-GitHub-Api-Version"), Equals, "2022-11-28") - // header setting is tested above and transferring is tested in the http library - body, _ := io.ReadAll(r.Body) - c.Assert(body, DeepEquals, expectedPayloadJSON) - // Set the no content response status code and body - w.WriteHeader(http.StatusNoContent) - w.Write([]byte(``)) - })) - defer mockServer.Close() - - request, _ := http.NewRequest(http.MethodPost, mockServer.URL, bytes.NewBuffer(mockJson)) - client.SetHeaderWithMap(request, header) - - // Call the DispatchWorkflow function - storeWorkflowDispatchURL := client.SetWorkflowDispatchURL(mockServer.URL) - defer storeWorkflowDispatchURL() - client.DispatchWorkflow(expectedPayload) -} diff --git a/tools/cli-client/internals/client/wf_poller.go b/tools/cli-client/internals/client/wf_poller.go deleted file mode 100644 index 6496086d..00000000 --- a/tools/cli-client/internals/client/wf_poller.go +++ /dev/null @@ -1,197 +0,0 @@ -package client - -import ( - "encoding/json" - "fmt" - "net/http" - "os" - "strings" - "time" - - "github.com/briandowns/spinner" - "github.com/canonical/oci-factory/tools/cli-client/internals/logger" -) - -const workflowRunsURL = "https://api.github.com/repos/canonical/oci-factory/actions/workflows/Image.yaml/runs" -const workflowSingleRunURL = "https://api.github.com/repos/canonical/oci-factory/actions/runs/" -const workflowSingleRunWebURL = "https://github.com/canonical/oci-factory/actions/runs/" - -const getWorkflowRunIdMaxTry = 60 -const getWorkflowRunTimeWindow = 5 * time.Minute -const spinnerRate = 500 * time.Millisecond -const pollingInterval = 5 * time.Second - -type WorkflowRunsScheme struct { - TotalCount int `json:"total_count"` - WorkflowRuns []WorkflowSingleRunScheme `json:"workflow_runs"` -} - -const ( - StatusCompleted string = "completed" - StatusInProgress string = "in_progress" - StatusQueued string = "queued" - StatusRequested string = "requested" - StatusWaiting string = "waiting" - StatusPending string = "pending" -) - -const ( - ConclusionActionRequired string = "action_required" - ConclusionCancelled string = "cancelled" - ConclusionFailure string = "failure" - ConclusionNeutral string = "neutral" - ConclusionSkipped string = "skipped" - ConclusionStale string = "stale" - ConclusionSuccess string = "success" - ConclusionTimedOut string = "timed_out" -) - -type WorkflowSingleRunScheme struct { - Id int `json:"id"` - JobsURL string `json:"jobs_url"` - Status string `json:"status"` - Conclusion string `json:"conclusion"` -} - -type WorkflowJobsScheme struct { - Jobs []WorkflowSingleJobScheme `json:"jobs"` -} - -type WorkflowSingleJobScheme struct { - Name string `json:"name"` - Steps []WorkflowSingleStepScheme `json:"steps"` - Status string `json:"status"` - Conclusion string `json:"conclusion"` -} - -type WorkflowSingleStepScheme struct { - Name string `json:"name"` -} - -// TODO how to implement test for nested http requests locally? -// Maybe an end-to-end test with a mock server? -func getWorkflowRunID(externalRefID string) (int, error) { - // Get the current time in UTC and subtract the deltaTime - timeWindow := time.Now().UTC().Add(-getWorkflowRunTimeWindow).Format("2006-01-02T15:04") - timeWindowFilter := "?created=%3E" + timeWindow - - s := spinner.New(spinner.CharSets[9], spinnerRate) - for numTries := 1; numTries < getWorkflowRunIdMaxTry+1; numTries++ { - responseBody := SendRequest(http.MethodGet, workflowRunsURL+timeWindowFilter, nil, http.StatusOK) - - // logger.Debugf("Workflow run response: %s", string(responseBody)) - var workflowRuns WorkflowRunsScheme - err := json.Unmarshal(responseBody, &workflowRuns) - if err != nil { - logger.Panicf("Unable to unmarshal json: %v", err) - } - - for _, workFlowSingleRun := range workflowRuns.WorkflowRuns { - jobsURL := workFlowSingleRun.JobsURL - responseBody := SendRequest(http.MethodGet, jobsURL, nil, http.StatusOK) - - // logger.Debugf("Workflow jobs response: %s", string(responseBody)) - var workflowJobs WorkflowJobsScheme - err = json.Unmarshal(responseBody, &workflowJobs) - if err != nil { - logger.Panicf("Unable to unmarshal json: %v", err) - } - - for _, job := range workflowJobs.Jobs { - if job.Name == "Prepare build" { - for _, step := range job.Steps { - logger.Debugf("Step name: %s", step.Name) - if step.Name == externalRefID { - s.Stop() - return workFlowSingleRun.Id, nil - } - } - } - } - } - s.Prefix = fmt.Sprintf("Waiting for task %s to show up (retry %d/%d) ", - externalRefID, numTries, getWorkflowRunIdMaxTry) - s.Start() - time.Sleep(pollingInterval) - logger.Debugf("Retrying getting workflow run ID for %s (%d/%d)\n", - externalRefID, numTries, getWorkflowRunIdMaxTry) - } - - return -1, fmt.Errorf("get workflow run ID failed after max tryouts") -} - -// Split out the response handling for testing -func getWorkflowRunStatusFromResp(responseBody []byte) (string, string) { - var workflowSingleRun WorkflowSingleRunScheme - err := json.Unmarshal(responseBody, &workflowSingleRun) - if err != nil { - logger.Panicf("Unable to unmarshal json: %v", err) - } - - return workflowSingleRun.Status, workflowSingleRun.Conclusion -} - -// Gets the status and conclusion of a workflow run -func getWorkflowRunStatus(runId int) (string, string) { - responseBody := SendRequest(http.MethodGet, workflowSingleRunURL+fmt.Sprint(runId), nil, http.StatusOK) - return getWorkflowRunStatusFromResp(responseBody) -} - -// Split out the response handling for testing -func getWorkflowJobsProgressFromResp(responseBody []byte) (int, int, string) { - var workflowJobs WorkflowJobsScheme - err := json.Unmarshal(responseBody, &workflowJobs) - if err != nil { - logger.Panicf("Unable to unmarshal json: %v", err) - } - - numJobs := len(workflowJobs.Jobs) - iInProgress := 0 - for i, job := range workflowJobs.Jobs[iInProgress:] { - if job.Status == StatusInProgress || job.Status == StatusQueued { - iInProgress += i - return iInProgress + 1, numJobs, job.Name - } else if job.Status == StatusCompleted && i+iInProgress == numJobs-1 { - // All finished - return numJobs, numJobs, "" - } - } - return -1, -1, "" -} - -// Gets the progress of a workflow run -func getWorkflowJobsProgress(runId int) (int, int, string) { - jobsURL := workflowSingleRunURL + fmt.Sprint(runId) + "/jobs" - responseBody := SendRequest(http.MethodGet, jobsURL, nil, http.StatusOK) - return getWorkflowJobsProgressFromResp(responseBody) -} - -// TODO how to implement test for calls into GetWorkflowRunID? -func WorkflowPolling(workflowExtRefId string) { - runId, err := getWorkflowRunID(workflowExtRefId) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to get workflow run ID: %v", err) - os.Exit(1) - } - logger.Debugf("%d\n", runId) - - fmt.Printf("Task %s started. Details available at %s%d.\n", workflowExtRefId, - workflowSingleRunWebURL, runId) - - s := spinner.New(spinner.CharSets[9], spinnerRate) - for { - status, conclusion := getWorkflowRunStatus(runId) - if status == StatusCompleted { - s.Stop() - fmt.Printf("Task %s finished with status %s\n", workflowExtRefId, conclusion) - break - } else { - currJob, totalJobs, jobName := getWorkflowJobsProgress(runId) - s.Prefix = fmt.Sprintf("Task %s is currently %s: %s (%d/%d) ", - workflowExtRefId, strings.ReplaceAll(status, "_", " "), - strings.Split(jobName, " (")[0], currJob, totalJobs) - s.Start() - time.Sleep(pollingInterval) - } - } -} diff --git a/tools/cli-client/internals/client/wf_poller_test.go b/tools/cli-client/internals/client/wf_poller_test.go deleted file mode 100644 index b770f9c0..00000000 --- a/tools/cli-client/internals/client/wf_poller_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package client_test - -import ( - . "gopkg.in/check.v1" - - "github.com/canonical/oci-factory/tools/cli-client/internals/client" -) - -type PollerSuite struct{} - -// `func Test(t *testing.T) { TestingT(t) }` defined in client_test.go - -var _ = Suite(&PollerSuite{}) - -func (s *PollerSuite) TestGetWorkflowRunStatusFromResp(c *C) { - mockResponseBody := []byte(`{"status":"completed","conclusion":"success"}`) - expectedStatus := "completed" - expectedConclusion := "success" - - status, conclusion := client.GetWorkflowRunStatusFromResp(mockResponseBody) - - // Verify the response - c.Assert(status, Equals, expectedStatus) - c.Assert(conclusion, Equals, expectedConclusion) -} - -func (s *PollerSuite) TestGetWorkflowJobsProgressFromResp(c *C) { - mockResponseBody := []byte(`{"jobs":[{"name":"Job 1","status":"completed"},{"name":"Job 2","status":"in_progress"},{"name":"Job 3","status":"queued"}]}`) - expectedCurrJob := 2 - expectedTotalJobs := 3 - expectedJobName := "Job 2" - - currJob, totalJobs, jobName := client.GetWorkflowJobsProgressFromResp(mockResponseBody) - - // Verify the response - c.Assert(currJob, Equals, expectedCurrJob) - c.Assert(totalJobs, Equals, expectedTotalJobs) - c.Assert(jobName, Equals, expectedJobName) -} diff --git a/tools/cli-client/internals/logger/logger.go b/tools/cli-client/internals/logger/logger.go deleted file mode 100644 index 6c4ac88d..00000000 --- a/tools/cli-client/internals/logger/logger.go +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) 2021 Canonical Ltd -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License version 3 as -// published by the Free Software Foundation. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package logger - -import ( - "bytes" - "fmt" - "io" - "os" - "sync" - "time" -) - -const ( - timestampFormat = "2006-01-02T15:04:05.000Z07:00" -) - -// A Logger is a fairly minimal logging tool. -type Logger interface { - // Notice is for messages that the user should see - Notice(msg string) - // Debug is for messages that the user should be able to find if they're debugging something - Debug(msg string) -} - -type nullLogger struct{} - -func (nullLogger) Notice(string) {} -func (nullLogger) Debug(string) {} - -// NullLogger is a logger that does nothing -var NullLogger = nullLogger{} - -var ( - logger Logger = NullLogger - loggerLock sync.Mutex -) - -// Panicf notifies the user and then panics -func Panicf(format string, v ...interface{}) { - loggerLock.Lock() - defer loggerLock.Unlock() - msg := fmt.Sprintf(format, v...) - logger.Notice("PANIC " + msg) - panic(msg) -} - -// Noticef notifies the user of something -func Noticef(format string, v ...interface{}) { - loggerLock.Lock() - defer loggerLock.Unlock() - msg := fmt.Sprintf(format, v...) - logger.Notice(msg) -} - -// Debugf records something in the debug log -func Debugf(format string, v ...interface{}) { - loggerLock.Lock() - defer loggerLock.Unlock() - msg := fmt.Sprintf(format, v...) - logger.Debug(msg) -} - -// MockLogger replaces the existing logger with a buffer and returns -// the log buffer and a restore function. -func MockLogger(prefix string) (buf *bytes.Buffer, restore func()) { - buf = &bytes.Buffer{} - oldLogger := SetLogger(New(buf, prefix)) - return buf, func() { - SetLogger(oldLogger) - } -} - -// SetLogger sets the global logger to the given one. It must be called -// from a single goroutine before any logs are written. -func SetLogger(l Logger) (old Logger) { - loggerLock.Lock() - defer loggerLock.Unlock() - old = logger - logger = l - return old -} - -type defaultLogger struct { - w io.Writer - prefix string - - buf []byte -} - -// Debug only prints if LOGGER_DEBUG is set. -func (l *defaultLogger) Debug(msg string) { - if os.Getenv("LOGGER_DEBUG") == "1" { - l.Notice("DEBUG " + msg) - } -} - -// Notice alerts the user about something, as well as putting it syslog -func (l *defaultLogger) Notice(msg string) { - l.buf = l.buf[:0] - now := time.Now().UTC() - l.buf = now.AppendFormat(l.buf, timestampFormat) - l.buf = append(l.buf, ' ') - l.buf = append(l.buf, l.prefix...) - l.buf = append(l.buf, msg...) - if len(msg) == 0 || msg[len(msg)-1] != '\n' { - l.buf = append(l.buf, '\n') - } - l.w.Write(l.buf) -} - -// New creates a log.Logger using the given io.Writer and prefix (which is -// printed between the timestamp and the message). -func New(w io.Writer, prefix string) Logger { - return &defaultLogger{w: w, prefix: prefix} -} diff --git a/tools/cli-client/internals/token/token.go b/tools/cli-client/internals/token/token.go deleted file mode 100644 index 37412251..00000000 --- a/tools/cli-client/internals/token/token.go +++ /dev/null @@ -1,50 +0,0 @@ -package token - -import ( - "fmt" - "os" - - "github.com/canonical/oci-factory/tools/cli-client/internals/logger" - "golang.org/x/term" -) - -const TokenVarName = "GITHUB_TOKEN" - -var _accessToken = "" - -func readAccessToken() string { - envAccessToken := os.Getenv(TokenVarName) - if len(envAccessToken) == 0 { - fmt.Print("GitHub personal access token: ") - accessTokenBytes, err := term.ReadPassword(0) - if err != nil { - // Not printing the err to prevent token from getting dumped - logger.Panicf("Error handling token") - } - // Put new logs into the new line - fmt.Println() - return string(accessTokenBytes) - } else { - fmt.Println("Using environment variable GITHUB_TOKEN") - return envAccessToken - } -} - -func GetAccessToken() string { - if len(_accessToken) == 0 { - _accessToken = readAccessToken() - } - return _accessToken -} - -func SetEnvToken(token string) (restore func()) { - saveToken := os.Getenv(TokenVarName) - os.Setenv(TokenVarName, token) - return func() { - if len(saveToken) == 0 { - os.Unsetenv(TokenVarName) - } else { - os.Setenv(TokenVarName, saveToken) - } - } -} diff --git a/tools/cli-client/internals/token/token_test.go b/tools/cli-client/internals/token/token_test.go deleted file mode 100644 index 5e15db13..00000000 --- a/tools/cli-client/internals/token/token_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package token_test - -import ( - "os" - "testing" - - "github.com/canonical/oci-factory/tools/cli-client/internals/token" -) - -func TestReadAccessTokenEnv(t *testing.T) { - expectedToken := "ghp_test123ToKeN" - originalToken := os.Getenv(token.TokenVarName) - restoreEnvToken := token.SetEnvToken(expectedToken) - err := os.Setenv(token.TokenVarName, expectedToken) - if err != nil { - t.Fatalf("Unable to set env variable: %v", err) - } - resultToken := token.GetAccessToken() - if resultToken != expectedToken { - t.Fatalf("") - } - restoreEnvToken() - if os.Getenv(token.TokenVarName) != originalToken { - t.Fatalf("Unable to restore saved env variable") - } -} diff --git a/tools/cli-client/internals/trigger/build_metadata.go b/tools/cli-client/internals/trigger/build_metadata.go deleted file mode 100644 index 377f0107..00000000 --- a/tools/cli-client/internals/trigger/build_metadata.go +++ /dev/null @@ -1,103 +0,0 @@ -package trigger - -import ( - "fmt" - "os" - "os/exec" - "regexp" - "strings" - - "github.com/canonical/oci-factory/tools/cli-client/internals/logger" - git "github.com/go-git/go-git/v5" - "gopkg.in/yaml.v3" -) - -type BuildMetadata struct { - Source string - Commit string - Directory string -} - -func InferBuildMetadata() BuildMetadata { - _, err := os.Stat("rockcraft.yaml") - if os.IsNotExist(err) { - fmt.Fprintln(os.Stderr, "No rockcraft.yaml found in current working directory") - os.Exit(1) - } else if err != nil { - logger.Panicf("OS error: %v", err) - } - repo, err := git.PlainOpenWithOptions(".", - &git.PlainOpenOptions{DetectDotGit: true, EnableDotGitCommonDir: false}) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to open repository: %v\n", err) - os.Exit(1) - } - - // find the source URL - remotes, err := repo.Remotes() - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to obtain remotes: %v\n", err) - os.Exit(1) - } - if len(remotes) < 1 || len(remotes[0].Config().URLs) < 1 { - fmt.Fprintf(os.Stderr, "No valid remote exists for this repo\n") - os.Exit(1) - } - remoteURL := remotes[0].Config().URLs[0] - logger.Debugf("Remote URL: %s", remoteURL) - - // use regex to match the repo location - regex := regexp.MustCompile(`github.com[:\/](canonical\/[A-Za-z0-9_-]*)(\.git)?`) - matches := regex.FindStringSubmatch(remoteURL) - if len(matches) < 3 { - fmt.Fprintf(os.Stderr, "oci-factory must be called in a git local repository belonging to the organization [canonical]\n") - os.Exit(1) - } - source := matches[1] - logger.Debugf("Source: %s", source) - - headSha256, err := repo.ResolveRevision("HEAD") - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to resolve HEAD: %v\n", err) - os.Exit(1) - } - logger.Debugf("HEAD SHA-256: %s", headSha256) - - // find the directory using git rev-parse - // have to use a subprocess since go-git - // does not support git rev-parse yet - prefixBytes, err := exec.Command("git", "rev-parse", "--show-prefix").Output() - if err != nil { - logger.Panicf("Subprocess `git rev-parse --show-prefix` failed: %v", err) - } - // remote the trailing newline - // also prepend a "./" to cope with empty prefixes - prefix := "./" + strings.TrimSpace(string(prefixBytes)) - logger.Debugf("Directory: %s", prefix) - - buildMetadata := BuildMetadata{ - Source: source, - Commit: headSha256.String(), - Directory: prefix, - } - - return buildMetadata -} - -func GetRockcraftImageName() string { - yamlFile, err := os.ReadFile("rockcraft.yaml") - if err != nil { - fmt.Fprint(os.Stderr, "Unable to read rockcraft.yaml in current working directory\n") - os.Exit(1) - } - y := struct { - Name string `yaml:"name"` - }{} - err = yaml.Unmarshal(yamlFile, &y) - if err != nil { - logger.Panicf("Unable to marshal trigger: %s", err) - } - - logger.Debugf("Image name: %s", y.Name) - return y.Name -} diff --git a/tools/cli-client/internals/trigger/build_metadata_test.go b/tools/cli-client/internals/trigger/build_metadata_test.go deleted file mode 100644 index 3285364c..00000000 --- a/tools/cli-client/internals/trigger/build_metadata_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package trigger_test - -import ( - "bytes" - "os" - "os/exec" - "path/filepath" - "strings" - - . "gopkg.in/check.v1" - - "github.com/canonical/oci-factory/tools/cli-client/internals/trigger" -) - -type BuildMetadataSuite struct { - dir string -} - -var _ = Suite(&BuildMetadataSuite{}) - -func (s *BuildMetadataSuite) SetUpTest(c *C) { - s.dir = c.MkDir() -} - -func (s *BuildMetadataSuite) TestGetRockcraftYamlName(c *C) { - f := filepath.Join(s.dir, "rockcraft.yaml") - err := os.WriteFile(f, []byte("name: cli-client-tester"), 0644) - c.Assert(err, IsNil) - err = os.Chdir(s.dir) - c.Assert(err, IsNil) - result := trigger.GetRockcraftImageName() - expected := "cli-client-tester" - c.Assert(result, Equals, expected) -} - -func (s *BuildMetadataSuite) TestGetBuildMetadataCustomDirectory(c *C) { - cmd := exec.Command("git", "--version") - if err := cmd.Run(); err != nil { - c.Fatal("git not installed") - } - repoPath := filepath.Join(s.dir, "tester-path") - cmd = exec.Command("git", "clone", "https://github.com/canonical/rocks-toolbox.git", repoPath) - var errBuf bytes.Buffer - cmd.Stderr = &errBuf - if err := cmd.Run(); err != nil { - c.Logf("stderr: %s", errBuf.String()) - c.Fatal(err) - } - - err := os.Chdir(filepath.Join(repoPath, "mock_rock", "1.0")) - c.Assert(err, IsNil) - - result := trigger.InferBuildMetadata() - - prefix := "./" + filepath.Join("mock_rock", "1.0") + "/" - head, err := exec.Command("git", "rev-parse", "HEAD").Output() - headStr := strings.TrimSpace(string(head)) - c.Assert(err, IsNil) - source := "canonical/rocks-toolbox" - expected := trigger.BuildMetadata{ - Source: source, - Directory: prefix, - Commit: headStr, - } - - c.Assert(result, DeepEquals, expected) -} diff --git a/tools/cli-client/internals/trigger/trigger.go b/tools/cli-client/internals/trigger/trigger.go deleted file mode 100644 index 28406dca..00000000 --- a/tools/cli-client/internals/trigger/trigger.go +++ /dev/null @@ -1,69 +0,0 @@ -package trigger - -import ( - "github.com/canonical/oci-factory/tools/cli-client/internals/logger" - "gopkg.in/yaml.v3" -) - -type UploadReleaseRisks struct { - Risks []string `yaml:"risks"` -} - -type UploadRelease struct { - EndOfLife string `yaml:"end-of-life"` - UploadReleaseRisks `yaml:",inline"` -} - -type UploadReleaseTrack map[string]UploadRelease - -type UploadImageTrigger struct { - Source string `yaml:"source"` - Commit string `yaml:"commit"` - Directory string `yaml:"directory"` - Release UploadReleaseTrack `yaml:"release,omitempty"` -} - -type UploadTrigger struct { - Version int `yaml:"version"` - UploadImageTriggers []UploadImageTrigger `yaml:"upload"` -} - -// All the arguments should be validated before passing to this function -func NewUploadReleaseTrack(track string, risks []string, eol string) UploadReleaseTrack { - newTrack := UploadReleaseTrack{ - track: { - EndOfLife: eol, - UploadReleaseRisks: UploadReleaseRisks{ - Risks: risks, - }, - }, - } - - return newTrack -} - -func NewUploadImageTrigger(buildMetadata BuildMetadata, tracks UploadReleaseTrack) UploadImageTrigger { - UploadImageTrigger := UploadImageTrigger{ - Source: buildMetadata.Source, - Commit: buildMetadata.Commit, - Directory: buildMetadata.Directory, - Release: tracks, - } - return UploadImageTrigger -} - -func NewUploadTrigger(imageTriggers []UploadImageTrigger) UploadTrigger { - trigger := UploadTrigger{ - Version: 1, - UploadImageTriggers: imageTriggers, - } - return trigger -} - -func (u *UploadTrigger) ToYamlString() string { - yamlData, err := yaml.Marshal(u) - if err != nil { - logger.Panicf("Unable to marshal trigger: %s", err) - } - return string(yamlData) -} diff --git a/tools/cli-client/internals/trigger/trigger_test.go b/tools/cli-client/internals/trigger/trigger_test.go deleted file mode 100644 index 56595818..00000000 --- a/tools/cli-client/internals/trigger/trigger_test.go +++ /dev/null @@ -1,81 +0,0 @@ -package trigger_test - -import ( - "testing" - - . "gopkg.in/check.v1" - - "github.com/canonical/oci-factory/tools/cli-client/internals/trigger" - "gopkg.in/yaml.v3" -) - -const track = "1.0-22.04" -const eol = "2025-05-01T00:00:00Z" - -type TriggerSuite struct{} - -func Test(t *testing.T) { TestingT(t) } - -var _ = Suite(&TriggerSuite{}) - -var buildMetadata = trigger.BuildMetadata{ - Source: "canonical/oci-factory", - Commit: "f0250895d1758cdab6619122a4fd67dbbde3004a", - Directory: "examples/mock-rock/1.0/", -} -var risks = []string{"candidate", "stable"} - -func (s *TriggerSuite) TestNewUploadReleaseTrack(c *C) { - result := trigger.NewUploadReleaseTrack(track, risks, eol) - expectedYaml := `1.0-22.04: - end-of-life: "2025-05-01T00:00:00Z" - risks: - - candidate - - stable -` - resultBytes, err := yaml.Marshal(result) - c.Assert(err, IsNil) - resultYaml := string(resultBytes) - - c.Assert(resultYaml, Equals, expectedYaml) -} - -func (s *TriggerSuite) TestNewUploadImageTrigger(c *C) { - result := trigger.NewUploadImageTrigger(buildMetadata, trigger.NewUploadReleaseTrack(track, risks, eol)) - expectedYaml := `source: canonical/oci-factory -commit: f0250895d1758cdab6619122a4fd67dbbde3004a -directory: examples/mock-rock/1.0/ -release: - 1.0-22.04: - end-of-life: "2025-05-01T00:00:00Z" - risks: - - candidate - - stable -` - resultBytes, err := yaml.Marshal(result) - c.Assert(err, IsNil) - resultYaml := string(resultBytes) - - c.Assert(resultYaml, Equals, expectedYaml) -} - -func (s *TriggerSuite) TestNewUploadTrigger(c *C) { - result := trigger.NewUploadTrigger([]trigger.UploadImageTrigger{ - trigger.NewUploadImageTrigger(buildMetadata, trigger.NewUploadReleaseTrack(track, risks, eol)), - }, - ) - resultYaml := result.ToYamlString() - expectedYaml := `version: 1 -upload: - - source: canonical/oci-factory - commit: f0250895d1758cdab6619122a4fd67dbbde3004a - directory: examples/mock-rock/1.0/ - release: - 1.0-22.04: - end-of-life: "2025-05-01T00:00:00Z" - risks: - - candidate - - stable -` - c.Assert(resultYaml, Equals, expectedYaml) -} diff --git a/tools/cli-client/snap/snapcraft.yaml b/tools/cli-client/snap/snapcraft.yaml deleted file mode 100644 index a41c8968..00000000 --- a/tools/cli-client/snap/snapcraft.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: oci-factory # TODO 'snapcraft register ' -base: core22 -version: '0.0.1' -summary: The OCI Factory CLI client to build, upload and release OCI images -description: | - The OCI Factory CLI client is a tool that builds, tests, and releases - the OCI images owned by Canonical using the Github workflow in - the OCI Factory repository. -grade: devel # must be 'stable' to release into candidate/stable channels -confinement: strict - -parts: - oci-factory: - plugin: go - build-snaps: [go/latest/stable] - source: . - stage-packages: - - git - -apps: - oci-factory: - command: bin/oci-factory - plugs: - - home - - network diff --git a/tools/junit_to_markdown/__init__.py b/tools/junit_to_markdown/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tools/junit_to_markdown/__main__.py b/tools/junit_to_markdown/__main__.py deleted file mode 100644 index 2d55304e..00000000 --- a/tools/junit_to_markdown/__main__.py +++ /dev/null @@ -1,25 +0,0 @@ -import argparse, sys -import xml.etree.ElementTree as ET -from .convert import print_junit_report - - -parser = argparse.ArgumentParser( - description="Generate markdown from a JUnit XML report for $GITHUB_STEP_SUMMARY" -) - -parser.add_argument( - "--input-junit", help="Path to JUnit XML Report", required=True, type=str -) - - -def main(): - args = parser.parse_args() - - tree = ET.parse(args.input_junit) - root = tree.getroot() - - print_junit_report(root, sys.stdout) - - -if __name__ == "__main__": - main() diff --git a/tools/junit_to_markdown/convert.py b/tools/junit_to_markdown/convert.py deleted file mode 100755 index 003281cd..00000000 --- a/tools/junit_to_markdown/convert.py +++ /dev/null @@ -1,145 +0,0 @@ -#! /bin/env python3 -import xml.etree.ElementTree as ET -from io import TextIOBase -import json - -DEFAULT_STATUS_ICON = ":white_check_mark:" -STATUS_ICONS = { - "failure": ":x:", - "error": ":warning:", - "skipped": ":information_source:", - "information_source": ":x:", -} - - -def print_element(element: ET.Element, output: TextIOBase = None): - """Generically display attrs and text of a element""" - print(f"
", file=output)
-
-    for key, value in element.attrib.items():
-        print(f"{key}: {value}", file=output)
-
-    if element.text is not None:
-        if content := element.text.strip():
-            print(f"text: \n{content}", file=output)
-
-    print(f"
", file=output) - - -def get_chart_data(testsuite: ET.Element): - """Extract and order data used in pie chart""" - - failed_tests = int(testsuite.attrib.get("failures", 0)) - error_tests = int(testsuite.attrib.get("errors", 0)) - skipped_tests = int(testsuite.attrib.get("skipped", 0)) - total_tests = int(testsuite.attrib.get("tests", 0)) - - # passed test has to be inferred - pass_tests = total_tests - failed_tests - error_tests - skipped_tests - - # disable black autoformatter for a moment - # fmt: off - - # name, value, colour, default_order - chart_data = [ - ("failed", failed_tests, "#f00", 1), - ("error", error_tests, "#fa0", 2), - ("skipped", skipped_tests, "#ff0", 3), - ("pass", pass_tests, "#0f0", 4), - ] - # note: default_order ensures color match if two wedges have the exact same value - # fmt: on - - # filter out wedges with 0 width - chart_data = list(filter(lambda w: w[1] != 0, chart_data)) - - # sort by value, then default order so colors match what we expect - chart_data = list(sorted(chart_data, key=lambda w: (w[1], w[3]), reverse=True)) - - return chart_data - - -def print_testsuite_pie_chart(testsuite: ET.Element, output: TextIOBase = None): - """Generate a pie chart showing test status from testsuite element""" - - chart_data = get_chart_data(testsuite) - - # create the chart theme - theme_dict = { - "theme": "base", - "themeVariables": {f"pie{n+1}": w[2] for n, w in enumerate(chart_data)}, - } - - # begin printing pie chart... - print("```mermaid", file=output) - - # theme colors in order: pass, failed, error, skipped - # Note: init cannot be in quotes - print(f"%%{{init:{json.dumps(theme_dict)}}}%%", file=output) - - print(f"pie", file=output) - for key, value, _, _ in chart_data: - print(f'"{key}" : {value}', file=output) - - print("```", file=output) - - -def get_testcase_status(testcase: ET.Element): - """Get status for individual testcase elements""" - - for key, value in STATUS_ICONS.items(): - if testcase.find(key) is not None: - return value - - return DEFAULT_STATUS_ICON - - -def print_header(testsuite: ET.Element, output: TextIOBase = None): - """Print a header for the summary""" - passed = ( - testsuite.attrib.get("failures") == "0" - and testsuite.attrib.get("errors") == "0" - ) - status = ":white_check_mark:" if passed else ":x:" - name = testsuite.attrib["name"] - - print(f"# {status} {name}", file=output) - - -def print_testsuite_report(testsuite: ET.Element, output: TextIOBase = None): - """Print complete testsuite element Report""" - - print_header(testsuite, output) - - # use pie chart header as title - print_testsuite_pie_chart(testsuite, output) - - # print testsuite info - print_element(testsuite, output) - - # print each test case in collapsable section - for testcase in testsuite.findall("testcase"): - - print("
", file=output) - - test_status = get_testcase_status(testcase) - test_name = ( - testcase.attrib["name"].replace("_", " ").title() - ) # make the title look better - test_class = testcase.attrib["classname"] - print( - f"{test_status} {test_name} - {test_class}", file=output - ) - - for child in testcase.iter(): - print(f"{child.tag}", file=output) - print_element(child, output) - - print("
", file=output) - - -def print_junit_report(root: ET.Element, output: TextIOBase = None): - """Print report by iterating over all elements in root""" - - for testsuite in root.findall("testsuite"): - print_testsuite_report(testsuite, output) diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/__init__.py b/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/__init__.py b/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/ca.crt b/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/ca.crt deleted file mode 100644 index dc57727f..00000000 --- a/tools/workflow-engine/charms/temporal-worker/oci_factory/activities/consumer/ca.crt +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFLTCCAxWgAwIBAgIUPw02SFy/2CDfZBZmYVN1S4fIK3IwDQYJKoZIhvcNAQEN -BQAwLTETMBEGA1UECgwKaW8uc3RyaW16aTEWMBQGA1UEAwwNY2x1c3Rlci1jYSB2 -MDAeFw0yNDA3MTUxMjUxMjdaFw0yNTA3MTUxMjUxMjdaMC0xEzARBgNVBAoMCmlv -LnN0cmltemkxFjAUBgNVBAMMDWNsdXN0ZXItY2EgdjAwggIiMA0GCSqGSIb3DQEB -AQUAA4ICDwAwggIKAoICAQDqke5PeB5bcZKCgzop2F6NEjavs60Wg/7QGak6IXXA -zrhw/M4Sxw9u3MydEd1yDpp4q38xH8QRBmzTublWKdXTVcxQiaXPeK6M3nUXOY36 -a/TWOH4tDFu/HtCyh9GMMrIPX5aORDnUhAndo/9ADn6chMB/VujsjTrRAcpIhR8Q -mDyhJ6vGlJT344fnPBAP9zrXHSm4myMK1D4iJsHvcowhO3rGFsF7uWQKH5zz4hUB -xHXW6E6XElta3KNLRQRoyLVTX3O8ZMRQsOCh2Dmo8UnZ2LPBmKQMiwvktm6Orcmf -ChM00eGZoassHG/ojQww6tMLkuLCcefegz66iVpvH83mYycrsNJ2zcgtoMGZSkVZ -ZLO57hNaCY4zWYeyyQXqVLtETnkKZe8okvHNjkvToPDhpFDA0Mki/QTNQ8js7BvS -oLBqPVcT09vpyUcO5Xv4pi5BVFwFO3PsOe8jJ8QnJqgFsNuWfc0ZmReBgqy2QBOY -34c2XRJyl19th9Fk1ruZfLCKj9UKBTCTIp+M/59JQ48DUWZewYIVZIKEWcuCGsa6 -fnjh/dR34BFfJmcx95w0BUomq38OL5WYAP+M/BQax1ws0U2kTI1e1u8jlTUyA9xi -m3t0TuIkKTCasIxC90Vn9xbepnChwNpGuI7vZySaisgciAIkrhQDoHKLWFtj04sV -CwIDAQABo0UwQzAdBgNVHQ4EFgQUJdKnCS58yN0bWek2+dv3FjiFI4cwEgYDVR0T -AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQADggIB -AFCD8EcTzpzD/AyH42dl6o0nBRiFNUPNRueuIu6RUhBpyj1xKyqbIseH4aDDMwrI -InUKiYcXrj9KL72oSdjXIUYYb+GuI7xTNem5I2qRhSWLBiOObPX+Z10+dJyc49Hl -QPJIxAGPZhoTSYaDuuAe7piFYIEbG5iCEQ5BiQysYYZlPzA5KvofVGsVTcpSFtMz -+SXsUYi392hk8RMHNvfqlQr++el5wzKi5PRmgX3q0x1mRMQrQsMXDIkNZV1uxUJ0 -zdwwisAubjzRZnvpnrwqLnXtWLsPV5nn7W58VaEqLOI2RVQSd4zlHS2ALb6jcI1v -VByLu36lG8zwwNDiSlTQtyzBZV8kZ5kyzmWRDmPvhDUfvv1XgCS52tjVDoyBNq2U -E5azHu7RUD6fqweT7JufOWyRVor2gxccoLvNCDt8X5giAUHBDLYoo4qOKosCrEQU -sVakBF6vRQPG9ftgPPCH9aEPLnD/xSgomJJXRYzVBjPbH+F8DDy/UtV9MUyEihKI -piP8g+ctKVefOCMwWkQuMZFlu4jPewz6+F+NMrkxoTqDlT2S4rCh0PphtmzVptJB -+CmOcoXhFtmiKNxhFF/Zy4SntW+f3lLAzq62gc8LiBAbxBhWRK2TwbR0Upr7vvji -9k5rAQ+CYQJf7G/Il8UMWwRXaQeCqPAGg8NT3oZAKZnc ------END CERTIFICATE----- diff --git a/tools/workflow-engine/charms/temporal-worker/oci_factory/notification/mattermost_notifier.py b/tools/workflow-engine/charms/temporal-worker/oci_factory/notification/mattermost_notifier.py deleted file mode 100644 index 4b7c0470..00000000 --- a/tools/workflow-engine/charms/temporal-worker/oci_factory/notification/mattermost_notifier.py +++ /dev/null @@ -1,96 +0,0 @@ -import json -import logging -import os -import sys -from typing import Optional - -import requests - -logging.basicConfig(level=os.environ.get("TWC_LOG_LEVEL", "info").upper()) - -COLORS = { - "success": "#33CC33", - "failure": "#CC0000", - "unknown": "#B0B0B0", -} - -REQUIRED_ENVS = ["MATTERMOST_CHANNEL_ID", "MATTERMOST_TOKEN", "MATTERMOST_SERVER"] - -missing_envs = [env for env in REQUIRED_ENVS if os.environ.get(env) is None] -if missing_envs: - logging.error( - f"Unable to notify on Mattermost, missing environment variables: {missing_envs}" - ) - sys.exit(1) - -MATTERMOST_CHANNEL_ID = os.environ.get("MATTERMOST_CHANNEL_ID") -MATTERMOST_TOKEN = os.environ.get("MATTERMOST_TOKEN") -MATTERMOST_SERVER = os.environ.get("MATTERMOST_SERVER") - -POST_URL = f"{MATTERMOST_SERVER}/api/v4/posts" -PATCH_URL = POST_URL + "/{}/patch" - -HEADERS = { - "Authorization": f"Bearer {MATTERMOST_TOKEN}", - "Content-Type": "application/json", -} - - -def send_message(title: str, message: str) -> str: - """Send a message to a Mattermost channel, - returning the message ID. Default status is unknown. - - :param title: the message title - :param message: the message body - """ - payload = { - "channel_id": MATTERMOST_CHANNEL_ID, - "props": { - "attachments": [ - { - "fallback": message, - "title": title, - "text": message, - "color": COLORS["unknown"], - } - ] - }, - } - - data = json.dumps(payload).encode("utf-8") - res = requests.post(POST_URL, headers=HEADERS, data=data, timeout=10) - res.raise_for_status() - return res.json()["id"] - - -def update_status_and_message( - message_id: str, success: bool, message: Optional[str] = None -) -> None: - """Update the status and the message of a post on Mattermost. - - :param message_id: the ID of the post to be updated - :param success: the new status of the post - :param message: the new message body - """ - post_res = requests.get( - f"{POST_URL}/{message_id}", - headers=HEADERS, - timeout=10, - ) - post_res.raise_for_status() - props = post_res.json()["props"] - - props["attachments"][0]["color"] = ( - COLORS["success"] if success else COLORS["failure"] - ) - - if message: - props["attachments"][0]["text"] = message - - payload = {"id": message_id, "props": props} - - data = json.dumps(payload).encode("utf-8") - res = requests.put( - PATCH_URL.format(message_id), headers=HEADERS, data=data, timeout=10 - ) - res.raise_for_status()