diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3c0f19ab..353bf4b9 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -66,7 +66,7 @@ runtime in order to build packer with the Vagrant plugin. 1. This project always releases from the latest version of golang. [Install go](https://golang.org/doc/install#install) To properly build from -source, you need to have golang >= 1.20 +source, you need to have golang >= 1.21 ## Setting up Vagrant plugin for dev diff --git a/.github/actions/build-and-persist-plugin-binary/action.yml b/.github/actions/build-and-persist-plugin-binary/action.yml index 56e26403..e1e6a7c0 100644 --- a/.github/actions/build-and-persist-plugin-binary/action.yml +++ b/.github/actions/build-and-persist-plugin-binary/action.yml @@ -10,14 +10,14 @@ inputs: runs: using: composite steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - run: "GOOS=${{ inputs.GOOS }} GOARCH=${{ inputs.GOARCH }} go build -o ./pkg/packer_plugin_vagrant_${{ inputs.GOOS }}_${{ inputs.GOARCH }} ." shell: bash - run: zip ./pkg/packer_plugin_vagrant_${{ inputs.GOOS }}_${{ inputs.GOARCH }}.zip ./pkg/packer_plugin_vagrant_${{ inputs.GOOS }}_${{ inputs.GOARCH }} shell: bash - run: rm ./pkg/packer_plugin_vagrant_${{ inputs.GOOS }}_${{ inputs.GOARCH }} shell: bash - - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 with: name: "packer_plugin_vagrant_${{ inputs.GOOS }}_${{ inputs.GOARCH }}.zip" path: "pkg/packer_plugin_vagrant_${{ inputs.GOOS }}_${{ inputs.GOARCH }}.zip" diff --git a/.github/workflows/build_plugin_binaries.yml b/.github/workflows/build_plugin_binaries.yml index 76ccd0e3..d4108cfc 100644 --- a/.github/workflows/build_plugin_binaries.yml +++ b/.github/workflows/build_plugin_binaries.yml @@ -15,9 +15,9 @@ jobs: working-directory: ~/go/src/github.com/hashicorp/packer-plugin-vagrant runs-on: ubuntu-latest container: - image: docker.mirror.hashicorp.services/cimg/go:1.20 + image: docker.mirror.hashicorp.services/cimg/go:1.21 steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - uses: "./.github/actions/build-and-persist-plugin-binary" with: GOOS: darwin @@ -32,9 +32,9 @@ jobs: working-directory: ~/go/src/github.com/hashicorp/packer-plugin-vagrant runs-on: ubuntu-latest container: - image: docker.mirror.hashicorp.services/cimg/go:1.20 + image: docker.mirror.hashicorp.services/cimg/go:1.21 steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - uses: "./.github/actions/build-and-persist-plugin-binary" with: GOOS: freebsd @@ -53,9 +53,9 @@ jobs: working-directory: ~/go/src/github.com/hashicorp/packer-plugin-vagrant runs-on: ubuntu-latest container: - image: docker.mirror.hashicorp.services/cimg/go:1.20 + image: docker.mirror.hashicorp.services/cimg/go:1.21 steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - uses: "./.github/actions/build-and-persist-plugin-binary" with: GOOS: linux @@ -78,9 +78,9 @@ jobs: working-directory: ~/go/src/github.com/hashicorp/packer-plugin-vagrant runs-on: ubuntu-latest container: - image: docker.mirror.hashicorp.services/cimg/go:1.20 + image: docker.mirror.hashicorp.services/cimg/go:1.21 steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - uses: "./.github/actions/build-and-persist-plugin-binary" with: GOOS: netbsd @@ -99,9 +99,9 @@ jobs: working-directory: ~/go/src/github.com/hashicorp/packer-plugin-vagrant runs-on: ubuntu-latest container: - image: docker.mirror.hashicorp.services/cimg/go:1.20 + image: docker.mirror.hashicorp.services/cimg/go:1.21 steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - uses: "./.github/actions/build-and-persist-plugin-binary" with: GOOS: openbsd @@ -120,9 +120,9 @@ jobs: working-directory: ~/go/src/github.com/hashicorp/packer-plugin-vagrant runs-on: ubuntu-latest container: - image: docker.mirror.hashicorp.services/cimg/go:1.20 + image: docker.mirror.hashicorp.services/cimg/go:1.21 steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - uses: "./.github/actions/build-and-persist-plugin-binary" with: GOOS: solaris @@ -133,9 +133,9 @@ jobs: working-directory: ~/go/src/github.com/hashicorp/packer-plugin-vagrant runs-on: ubuntu-latest container: - image: docker.mirror.hashicorp.services/cimg/go:1.20 + image: docker.mirror.hashicorp.services/cimg/go:1.21 steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - uses: "./.github/actions/build-and-persist-plugin-binary" with: GOOS: windows diff --git a/.github/workflows/go-test-darwin.yml b/.github/workflows/go-test-darwin.yml index 9b9f601c..ee4bd092 100644 --- a/.github/workflows/go-test-darwin.yml +++ b/.github/workflows/go-test-darwin.yml @@ -23,7 +23,7 @@ jobs: outputs: go-version: ${{ steps.get-go-version.outputs.go-version }} steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: 'Determine Go version' id: get-go-version run: | @@ -35,8 +35,8 @@ jobs: runs-on: macos-latest name: Darwin Go tests steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: ${{ needs.get-go-version.outputs.go-version }} - run: | diff --git a/.github/workflows/go-test-linux.yml b/.github/workflows/go-test-linux.yml index 092890c1..623b6519 100644 --- a/.github/workflows/go-test-linux.yml +++ b/.github/workflows/go-test-linux.yml @@ -23,7 +23,7 @@ jobs: outputs: go-version: ${{ steps.get-go-version.outputs.go-version }} steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: 'Determine Go version' id: get-go-version run: | @@ -35,8 +35,8 @@ jobs: runs-on: ubuntu-latest name: Linux Go tests steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: ${{ needs.get-go-version.outputs.go-version }} - run: | diff --git a/.github/workflows/go-test-windows.yml b/.github/workflows/go-test-windows.yml index 63033e60..6b6482d2 100644 --- a/.github/workflows/go-test-windows.yml +++ b/.github/workflows/go-test-windows.yml @@ -23,7 +23,7 @@ jobs: outputs: go-version: ${{ steps.get-go-version.outputs.go-version }} steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: 'Determine Go version' id: get-go-version run: | @@ -35,8 +35,8 @@ jobs: runs-on: windows-latest name: Windows Go tests steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: ${{ needs.get-go-version.outputs.go-version }} - run: | diff --git a/.github/workflows/go-validate.yml b/.github/workflows/go-validate.yml index 7765a187..7e67a99b 100644 --- a/.github/workflows/go-validate.yml +++ b/.github/workflows/go-validate.yml @@ -22,7 +22,7 @@ jobs: outputs: go-version: ${{ steps.get-go-version.outputs.go-version }} steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: 'Determine Go version' id: get-go-version run: | @@ -34,8 +34,8 @@ jobs: runs-on: ubuntu-latest name: Go Mod Tidy steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: ${{ needs.get-go-version.outputs.go-version }} - run: go mod tidy @@ -45,13 +45,13 @@ jobs: runs-on: ubuntu-latest name: Lint check steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: ${{ needs.get-go-version.outputs.go-version }} - - uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299 # v3.6.0 + - uses: golangci/golangci-lint-action@82d40c283aeb1f2b6595839195e95c2d6a49081b # v5.0.0 with: - version: v1.53.3 + version: v1.54.2 only-new-issues: true check-fmt: needs: @@ -59,8 +59,8 @@ jobs: runs-on: ubuntu-latest name: Gofmt check steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: ${{ needs.get-go-version.outputs.go-version }} - run: | @@ -77,13 +77,20 @@ jobs: runs-on: ubuntu-latest name: Generate check steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: ${{ needs.get-go-version.outputs.go-version }} - run: | export PATH=$PATH:$(go env GOPATH)/bin make generate - git diff --exit-code || ( echo "Found diffs in generated code" \ - && echo "You can use the command: \`make generate\` to reformat code." \ - && false ) + uncommitted="$(git status -s)" + if [[ -z "$uncommitted" ]]; then + echo "OK" + else + echo "Docs have been updated, but the compiled docs have not been committed." + echo "Run 'make generate', and commit the result to resolve this error." + echo "Generated but uncommitted files:" + echo "$uncommitted" + exit 1 + fi diff --git a/.github/workflows/notify-integration-release-via-manual.yaml b/.github/workflows/notify-integration-release-via-manual.yaml index 11b8c63a..ba510a96 100644 --- a/.github/workflows/notify-integration-release-via-manual.yaml +++ b/.github/workflows/notify-integration-release-via-manual.yaml @@ -11,29 +11,45 @@ on: default: 'main' required: false jobs: + strip-version: + runs-on: ubuntu-latest + outputs: + packer-version: ${{ steps.strip.outputs.packer-version }} + steps: + - name: Strip leading v from version tag + id: strip + env: + REF: ${{ github.event.inputs.version }} + run: | + echo "packer-version=$(echo "$REF" | sed -E 's/v?([0-9]+\.[0-9]+\.[0-9]+)/\1/')" >> "$GITHUB_OUTPUT" notify-release: + needs: + - strip-version runs-on: ubuntu-latest steps: - name: Checkout this repo - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: ref: ${{ github.event.inputs.branch }} # Ensure that Docs are Compiled - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 - shell: bash run: make generate - shell: bash run: | - if [[ -z "$(git status -s)" ]]; then + uncommitted="$(git status -s)" + if [[ -z "$uncommitted" ]]; then echo "OK" else echo "Docs have been updated, but the compiled docs have not been committed." echo "Run 'make generate', and commit the result to resolve this error." + echo "Generated but uncommitted files:" + echo "$uncommitted" exit 1 fi # Perform the Release - name: Checkout integration-release-action - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: repository: hashicorp/integration-release-action path: ./integration-release-action @@ -41,6 +57,6 @@ jobs: uses: ./integration-release-action with: integration_identifier: "packer/hashicorp/vagrant" - release_version: ${{ github.event.inputs.version }} + release_version: ${{ needs.strip-version.outputs.packer-version }} release_sha: ${{ github.event.inputs.branch }} github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/notify-integration-release-via-tag.yaml b/.github/workflows/notify-integration-release-via-tag.yaml index f17adbb5..b3461b0d 100644 --- a/.github/workflows/notify-integration-release-via-tag.yaml +++ b/.github/workflows/notify-integration-release-via-tag.yaml @@ -21,25 +21,28 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout this repo - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: ref: ${{ github.ref }} # Ensure that Docs are Compiled - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 - shell: bash run: make generate - shell: bash run: | - if [[ -z "$(git status -s)" ]]; then + uncommitted="$(git status -s)" + if [[ -z "$uncommitted" ]]; then echo "OK" else echo "Docs have been updated, but the compiled docs have not been committed." echo "Run 'make generate', and commit the result to resolve this error." + echo "Generated but uncommitted files:" + echo "$uncommitted" exit 1 fi # Perform the Release - name: Checkout integration-release-action - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: repository: hashicorp/integration-release-action path: ./integration-release-action diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ec531d5..ec35d84b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: outputs: go-version: ${{ steps.get-go-version.outputs.go-version }} steps: - - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: 'Determine Go version' id: get-go-version run: | @@ -38,11 +38,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Unshallow run: git fetch --prune --unshallow - name: Set up Go - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: go-version: ${{ needs.get-go-version.outputs.go-version }} - name: Describe plugin @@ -51,7 +51,7 @@ jobs: - name: Install signore uses: hashicorp/setup-signore-package@v1 - name: Run GoReleaser - uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b # v4.2.0 + uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 with: version: latest args: release --clean --timeout 120m diff --git a/.go-version b/.go-version index f57ebca3..db98d22c 100644 --- a/.go-version +++ b/.go-version @@ -1,2 +1,2 @@ -1.20.11 +1.21.8 diff --git a/.goreleaser.yml b/.goreleaser.yml index f9dbc171..637013fa 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,6 +3,7 @@ # This is an example goreleaser.yaml file with some defaults. # Make sure to check the documentation at http://goreleaser.com +version: 2 env: - CGO_ENABLED=0 before: @@ -12,6 +13,8 @@ before: - go test ./... # Check plugin compatibility with required version of the Packer SDK - make plugin-check + # Copy LICENSE file for inclusion in zip archive + - cp LICENSE LICENSE.txt builds: # A separated build to run the packer-plugins-check only once for a linux_amd64 binary - @@ -85,7 +88,8 @@ builds: archives: - format: zip files: - - none* + - "LICENSE.txt" + name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.API_VERSION }}_{{ .Os }}_{{ .Arch }}' checksum: name_template: '{{ .ProjectName }}_v{{ .Version }}_SHA256SUMS' diff --git a/.web-docs/components/builder/vagrant/README.md b/.web-docs/components/builder/vagrant/README.md index d1dc9c5a..f9c7ea20 100644 --- a/.web-docs/components/builder/vagrant/README.md +++ b/.web-docs/components/builder/vagrant/README.md @@ -90,15 +90,13 @@ the Compress post-processor will not work with this builder. This parameter is required when source_path have more than one provider, or when using vagrant-cloud post-processor. Defaults to unset. -- `vagrantfile_template` (string) - What vagrantfile to use - - `teardown_method` (string) - Whether to halt, suspend, or destroy the box when the build has completed. Defaults to "halt" - `box_version` (string) - What box version to use when initializing Vagrant. - `template` (string) - a path to a golang template for a vagrantfile. Our default template can - be found here. The template variables available to you are + be found [here](https://github.com/hashicorp/packer-plugin-vagrant/blob/main/builder/vagrant/step_create_vagrantfile.go#L39-L54). The template variables available to you are `{{ .BoxName }}`, `{{ .SyncedFolder }}`, and `{{.InsertKey}}`, which correspond to the Packer options box_name, synced_folder, and insert_key. Alternatively, the template variable `{{.DefaultTemplate}}` is available for diff --git a/.web-docs/components/post-processor/vagrant-registry/README.md b/.web-docs/components/post-processor/vagrant-registry/README.md new file mode 100644 index 00000000..e970d80d --- /dev/null +++ b/.web-docs/components/post-processor/vagrant-registry/README.md @@ -0,0 +1,282 @@ +Type: `vagrant-registry` +Artifact BuilderId: `hashicorp.post-processor.vagrant-registry` + +[HCP Vagrant Box Registry](https://portal.cloud.hashicorp.com/vagrant/discover) +hosts and serves boxes to Vagrant, allowing you to version and distribute boxes +to an organization or the public in a simple way. + +The Vagrant Registry post-processor enables the upload of Vagrant boxes to HCP +Vagrant Box Registry. Currently, the Vagrant Registry post-processor will accept +and upload boxes supplied to it from the [Vagrant](/docs/post-processor/vagrant.mdx) or +[Artifice](https://developer.hashicorp.com/packer/docs/post-processor/artifice) post-processors and the +[Vagrant](/docs/builder/vagrant.mdx) builder. + +## Workflow + +It's important to understand the workflow that using this post-processor +enforces in order to take full advantage of Vagrant and HCP Vagrant Box Registry. + +The use of this processor assume that you currently distribute, or plan to +distribute, boxes via HCP Vagrant Box Registry. It also assumes you create +Vagrant Boxes and deliver them to your team in some fashion. + +Here is an example workflow: + +1. You use Packer to build a Vagrant Box for the `virtualbox` provider +1. The `vagrant-registry` post-processor is configured to point to the box + `hashicorp/foobar` on HCP Vagrant Box Registry via the `box_tag` configuration +1. The post-processor receives the box from the `vagrant` post-processor +1. It then creates the box name, or verifies the existence of it, on HCP + Vagrant Box Registry +1. It then creates the configured version, or verifies the existence of it +1. It then creates the configured provider, or verifies the existence of it +1. It then creates the configured architecture, or verifies the existence of it +1. The box artifact is uploaded to HCP Vagrant Box Registry +1. The upload is verified +1. The version is released and available to users of the box + +## Configuration + +The configuration allows you to specify the target box that you have access to +on HCP Vagrant Box Registry, as well as authentication and version information. + +### Required + +- `box_tag` (string) - The shorthand tag for your box that maps to the box registry + name and the box name. For example, `hashicorp/precise64` is the shorthand tag + for the `precise64` box in the `hashicorp` box registry. + +- `version` (string) - The version number, typically incrementing a previous + version. The version string is validated based on [Semantic + Versioning](http://semver.org/). The string must match a pattern that could + be semver, and doesn't validate that the version comes after your previous + versions. + +- `client_id` (string) - The service principal client ID for the HCP API. This + value can be omitted if the `HCP_CLIENT_ID` environment variable is set. See + the [HCP documentation](https://developer.hashicorp.com/hcp/docs/hcp/admin/iam/service-principals) + for creating a service principal. + +- `client_secret` (string) - The service principal client secret for the HCP API. This + value can be omitted if the `HCP_CLIENT_SECRET` environment variable is set. See + the [HCP documentation](https://developer.hashicorp.com/hcp/docs/hcp/admin/iam/service-principals) + for creating a service principal. + +### Optional +- `architecture` (string) - The architecture of the Vagrant box. This will be + detected from the box if possible by default. Supported values: amd64, i386, + arm, arm64, ppc64le, ppc64, mips64le, mips64, mipsle, mips, and s390x. + +- `default_architecture` (string) - The architecture that should be flagged as + the default architecture for this provider. + +- `no_release` (boolean) - If set to true, does not release the version on + HCP Vagrant Box Registry, making it active. You can manually release the version via + the API or Web UI. Defaults to `false`. + +- `keep_input_artifact` (boolean) - When true, preserve the local box + after uploading to HCP Vagrant Box Registry. Defaults to `true`. + +- `version_description` (string) - Optional Markdown text used as a + full-length and in-depth description of the version, typically for denoting + changes introduced + +- `box_download_url` (string) - Optional URL for a self-hosted box. + If this is set the box will not be uploaded to HCP Vagrant Box Registry. + This is a [template engine](https://developer.hashicorp.com/packer/docs/templates/legacy_json_templates/engine). + Therefore, you may use user variables and template functions in this field. + The following extra variables are also available in this engine: + + - `Architecture`: The architecture of the Vagrant box + - `Provider`: The Vagrant provider the box is for + - `ArtifactId`: The ID of the input artifact. + +- `box_checksum` (string) - Optional checksum for the provider .box file. + The type of the checksum is specified within the checksum field as a prefix, + ex: "md5:{$checksum}". Valid values are: + - null or "" + - "md5:{$checksum}" + - "sha1:{$checksum}" + - "sha256:{$checksum}" + - "sha512:{$checksum}" + +- `no_direct_upload` (boolean) - When `true`, upload the box artifact through + HCP Vagrant Box Registry instead of directly to the backend storage. + +## Use with the Vagrant Post-Processor + +An example configuration is shown below. Note the use of the [post-processors](https://developer.hashicorp.com/packer/docs/templates/hcl_templates/blocks/build/post-processors) +block that wraps both the Vagrant and Vagrant Registry [post-processor](https://developer.hashicorp.com/packer/docs/templates/hcl_templates/blocks/build/post-processor) blocks within the post-processor section. Chaining +the post-processors together in this way tells Packer that the artifact +produced by the Vagrant post-processor should be passed directly to the Vagrant +Registry Post-Processor. It also sets the order in which the post-processors +should run. + +Failure to chain the post-processors together in this way will result in the +wrong artifact being supplied to the Vagrant Registry post-processor. This will +likely cause the Vagrant Registry post-processor to error and fail. + +**JSON** + +```json +{ + "variables": { + "hcp_client_id": "{{ env `HCP_CLIENT_ID` }}", + "hcp_client_secret": "{{ env `HCP_CLIENT_SECRET` }}" + "version": "1.0.{{timestamp}}" + "architecture": "amd64", + }, + "post-processors": [ + { + "type": "shell-local", + "inline": ["echo Doing stuff..."] + }, + [ + { + "type": "vagrant", + "include": ["image.iso"], + "vagrantfile_template": "vagrantfile.tpl", + "output": "proxycore_{{.Provider}}_{{.Architecture}}_.box" + }, + { + "type": "vagrant-registry", + "box_tag": "hashicorp/precise64", + "client_id": "{{user `hcp_client_id`}}", + "client_secret": "{{user `hcp_client_secret`}}", + "version": "{{user `version`}}", + "architecture": "{{user `architecture`}}" + } + ] + ] +} +``` + +**HCL2** + +```hcl +variable "hcp_client_id" { + type = string + default = "${env("HCP_CLIENT_ID")}" +} + +variable "hcp_client_secret" { + type = string + default = "${env("HCP_CLIENT_SECRET")}" +} + +build { + sources = ["source.null.autogenerated_1"] + + post-processor "shell-local" { + inline = ["echo Doing stuff..."] + } + post-processors { + post-processor "vagrant" { + include = ["image.iso"] + output = "proxycore_{{.Provider}}_{{.Architecture}}_.box" + vagrantfile_template = "vagrantfile.tpl" + } + post-processor "vagrant-registry" { + client_id = "${var.hcp_client_id}" + client_secret = "${var.hcp_client_secret}" + box_tag = "hashicorp/precise64" + version = "${local.version}" + architecture = "${local.architecture}" + } + } +} +``` + +## Use with the Artifice Post-Processor + +An example configuration is shown below. Note the use of the nested array that +wraps both the Artifice and Vagrant Registry post-processors within the +post-processor section. Chaining the post-processors together in this way tells +Packer that the artifact produced by the Artifice post-processor should be +passed directly to the Vagrant Registry Post-Processor. It also sets the order in +which the post-processors should run. + +Failure to chain the post-processors together in this way will result in the +wrong artifact being supplied to the Vagrant Registry post-processor. This will +likely cause the Vagrant Registry post-processor to error and fail. + +Note that the Vagrant box specified in the Artifice post-processor `files` array +must end in the `.box` extension. It must also be the first file in the array. +Additional files bundled by the Artifice post-processor will be ignored. + +**JSON** + +```json +{ + "variables": { + "hcp_client_id": "{{ env `HCP_CLIENT_ID` }}", + "hcp_client_secret": "{{ env `HCP_CLIENT_SECRET` }}" + }, + + "builders": [ + { + "type": "null", + "communicator": "none" + } + ], + + "post-processors": [ + { + "type": "shell-local", + "inline": ["echo Doing stuff..."] + }, + [ + { + "type": "artifice", + "files": ["./path/to/my.box"] + }, + { + "type": "vagrant-registry", + "box_tag": "myorganisation/mybox", + "client_id": "{{user `hcp_client_id`}}", + "client_secret": "{{user `hcp_client_secret`}}", + "version": "0.1.0", + "architecture": "amd64" + } + ] + ] +} +``` + +**HCL2** + +```hcl +variable "hcp_client_id" { + type = string + default = "${env("HCP_CLIENT_ID")}" +} + +variable "hcp_client_secret" { + type = string + default = "${env("HCP_CLIENT_SECRET")}" +} + +source "null" "autogenerated_1" { + communicator = "none" +} + +build { + sources = ["source.null.autogenerated_1"] + + post-processor "shell-local" { + inline = ["echo Doing stuff..."] + } + post-processors { + post-processor "artifice" { + files = ["./path/to/my.box"] + } + post-processor "vagrant-registry" { + client_id = "${var.hcp_client_id}" + client_secret = "${var.hcp_client_secret}" + box_tag = "myorganisation/mybox" + version = "0.1.0" + architecture = "amd64" + } + } +} +``` diff --git a/GNUmakefile b/GNUmakefile index 3d028cc9..d5fc10be 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,5 +1,6 @@ NAME=vagrant BINARY=packer-plugin-${NAME} +PLUGIN_FQN="$(shell grep -E '^module' 0 { + return errs + } + + return nil +} + +func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, bool, error) { + if _, ok := builtins[artifact.BuilderId()]; !ok { + return nil, false, false, fmt.Errorf( + "Unknown artifact type: this post-processor requires an input artifact from the artifice post-processor, vagrant post-processor, or vagrant builder: %s", artifact.BuilderId()) + } + + if len(artifact.Files()) == 0 { + return nil, false, false, fmt.Errorf("No files provided in artifact for upload") + } else if !strings.HasSuffix(artifact.Files()[0], ".box") { + return nil, false, false, fmt.Errorf( + "Unknown file in artifact, Vagrant box with .box suffix is required as first artifact file: %s", artifact.Files()) + } + + var boxMetadata map[string]interface{} + var err error + + // Get the architecture + archName := p.config.Architecture + if archName == "" { + if boxMetadata, err = metadataFromVagrantBox(artifact.Files()[0]); err != nil { + return nil, false, false, err + } + if archName, err = getArchitecture(boxMetadata); err != nil { + return nil, false, false, err + } + } + + providerName, err := getProvider(artifact.Id(), artifact.Files()[0], builtins[artifact.BuilderId()], boxMetadata) + if err != nil { + return nil, false, false, fmt.Errorf("error getting provider name: %s", err) + } + + var generatedData map[interface{}]interface{} + stateData := artifact.State("generated_data") + if stateData != nil { + // Make sure it's not a nil map so we can assign to it later. + generatedData = stateData.(map[interface{}]interface{}) + } + // If stateData has a nil map generatedData will be nil + // and we need to make sure it's not + if generatedData == nil { + generatedData = make(map[interface{}]interface{}) + } + generatedData["ArtifactId"] = artifact.Id() + generatedData["Provider"] = providerName + generatedData["Architecture"] = archName + p.config.ctx.Data = generatedData + + boxDownloadUrl, err := interpolate.Render(p.config.BoxDownloadUrl, &p.config.ctx) + if err != nil { + return nil, false, false, fmt.Errorf("Failed processing box_download_url: %s", err) + } + + if p.config.BoxChecksum != "" { + if checksumParts := strings.SplitN(p.config.BoxChecksum, ":", 2); len(checksumParts) != 2 { + return nil, false, false, fmt.Errorf("box checksum must be specified as `$type:$digest`") + } + } + + // Set up the state + state := new(multistep.BasicStateBag) + state.Put("config", &p.config) + state.Put("client", registry_service.New(p.client.Transport, nil)) + state.Put("artifact", artifact) + state.Put("artifactFilePath", artifact.Files()[0]) + state.Put("ui", ui) + state.Put("providerName", providerName) + state.Put("downloadUrl", boxDownloadUrl) + state.Put("architecture", archName) + + // Build the steps + steps := []multistep.Step{ + new(stepCreateBox), + new(stepCreateVersion), + new(stepCreateProvider), + new(stepCreateArchitecture), + } + if p.config.BoxDownloadUrl == "" { + steps = append(steps, + new(stepPrepareUpload), + new(stepUpload), + new(stepConfirmUpload)) + } + steps = append(steps, new(stepReleaseVersion)) + + // Run the steps + p.runner = commonsteps.NewRunner(steps, p.config.PackerConfig, ui) + p.runner.Run(ctx, state) + + // If there was an error, return that + if rawErr, ok := state.GetOk("error"); ok { + return nil, false, false, rawErr.(error) + } + + return NewArtifact(providerName, p.config.Tag), true, false, nil +} + +func getArchitecture(metadata map[string]interface{}) (architectureName string, err error) { + if arch, ok := metadata["architecture"]; ok { + if architectureName, ok = arch.(string); ok && architectureName != "" { + return + } + } + + return "", fmt.Errorf("Error: Could not determine architecture from box metadata.json file") +} + +func getProvider(builderName, boxfile, builderId string, metadata map[string]interface{}) (string, error) { + if builderId == "artifice" { + // The artifice post processor cannot embed any data in the + // supplied artifact so the provider information must be extracted + // from the box file directly + return providerFromVagrantBox(boxfile, metadata) + } + // For the Vagrant builder and Vagrant post processor the provider can + // be determined from information embedded in the artifact + return providerFromBuilderName(builderName), nil +} + +// Converts a packer builder name to the corresponding vagrant provider +func providerFromBuilderName(name string) string { + switch name { + case "aws": + return "aws" + case "scaleway": + return "scaleway" + case "digitalocean": + return "digitalocean" + case "virtualbox": + return "virtualbox" + case "vmware": + return "vmware_desktop" + case "parallels": + return "parallels" + default: + return name + } +} + +// Returns the Vagrant provider the box is intended for use with by +// reading the metadata file packaged inside the box +func providerFromVagrantBox(boxfile string, metadata map[string]interface{}) (providerName string, err error) { + if len(metadata) == 0 { + if metadata, err = metadataFromVagrantBox(boxfile); err != nil { + return + } + } + + if prov, ok := metadata["provider"]; ok { + if providerName, ok = prov.(string); ok && providerName != "" { + return + } + } + + return "", fmt.Errorf("Could not determine provider from box metadata.json file") +} + +// Returns the metadata found within the metadata file +// packaged inside the box +func metadataFromVagrantBox(boxfile string) (metadata map[string]interface{}, err error) { + log.Printf("Attempting to extract metadata in box file. This may take some time...") + + f, err := os.Open(boxfile) + if err != nil { + return nil, fmt.Errorf("Failed to open box file: %w", err) + } + defer f.Close() + + // Vagrant boxes are gzipped tar archives + ar, err := gzip.NewReader(f) + if err != nil { + return nil, fmt.Errorf("Failed unpacking box archive: %w", err) + } + tr := tar.NewReader(ar) + + for { + var hdr *tar.Header + hdr, err = tr.Next() + if err == io.EOF { + return nil, fmt.Errorf("metadata.json file not found in box: %s", boxfile) + } + + if err != nil && err != io.EOF { + return nil, fmt.Errorf("Failed reading header info from box tar archive: %w", err) + } + + if hdr.Name == "metadata.json" { + var contents []byte + contents, err = io.ReadAll(tr) + if err != nil { + return nil, fmt.Errorf("Failed reading contents of metadata.json file from box file: %w", err) + } + err = json.Unmarshal(contents, &metadata) + if err != nil { + return nil, fmt.Errorf("Failed parsing metadata.json file: %w", err) + } + + return + } + } +} diff --git a/post-processor/hcp-vagrant-registry/post-processor.hcl2spec.go b/post-processor/hcp-vagrant-registry/post-processor.hcl2spec.go new file mode 100644 index 00000000..53bd6b3b --- /dev/null +++ b/post-processor/hcp-vagrant-registry/post-processor.hcl2spec.go @@ -0,0 +1,77 @@ +// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT. + +package hcpvagrantregistry + +import ( + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" +) + +// FlatConfig is an auto-generated flat version of Config. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatConfig struct { + PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"` + PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"` + PackerCoreVersion *string `mapstructure:"packer_core_version" cty:"packer_core_version" hcl:"packer_core_version"` + PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"` + PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"` + PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"` + PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"` + PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"` + Tag *string `mapstructure:"box_tag" cty:"box_tag" hcl:"box_tag"` + BoxDescription *string `mapstructure:"box_description" cty:"box_description" hcl:"box_description"` + BoxPrivate *bool `mapstructure:"box_private" cty:"box_private" hcl:"box_private"` + Version *string `mapstructure:"version" cty:"version" hcl:"version"` + VersionDescription *string `mapstructure:"version_description" cty:"version_description" hcl:"version_description"` + NoRelease *bool `mapstructure:"no_release" cty:"no_release" hcl:"no_release"` + Architecture *string `mapstructure:"architecture" cty:"architecture" hcl:"architecture"` + DefaultArchitecture *string `mapstructure:"default_architecture" cty:"default_architecture" hcl:"default_architecture"` + ClientID *string `mapstructure:"client_id" cty:"client_id" hcl:"client_id"` + ClientSecret *string `mapstructure:"client_secret" cty:"client_secret" hcl:"client_secret"` + BoxDownloadUrl *string `mapstructure:"box_download_url" cty:"box_download_url" hcl:"box_download_url"` + NoDirectUpload *bool `mapstructure:"no_direct_upload" cty:"no_direct_upload" hcl:"no_direct_upload"` + BoxChecksum *string `mapstructure:"box_checksum" cty:"box_checksum" hcl:"box_checksum"` + HcpApiAddress *string `mapstructure:"hcp_api_address" cty:"hcp_api_address" hcl:"hcp_api_address"` + HcpAuthUrl *string `mapstructure:"hcp_auth_url" cty:"hcp_auth_url" hcl:"hcp_auth_url"` + InsecureSkipTLSVerify *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"` +} + +// FlatMapstructure returns a new FlatConfig. +// FlatConfig is an auto-generated flat version of Config. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatConfig) +} + +// HCL2Spec returns the hcl spec of a Config. +// This spec is used by HCL to read the fields of Config. +// The decoded values from this spec will then be applied to a FlatConfig. +func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false}, + "packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false}, + "packer_core_version": &hcldec.AttrSpec{Name: "packer_core_version", Type: cty.String, Required: false}, + "packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false}, + "packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false}, + "packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false}, + "packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false}, + "packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false}, + "box_tag": &hcldec.AttrSpec{Name: "box_tag", Type: cty.String, Required: false}, + "box_description": &hcldec.AttrSpec{Name: "box_description", Type: cty.String, Required: false}, + "box_private": &hcldec.AttrSpec{Name: "box_private", Type: cty.Bool, Required: false}, + "version": &hcldec.AttrSpec{Name: "version", Type: cty.String, Required: false}, + "version_description": &hcldec.AttrSpec{Name: "version_description", Type: cty.String, Required: false}, + "no_release": &hcldec.AttrSpec{Name: "no_release", Type: cty.Bool, Required: false}, + "architecture": &hcldec.AttrSpec{Name: "architecture", Type: cty.String, Required: false}, + "default_architecture": &hcldec.AttrSpec{Name: "default_architecture", Type: cty.String, Required: false}, + "client_id": &hcldec.AttrSpec{Name: "client_id", Type: cty.String, Required: false}, + "client_secret": &hcldec.AttrSpec{Name: "client_secret", Type: cty.String, Required: false}, + "box_download_url": &hcldec.AttrSpec{Name: "box_download_url", Type: cty.String, Required: false}, + "no_direct_upload": &hcldec.AttrSpec{Name: "no_direct_upload", Type: cty.Bool, Required: false}, + "box_checksum": &hcldec.AttrSpec{Name: "box_checksum", Type: cty.String, Required: false}, + "hcp_api_address": &hcldec.AttrSpec{Name: "hcp_api_address", Type: cty.String, Required: false}, + "hcp_auth_url": &hcldec.AttrSpec{Name: "hcp_auth_url", Type: cty.String, Required: false}, + "insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false}, + } + return s +} diff --git a/post-processor/hcp-vagrant-registry/post-processor_test.go b/post-processor/hcp-vagrant-registry/post-processor_test.go new file mode 100644 index 00000000..ce6e6514 --- /dev/null +++ b/post-processor/hcp-vagrant-registry/post-processor_test.go @@ -0,0 +1,897 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcpvagrantregistry + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/hashicorp/packer-plugin-sdk/packer" + "github.com/stretchr/testify/require" +) + +type stubResponse struct { + Path string + Method string + Response string + StatusCode int +} + +type tarFiles []struct { + Name, Body string +} + +func TestPostProcessor(t *testing.T) { + require := require.New(t) + ctx := context.Background() + + t.Run("Configure", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + testCases := []struct { + desc string + setup func(map[string]interface{}) + }{ + { + desc: "minimal", + }, + { + desc: "with checksum", + setup: func(c map[string]interface{}) { + c["box_checksum"] = "sha256:testchecksumvalue" + }, + }, + { + desc: "with custom address and no credentials", + setup: func(c map[string]interface{}) { + delete(c, "client_id") + delete(c, "client_secret") + c["hcp_api_address"] = "localhost" + }, + }, + { + desc: "with custom address and skip tls verify", + setup: func(c map[string]interface{}) { + c["hcp_api_address"] = "localhost" + c["insecure_skip_tls_verify"] = true + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + var p PostProcessor + config := testMinimalConfig() + if tc.setup != nil { + tc.setup(config) + } + + require.NoError(p.Configure(config)) + }) + } + }) + + t.Run("invalid", func(t *testing.T) { + testCases := []struct { + desc string + setup func(map[string]interface{}) + wantErr string + }{ + { + desc: "bad tag", + setup: func(c map[string]interface{}) { + c["box_tag"] = "box-name" + }, + wantErr: "box_tag must include registry and box name", + }, + { + desc: "missing tag", + setup: func(c map[string]interface{}) { + delete(c, "box_tag") + }, + wantErr: "box_tag must be set", + }, + { + desc: "missing version", + setup: func(c map[string]interface{}) { + delete(c, "version") + }, + wantErr: "version must be set", + }, + { + desc: "missing client id", + setup: func(c map[string]interface{}) { + delete(c, "client_id") + }, + wantErr: "client_id must be set", + }, + { + desc: "missing client secret", + setup: func(c map[string]interface{}) { + delete(c, "client_secret") + }, + wantErr: "client_secret must be set", + }, + { + desc: "checksum format", + setup: func(c map[string]interface{}) { + c["box_checksum"] = "testchecksumvalue" + }, + wantErr: "box_checksum format invalid", + }, + { + desc: "skip tls verify", + setup: func(c map[string]interface{}) { + c["insecure_skip_tls_verify"] = true + }, + wantErr: "insecure_skip_tls_verify cannot be enabled for HCP", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + var p PostProcessor + config := testMinimalConfig() + if tc.setup != nil { + tc.setup(config) + } + + require.ErrorContains(p.Configure(config), tc.wantErr) + }) + } + }) + }) + + t.Run("PostProcess", func(t *testing.T) { + uploadServer := newDummyServer() + uploadURL := fmt.Sprintf("http://%s/do-upload", uploadServer.Listener.Addr()) + uploadObject := "TEST_OBJECT_ID" + uploadCallbackURL := fmt.Sprintf("http://%s/do-upload-callback", uploadServer.Listener.Addr()) + + testCases := []struct { + desc string + files tarFiles + stack []stubResponse + setup func(config map[string]interface{}, artifact *packer.MockArtifact) + wantErr string + }{ + { + desc: "Invalid - missing architecture", + files: tarFiles{ + {"foo.txt", "This is a foo file"}, + {"bar.txt", "This is a bar file"}, + {"metadata.json", `{"provider": "virtualbox"}`}, + }, + wantErr: "not determine architecture", + }, + { + desc: "OK - architecture config only", + stack: []stubResponse{ + { + Method: "POST", + Path: "/oauth2/token", + Response: `{"access_token": "TEST_TOKEN", "expiry": 0}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5", + Response: `{"version": {"state": "UNRELEASED"}}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64/upload", + Response: fmt.Sprintf(`{"url": "%s"}`, uploadURL), + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/release", + Response: `{}`, + StatusCode: 200, + }, + }, + files: tarFiles{ + {"foo.txt", "This is a foo file"}, + {"bar.txt", "This is a bar file"}, + {"metadata.json", `{"provider": "virtualbox"}`}, + }, + setup: func(c map[string]interface{}, _ *packer.MockArtifact) { + c["architecture"] = "amd64" + }, + }, + { + desc: "OK - architecture metadata", + stack: []stubResponse{ + { + Method: "POST", + Path: "/oauth2/token", + Response: `{"access_token": "TEST_TOKEN", "expiry": 0}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5", + Response: `{"version": {"state": "UNRELEASED"}}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64/upload", + Response: fmt.Sprintf(`{"url": "%s"}`, uploadURL), + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/release", + Response: `{}`, + StatusCode: 200, + }, + }, + files: tarFiles{ + {"foo.txt", "This is a foo file"}, + {"bar.txt", "This is a bar file"}, + {"metadata.json", `{"provider": "virtualbox", "architecture": "amd64"}`}, + }, + }, + { + desc: "OK - architecture metadata and config", + stack: []stubResponse{ + { + Method: "POST", + Path: "/oauth2/token", + Response: `{"access_token": "TEST_TOKEN", "expiry": 0}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5", + Response: `{"version": {"state": "UNRELEASED"}}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64/upload", + Response: fmt.Sprintf(`{"url": "%s"}`, uploadURL), + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/release", + Response: `{}`, + StatusCode: 200, + }, + }, + files: tarFiles{ + {"foo.txt", "This is a foo file"}, + {"bar.txt", "This is a bar file"}, + {"metadata.json", `{"provider": "virtualbox", "architecture": "arm"}`}, + }, + setup: func(c map[string]interface{}, _ *packer.MockArtifact) { + c["architecture"] = "amd64" + }, + }, + { + desc: "Invalid - bad version read response", + stack: []stubResponse{ + { + Method: "POST", + Path: "/oauth2/token", + Response: `{"access_token": "TEST_TOKEN", "expiry": 0}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5", + Response: `{}`, + StatusCode: 200, + }, + }, + files: tarFiles{ + {"foo.txt", "This is a foo file"}, + {"bar.txt", "This is a bar file"}, + {"metadata.json", `{"provider": "virtualbox", "architecture": "amd64"}`}, + }, + wantErr: "Invalid response body", + }, + { + desc: "OK - creates box when missing", + stack: []stubResponse{ + { + Method: "POST", + Path: "/oauth2/token", + Response: `{"access_token": "TEST_TOKEN", "expiry": 0}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64", + StatusCode: 404, + }, + { + Method: "POST", + Path: "/vagrant/2022-09-30/registry/hashicorp/boxes", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5", + StatusCode: 404, + }, + { + Method: "POST", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/versions", + Response: `{"version": {"state": "UNRELEASED"}}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64/upload", + Response: fmt.Sprintf(`{"url": "%s"}`, uploadURL), + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/release", + Response: `{}`, + StatusCode: 200, + }, + }, + files: tarFiles{ + {"foo.txt", "This is a foo file"}, + {"bar.txt", "This is a bar file"}, + {"metadata.json", `{"provider": "virtualbox", "architecture": "amd64"}`}, + }, + }, + { + desc: "OK - creates version when missing", + stack: []stubResponse{ + { + Method: "POST", + Path: "/oauth2/token", + Response: `{"access_token": "TEST_TOKEN", "expiry": 0}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5", + StatusCode: 404, + }, + { + Method: "POST", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/versions", + Response: `{"version": {"state": "UNRELEASED"}}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64/upload", + Response: fmt.Sprintf(`{"url": "%s"}`, uploadURL), + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/release", + Response: `{}`, + StatusCode: 200, + }, + }, + files: tarFiles{ + {"foo.txt", "This is a foo file"}, + {"bar.txt", "This is a bar file"}, + {"metadata.json", `{"provider": "virtualbox", "architecture": "amd64"}`}, + }, + }, + { + desc: "OK - creates provider when missing", + stack: []stubResponse{ + { + Method: "POST", + Path: "/oauth2/token", + Response: `{"access_token": "TEST_TOKEN", "expiry": 0}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5", + Response: `{"version": {"state": "UNRELEASED"}}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox", + StatusCode: 404, + }, + { + Method: "POST", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/providers", + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64/upload", + Response: fmt.Sprintf(`{"url": "%s"}`, uploadURL), + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/release", + Response: `{}`, + StatusCode: 200, + }, + }, + files: tarFiles{ + {"foo.txt", "This is a foo file"}, + {"bar.txt", "This is a bar file"}, + {"metadata.json", `{"provider": "virtualbox", "architecture": "amd64"}`}, + }, + }, + { + desc: "OK - creates architecture when missing", + stack: []stubResponse{ + { + Method: "POST", + Path: "/oauth2/token", + Response: `{"access_token": "TEST_TOKEN", "expiry": 0}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5", + Response: `{"version": {"state": "UNRELEASED"}}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + StatusCode: 404, + }, + { + Method: "POST", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architectures", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64/upload", + Response: fmt.Sprintf(`{"url": "%s"}`, uploadURL), + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/release", + Response: `{}`, + StatusCode: 200, + }, + }, + files: tarFiles{ + {"foo.txt", "This is a foo file"}, + {"bar.txt", "This is a bar file"}, + {"metadata.json", `{"provider": "virtualbox", "architecture": "amd64"}`}, + }, + }, + { + desc: "OK - does not release version when already released", + stack: []stubResponse{ + { + Method: "POST", + Path: "/oauth2/token", + Response: `{"access_token": "TEST_TOKEN", "expiry": 0}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5", + Response: `{"version": {"state": "RELEASED"}}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + StatusCode: 404, + }, + { + Method: "POST", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architectures", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64/upload", + Response: fmt.Sprintf(`{"url": "%s"}`, uploadURL), + StatusCode: 200, + }, + }, + files: tarFiles{ + {"foo.txt", "This is a foo file"}, + {"bar.txt", "This is a bar file"}, + {"metadata.json", `{"provider": "virtualbox", "architecture": "amd64"}`}, + }, + }, + { + desc: "OK - direct upload", + stack: []stubResponse{ + { + Method: "POST", + Path: "/oauth2/token", + Response: `{"access_token": "TEST_TOKEN", "expiry": 0}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5", + Response: `{"version": {"state": "RELEASED"}}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64", + Response: `{}`, + StatusCode: 200, + }, + { + Method: "GET", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64/direct/upload", + Response: fmt.Sprintf(`{"url": "%s", "callback": "%s", "object": "%s"}`, uploadURL, uploadCallbackURL, uploadObject), + StatusCode: 200, + }, + { + Method: "PUT", + Path: "/vagrant/2022-09-30/registry/hashicorp/box/precise64/version/0.5/provider/virtualbox/architecture/amd64/direct/complete/TEST_OBJECT_ID", + Response: `{}`, + StatusCode: 200, + }, + }, + files: tarFiles{ + {"foo.txt", "This is a foo file"}, + {"bar.txt", "This is a bar file"}, + {"metadata.json", `{"provider": "virtualbox", "architecture": "amd64"}`}, + }, + setup: func(c map[string]interface{}, _ *packer.MockArtifact) { + c["no_direct_upload"] = false + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + boxfile, err := createBox(tc.files) + require.NoError(err, "failed to create test box") + defer os.Remove(boxfile.Name()) + + artifact := &packer.MockArtifact{ + BuilderIdValue: "mitchellh.post-processor.vagrant", + FilesValue: []string{boxfile.Name()}, + IdValue: "virtualbox", + } + server := newStackServer(tc.stack) + defer server.Close() + + config := testMinimalConfigAddr(server.Listener.Addr().String()) + config["no_direct_upload"] = true + + if tc.setup != nil { + tc.setup(config, artifact) + } + + var p PostProcessor + require.NoError(p.Configure(config), "failed to configure post processor") + + _, _, _, err = p.PostProcess(ctx, testUi(), artifact) + if tc.wantErr != "" { + require.Error(err) + require.ErrorContains(err, tc.wantErr) + } else { + require.NoError(err) + } + }) + } + }) +} + +func newBoxFile() (boxfile *os.File, err error) { + boxfile, err = os.CreateTemp(os.TempDir(), "test*.box") + if err != nil { + return boxfile, fmt.Errorf("Error creating test box file: %s", err) + } + return boxfile, nil +} + +func createBox(files tarFiles) (boxfile *os.File, err error) { + boxfile, err = newBoxFile() + if err != nil { + return boxfile, err + } + + // Box files are gzipped tar archives + aw := gzip.NewWriter(boxfile) + tw := tar.NewWriter(aw) + + // Add each file to the box + for _, file := range files { + // Create and write the tar file header + hdr := &tar.Header{ + Name: file.Name, + Mode: 0644, + Size: int64(len(file.Body)), + } + err = tw.WriteHeader(hdr) + if err != nil { + return boxfile, fmt.Errorf("Error writing box tar file header: %s", err) + } + // Write the file contents + _, err = tw.Write([]byte(file.Body)) + if err != nil { + return boxfile, fmt.Errorf("Error writing box tar file contents: %s", err) + } + } + // Flush and close each writer + err = tw.Close() + if err != nil { + return boxfile, fmt.Errorf("Error flushing tar file contents: %s", err) + } + err = aw.Close() + if err != nil { + return boxfile, fmt.Errorf("Error flushing gzip file contents: %s", err) + } + + return boxfile, nil +} + +func testUi() *packer.BasicUi { + return &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + } +} + +func testMinimalConfig() map[string]interface{} { + return map[string]interface{}{ + "box_tag": "hashicorp/precise64", + "version": "0.5", + "client_id": "TEST-CLIENT-ID", + "client_secret": "TEST-CLIENT-SECRET", + } +} + +func testMinimalConfigAddr(addr string) map[string]interface{} { + c := testMinimalConfig() + c["hcp_api_address"] = addr + c["hcp_auth_url"] = fmt.Sprintf("https://%s", addr) + c["insecure_skip_tls_verify"] = true + + return c +} + +func newStackServer(stack []stubResponse) *httptest.Server { + return httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if len(stack) < 1 { + fmt.Printf("Request stack is empty - unhandled %s %s\n", req.Method, req.URL.Path) + rw.Header().Add("Error", fmt.Sprintf("Request stack is empty - Method: %s Path: %s", req.Method, req.URL.Path)) + http.Error(rw, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + match := stack[0] + stack = stack[1:] + if match.Method != "" && req.Method != match.Method { + fmt.Printf("Request not matched - request[%q %s] != match[%q %s]\n", req.URL.Path, req.Method, match.Path, match.Method) + rw.Header().Add("Error", fmt.Sprintf("Request %s != %s", match.Method, req.Method)) + http.Error(rw, fmt.Sprintf("Request %s != %s", match.Method, req.Method), http.StatusInternalServerError) + return + } + if match.Path != "" && match.Path != req.URL.Path { + fmt.Printf("Request not matched - request[%q %s] != match[%q %s]\n", req.URL.Path, req.Method, match.Path, match.Method) + rw.Header().Add("Error", fmt.Sprintf("Request %s != %s", match.Path, req.URL.Path)) + http.Error(rw, fmt.Sprintf("Request %s != %s", match.Path, req.URL.Path), http.StatusInternalServerError) + return + } + rw.Header().Add("Complete", fmt.Sprintf("Method: %s Path: %s", match.Method, match.Path)) + rw.Header().Add("Content-Type", "application/json") + rw.WriteHeader(match.StatusCode) + if match.Response != "" { + _, err := rw.Write([]byte(match.Response)) + if err != nil { + panic("failed to write response: " + err.Error()) + } + } + })) +} + +func newDummyServer() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + fmt.Printf("[Dummy Server] %s %s\n", req.Method, req.URL.Path) + + rw.WriteHeader(200) + })) +} diff --git a/post-processor/hcp-vagrant-registry/step_confirm_upload.go b/post-processor/hcp-vagrant-registry/step_confirm_upload.go new file mode 100644 index 00000000..2105faa1 --- /dev/null +++ b/post-processor/hcp-vagrant-registry/step_confirm_upload.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcpvagrantregistry + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/client/registry_service" + "github.com/hashicorp/packer-plugin-sdk/multistep" +) + +type stepConfirmUpload struct{} + +func (s *stepConfirmUpload) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*registry_service.Client) + config := state.Get("config").(*Config) + + if config.NoDirectUpload { + return multistep.ActionContinue + } + + providerName := state.Get("providerName").(string) + archName := state.Get("architecture").(string) + object := state.Get("upload-object").(string) + + _, err := client.CompleteDirectUploadBox( + ®istry_service.CompleteDirectUploadBoxParams{ + Context: ctx, + Registry: config.registry, + Box: config.box, + Version: config.Version, + Provider: providerName, + Architecture: archName, + Object: object, + }, nil, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if errMsg, ok := errorResponseMsg(err); ok { + state.Put("error", fmt.Errorf("Failure confirming upload: %s", errMsg)) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepConfirmUpload) Cleanup(state multistep.StateBag) {} diff --git a/post-processor/hcp-vagrant-registry/step_create_architecture.go b/post-processor/hcp-vagrant-registry/step_create_architecture.go new file mode 100644 index 00000000..e1405084 --- /dev/null +++ b/post-processor/hcp-vagrant-registry/step_create_architecture.go @@ -0,0 +1,117 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcpvagrantregistry + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/client/registry_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/models" + "github.com/hashicorp/packer-plugin-sdk/multistep" +) + +type stepCreateArchitecture struct{} + +func (s *stepCreateArchitecture) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*registry_service.Client) + providerName := state.Get("providerName").(string) + downloadUrl := state.Get("downloadUrl").(string) + config := state.Get("config").(*Config) + archName := state.Get("architecture").(string) + + resp, err := client.ReadArchitecture( + ®istry_service.ReadArchitectureParams{ + Context: ctx, + Registry: config.registry, + Box: config.box, + Version: config.Version, + Provider: providerName, + Architecture: archName, + }, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if resp, ok := errorResponse(err); ok { + // Any code outside of not found should be an error + if !resp.IsCode(404) { + state.Put("error", fmt.Errorf("Failure retrieving architecture: %s", resp.GetPayload().Message)) + return multistep.ActionHalt + } + } + + data := &models.HashicorpCloudVagrant20220930BoxData{} + + if downloadUrl != "" { + data.DownloadURL = downloadUrl + } + + if config.BoxChecksum != "" { + data.Checksum = config.checksum + data.ChecksumType = models.NewHashicorpCloudVagrant20220930ChecksumType( + models.HashicorpCloudVagrant20220930ChecksumType(config.checksumType), + ) + } else { + data.ChecksumType = models.HashicorpCloudVagrant20220930ChecksumTypeNONE.Pointer() + } + + // If the architecture already exists, update it + if resp != nil && resp.IsSuccess() { + _, err := client.UpdateArchitecture( + ®istry_service.UpdateArchitectureParams{ + Context: ctx, + Registry: config.registry, + Box: config.box, + Version: config.Version, + Provider: providerName, + Architecture: archName, + Data: &models.HashicorpCloudVagrant20220930Architecture{ + BoxData: data, + }, + }, nil, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if errMsg, ok := errorResponseMsg(err); ok { + state.Put("error", fmt.Errorf("Failure updating existing architecture: %s", errMsg)) + return multistep.ActionHalt + } + + return multistep.ActionContinue + } + + _, err = client.CreateArchitecture( + ®istry_service.CreateArchitectureParams{ + Context: ctx, + Registry: config.registry, + Box: config.box, + Version: config.Version, + Provider: providerName, + Data: &models.HashicorpCloudVagrant20220930Architecture{ + ArchitectureType: archName, + Default: archName == config.DefaultArchitecture, + BoxData: data, + }, + }, nil, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if errMsg, ok := errorResponseMsg(err); ok { + state.Put("error", fmt.Errorf("Failure creating new architecture: %s", errMsg)) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepCreateArchitecture) Cleanup(state multistep.StateBag) {} diff --git a/post-processor/hcp-vagrant-registry/step_create_box.go b/post-processor/hcp-vagrant-registry/step_create_box.go new file mode 100644 index 00000000..f1b8a484 --- /dev/null +++ b/post-processor/hcp-vagrant-registry/step_create_box.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcpvagrantregistry + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/client/registry_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/models" + "github.com/hashicorp/packer-plugin-sdk/multistep" + "github.com/hashicorp/packer-plugin-sdk/packer" +) + +type stepCreateBox struct{} + +func (s *stepCreateBox) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*registry_service.Client) + ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(*Config) + + resp, err := client.ReadBox(®istry_service.ReadBoxParams{ + Context: ctx, + Box: config.box, + Registry: config.registry, + }) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if resp, ok := errorResponse(err); ok { + // Any code outside of not found should be an error + if !resp.IsCode(404) { + state.Put("error", fmt.Errorf("Failure retrieving box: %s", resp.GetPayload().Message)) + return multistep.ActionHalt + } + } + + // If the request was successful, nothing to do + if resp != nil && resp.IsSuccess() { + ui.Say(fmt.Sprintf("Found box and verified accessible: %s", config.Tag)) + return multistep.ActionContinue + } + + // Create the box + _, err = client.CreateBox( + ®istry_service.CreateBoxParams{ + Context: ctx, + Registry: config.registry, + Data: &models.HashicorpCloudVagrant20220930Box{ + Name: config.box, + Description: config.BoxDescription, + IsPrivate: config.BoxPrivate, + }, + }, nil, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if errMsg, ok := errorResponseMsg(err); ok { + state.Put("error", fmt.Errorf("Failure creating new box: %s", errMsg)) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Created new box: %s", config.Tag)) + + return multistep.ActionContinue +} + +func (s *stepCreateBox) Cleanup(state multistep.StateBag) {} diff --git a/post-processor/hcp-vagrant-registry/step_create_provider.go b/post-processor/hcp-vagrant-registry/step_create_provider.go new file mode 100644 index 00000000..de5e7366 --- /dev/null +++ b/post-processor/hcp-vagrant-registry/step_create_provider.go @@ -0,0 +1,77 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcpvagrantregistry + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/client/registry_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/models" + "github.com/hashicorp/packer-plugin-sdk/multistep" + "github.com/hashicorp/packer-plugin-sdk/packer" +) + +type stepCreateProvider struct{} + +func (s *stepCreateProvider) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*registry_service.Client) + ui := state.Get("ui").(packer.Ui) + providerName := state.Get("providerName").(string) + config := state.Get("config").(*Config) + + resp, err := client.ReadProvider( + ®istry_service.ReadProviderParams{ + Context: ctx, + Registry: config.registry, + Box: config.box, + Version: config.Version, + Provider: providerName, + }, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if resp, ok := errorResponse(err); ok { + // Any code outside of not found should be an error + if !resp.IsCode(404) { + state.Put("error", fmt.Errorf("Failure retrieving provider: %s", resp.GetPayload().Message)) + return multistep.ActionHalt + } + } + + if resp != nil && resp.IsSuccess() { + ui.Message("Provider exists, skipping creation") + return multistep.ActionContinue + } + + _, err = client.CreateProvider( + ®istry_service.CreateProviderParams{ + Context: ctx, + Registry: config.registry, + Box: config.box, + Version: config.Version, + Data: &models.HashicorpCloudVagrant20220930Provider{ + Name: providerName, + }, + }, nil, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if errMsg, ok := errorResponseMsg(err); ok { + state.Put("error", fmt.Errorf("Failure creating new provider: %s", errMsg)) + return multistep.ActionHalt + } + + ui.Say(fmt.Sprintf("Created new provider: %s", providerName)) + + return multistep.ActionContinue +} + +func (s *stepCreateProvider) Cleanup(state multistep.StateBag) {} diff --git a/post-processor/hcp-vagrant-registry/step_create_version.go b/post-processor/hcp-vagrant-registry/step_create_version.go new file mode 100644 index 00000000..70402c31 --- /dev/null +++ b/post-processor/hcp-vagrant-registry/step_create_version.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcpvagrantregistry + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/client/registry_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/models" + "github.com/hashicorp/packer-plugin-sdk/multistep" + "github.com/hashicorp/packer-plugin-sdk/packer" +) + +type stepCreateVersion struct{} + +func (s *stepCreateVersion) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*registry_service.Client) + ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(*Config) + + resp, err := client.ReadVersion( + ®istry_service.ReadVersionParams{ + Context: ctx, + Registry: config.registry, + Box: config.box, + Version: config.Version, + }, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if resp, ok := errorResponse(err); ok { + // Any code outside of not found should error + if !resp.IsCode(404) { + state.Put("error", fmt.Errorf("Failure retrieving version: %s", resp.GetPayload().Message)) + return multistep.ActionHalt + } + } + + // If the request was successful, nothing to do + if resp != nil && resp.IsSuccess() { + if resp.Payload == nil || resp.Payload.Version == nil { + state.Put("error", fmt.Errorf("Invalid response body for version read")) + return multistep.ActionHalt + } + + state.Put("version", resp.Payload.Version) + ui.Message("Version exists, skipping creation") + return multistep.ActionContinue + } + + vresp, err := client.CreateVersion( + ®istry_service.CreateVersionParams{ + Context: ctx, + Registry: config.registry, + Box: config.box, + Data: &models.HashicorpCloudVagrant20220930Version{ + Name: config.Version, + Description: config.VersionDescription, + }, + }, nil, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if errMsg, ok := errorResponseMsg(err); ok { + state.Put("error", fmt.Errorf("Failure creating new version: %s", errMsg)) + return multistep.ActionHalt + } + + if vresp.Payload == nil || vresp.Payload.Version == nil { + state.Put("error", fmt.Errorf("Invalid response body for version create")) + return multistep.ActionHalt + } + + state.Put("version", vresp.Payload.Version) + ui.Say(fmt.Sprintf("Created new version: %s", config.Version)) + + return multistep.ActionContinue +} + +func (s *stepCreateVersion) Cleanup(state multistep.StateBag) {} diff --git a/post-processor/hcp-vagrant-registry/step_prepare_upload.go b/post-processor/hcp-vagrant-registry/step_prepare_upload.go new file mode 100644 index 00000000..87444bd1 --- /dev/null +++ b/post-processor/hcp-vagrant-registry/step_prepare_upload.go @@ -0,0 +1,92 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcpvagrantregistry + +import ( + "context" + "fmt" + "os" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/client/registry_service" + "github.com/hashicorp/packer-plugin-sdk/multistep" + "github.com/hashicorp/packer-plugin-sdk/packer" +) + +const HCP_VAGRANT_REGISTRY_DIRECT_UPLOAD_LIMIT = 5368709120 // Upload limit is 5G + +type stepPrepareUpload struct{} + +func (s *stepPrepareUpload) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*registry_service.Client) + config := state.Get("config").(*Config) + ui := state.Get("ui").(packer.Ui) + providerName := state.Get("providerName").(string) + archName := state.Get("architecture").(string) + artifactFilePath := state.Get("artifactFilePath").(string) + + // If direct upload is enabled, the asset size must be <= 5 GB + if config.NoDirectUpload == false { + f, err := os.Stat(artifactFilePath) + if err != nil { + ui.Error(fmt.Sprintf("failed determining size of upload artifact: %s", artifactFilePath)) + } + if f.Size() > HCP_VAGRANT_REGISTRY_DIRECT_UPLOAD_LIMIT { + ui.Say(fmt.Sprintf("Asset %s is larger than the direct upload limit. Setting `NoDirectUpload` to true", artifactFilePath)) + config.NoDirectUpload = true + } + } + + ui.Say(fmt.Sprintf("Preparing upload of box: %s", artifactFilePath)) + + if config.NoDirectUpload { + resp, err := client.UploadBox( + ®istry_service.UploadBoxParams{ + Context: ctx, + Registry: config.registry, + Box: config.box, + Version: config.Version, + Provider: providerName, + Architecture: archName, + }, nil, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if errMsg, ok := errorResponseMsg(err); ok { + state.Put("error", fmt.Errorf("Failure preparing upload: %s", errMsg)) + return multistep.ActionHalt + } + + state.Put("upload-url", resp.Payload.URL) + } else { + resp, err := client.DirectUploadBox( + ®istry_service.DirectUploadBoxParams{ + Context: ctx, + Registry: config.registry, + Box: config.box, + Version: config.Version, + Provider: providerName, + Architecture: archName, + }, nil, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if errMsg, ok := errorResponseMsg(err); ok { + state.Put("error", fmt.Errorf("Failure preparing upload: %s", errMsg)) + return multistep.ActionHalt + } + + state.Put("upload-url", resp.Payload.URL) + state.Put("upload-object", resp.Payload.Object) + } + + return multistep.ActionContinue +} + +func (s *stepPrepareUpload) Cleanup(state multistep.StateBag) {} diff --git a/post-processor/hcp-vagrant-registry/step_release_version.go b/post-processor/hcp-vagrant-registry/step_release_version.go new file mode 100644 index 00000000..fd3bd811 --- /dev/null +++ b/post-processor/hcp-vagrant-registry/step_release_version.go @@ -0,0 +1,58 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcpvagrantregistry + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/client/registry_service" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-vagrant-box-registry/stable/2022-09-30/models" + "github.com/hashicorp/packer-plugin-sdk/multistep" + "github.com/hashicorp/packer-plugin-sdk/packer" +) + +type stepReleaseVersion struct{} + +func (s *stepReleaseVersion) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*registry_service.Client) + ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(*Config) + version := state.Get("version").(*models.HashicorpCloudVagrant20220930Version) + + if config.NoRelease { + ui.Message("Not releasing version due to configuration") + return multistep.ActionContinue + } + + if version.State != nil && *version.State != models.HashicorpCloudVagrant20220930VersionStateUNRELEASED { + ui.Message("Version not in unreleased state, skipping release") + return multistep.ActionContinue + } + + ui.Say(fmt.Sprintf("Releasing version: %s", config.Version)) + + _, err := client.ReleaseVersion( + ®istry_service.ReleaseVersionParams{ + Context: ctx, + Registry: config.registry, + Box: config.box, + Version: config.Version, + }, nil, + ) + + if isErrorUnexpected(err, state) { + return multistep.ActionHalt + } + + if errMsg, ok := errorResponseMsg(err); ok { + state.Put("error", fmt.Errorf("Failure releasing version: %s", errMsg)) + return multistep.ActionHalt + } + + ui.Message("Version successfully released and available") + return multistep.ActionContinue +} + +func (s *stepReleaseVersion) Cleanup(state multistep.StateBag) {} diff --git a/post-processor/hcp-vagrant-registry/step_upload.go b/post-processor/hcp-vagrant-registry/step_upload.go new file mode 100644 index 00000000..c189f6c5 --- /dev/null +++ b/post-processor/hcp-vagrant-registry/step_upload.go @@ -0,0 +1,90 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package hcpvagrantregistry + +import ( + "context" + "fmt" + "net/http" + "os" + "time" + + "github.com/hashicorp/packer-plugin-sdk/multistep" + "github.com/hashicorp/packer-plugin-sdk/net" + "github.com/hashicorp/packer-plugin-sdk/packer" + "github.com/hashicorp/packer-plugin-sdk/retry" +) + +type stepUpload struct{} + +func (s *stepUpload) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + url := state.Get("upload-url").(string) + artifactFilePath := state.Get("artifactFilePath").(string) + + client := net.HttpClientWithEnvironmentProxy() + + // Stash the http client we built so it can + // be used in the confrim step if needed. + state.Put("http-client", client) + + ui.Say(fmt.Sprintf("Uploading box: %s", artifactFilePath)) + ui.Message( + "Depending on your internet connection and the size of the box,\n" + + "this may take some time") + + err := retry.Config{ + Tries: 3, + RetryDelay: (&retry.Backoff{InitialBackoff: 10 * time.Second, MaxBackoff: 10 * time.Second, Multiplier: 2}).Linear, + }.Run(ctx, func(ctx context.Context) (err error) { + ui.Message("Uploading box") + + defer func() { + if err != nil { + ui.Message(fmt.Sprintf( + "Error uploading box! Will retry in 10 seconds. Error: %s", err)) + } + }() + + file, err := os.Open(artifactFilePath) + if err != nil { + return + } + defer file.Close() + + info, err := file.Stat() + if err != nil { + return + } + + request, err := http.NewRequest("PUT", url, file) + if err != nil { + return + } + + request.ContentLength = info.Size() + resp, err := client.Do(request) + if err != nil { + return + } + + if resp.StatusCode != 200 { + err = fmt.Errorf("bad HTTP status: %d", resp.StatusCode) + return + } + + return + }) + + if err != nil { + state.Put("error", fmt.Errorf("Failed to upload box asset: %s", err)) + return multistep.ActionHalt + } + + ui.Message("Box successfully uploaded") + + return multistep.ActionContinue +} + +func (s *stepUpload) Cleanup(state multistep.StateBag) {} diff --git a/post-processor/hcp-vagrant-registry/util.go b/post-processor/hcp-vagrant-registry/util.go new file mode 100644 index 00000000..051b1173 --- /dev/null +++ b/post-processor/hcp-vagrant-registry/util.go @@ -0,0 +1,71 @@ +package hcpvagrantregistry + +import ( + "fmt" + + "github.com/go-openapi/runtime" + "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + "github.com/hashicorp/packer-plugin-sdk/multistep" +) + +type hcpErrorResponse interface { + IsSuccess() bool + IsRedirect() bool + IsClientError() bool + IsServerError() bool + IsCode(int) bool + Code() int + GetPayload() *models.GoogleRPCStatus +} + +func isErrorUnexpected(err error, state multistep.StateBag) bool { + if err == nil { + return false + } + + if _, ok := errorResponse(err); !ok { + state.Put("error", fmt.Errorf("Unexpected client error: %s", err)) + return true + } + + return false +} + +func errorResponse(err error) (hcpErrorResponse, bool) { + if err == nil { + return nil, false + } + + if val, ok := err.(*runtime.APIError); ok { + if resp, ok := val.Response.(hcpErrorResponse); ok { + return resp, true + } + } + + if resp, ok := err.(hcpErrorResponse); ok { + return resp, true + } + + return nil, false +} + +func errorStatus(err error) (*models.GoogleRPCStatus, bool) { + if val, ok := errorResponse(err); ok { + return val.GetPayload(), true + } + + return nil, false +} + +func errorResponseMsg(err error) (string, bool) { + if val, ok := errorStatus(err); ok { + msg := val.Message + if msg == "" { + msg = "Unexpected error encountered" + } + + return msg, true + } + + return "", false +} diff --git a/version/version.go b/version/version.go index 29250ecc..660b612b 100644 --- a/version/version.go +++ b/version/version.go @@ -7,7 +7,7 @@ import "github.com/hashicorp/packer-plugin-sdk/version" var ( // Version is the main version number that is being run at the moment. - Version = "1.1.2" + Version = "1.1.4" // VersionPrerelease is A pre-release marker for the Version. If this is "" // (empty string) then it means that it is a final release. Otherwise, this