├── .codacy.yml ├── .github ├── dependabot.yml └── workflows │ ├── check-pr.yml │ ├── notify-integration-release-via-manual.yaml │ └── notify-integration-release-via-tag.yaml ├── .gitignore ├── .goreleaser.yml ├── .web-docs ├── README.md ├── components │ ├── builder │ │ ├── vm-clone │ │ │ └── README.md │ │ └── vm-create │ │ │ └── README.md │ └── post-processor │ │ └── anka-registry-push │ │ └── README.md └── scripts │ └── compile-to-webdocs.sh ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── VERSIONING.md ├── builder └── anka │ ├── artifact.go │ ├── builder.go │ ├── builder_test.go │ ├── communicator.go │ ├── config.go │ ├── config.hcl2spec.go │ ├── step_clone_vm.go │ ├── step_clone_vm_test.go │ ├── step_connect_anka.go │ ├── step_create_vm.go │ ├── step_create_vm_test.go │ ├── step_set_generated_data.go │ ├── step_set_generated_data_test.go │ ├── step_start_vm.go │ ├── step_start_vm_test.go │ ├── step_temp_dir.go │ └── test-fixtures │ ├── onecakes │ └── strawberry │ └── scripts │ ├── script1.sh │ └── script2.sh ├── client ├── cli.go ├── client.go ├── client_core.go ├── client_registry.go └── runner.go ├── common └── errors.go ├── docs ├── README.md ├── builders │ ├── vm-clone.mdx │ └── vm-create.mdx ├── metadata.hcl └── post-processors │ └── anka-registry-push.mdx ├── examples ├── .cicd │ ├── should-fail-disk-shrink.pkr.hcl │ └── should-fail-exit-code.pkr.hcl ├── ansible │ ├── ansible.json │ └── playbook.yml ├── clone-existing-with-expect-disconnect.pkr.hcl ├── clone-existing-with-file-provisioner.pkr.hcl ├── clone-existing-with-hwuuid.pkr.hcl ├── clone-existing-with-new-disk-size.pkr.hcl ├── clone-existing-with-pg-display_controller.pkr.hcl ├── clone-existing-with-port-forwarding-rules.pkr.hcl ├── clone-existing-with-post-processing.pkr.hcl ├── clone-existing-with-update_addons.pkr.hcl ├── clone-existing-with-use-anka-cp.pkr.hcl ├── clone-existing.pkr.hcl ├── create-from-installer-with-port-forwarding-rules.pkr.hcl ├── create-from-installer-with-post-processing.pkr.hcl ├── create-from-installer.pkr.hcl ├── create-stopped-from-installer.pkr.hcl └── pull-and-clone-existing.pkr.hcl ├── go.mod ├── go.sum ├── main.go ├── mocks ├── client_mock.go └── util_mock.go ├── post-processor └── ankaregistry │ ├── post-processor.go │ ├── post-processor.hcl2spec.go │ └── post-processor_test.go └── util └── util.go /.codacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - "./README.md" 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://docs.github.com/en/github/administering-a-repository/enabling-and-disabling-version-updates 3 | version: 2 4 | updates: 5 | - package-ecosystem: "gomod" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | day: "tuesday" 10 | assignees: 11 | - "NorseGaud" 12 | reviewers: 13 | - "NorseGaud" 14 | -------------------------------------------------------------------------------- /.github/workflows/check-pr.yml: -------------------------------------------------------------------------------- 1 | name: Check PR 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | branches: [ master ] 7 | 8 | jobs: 9 | 10 | goreleaser: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | ref: ${{ github.event.pull_request.head.sha }} 16 | fetch-depth: 0 17 | - run: git tag $(cat VERSION) 18 | - name: Set up Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: 1.19 22 | - name: Setup `packer` 23 | uses: hashicorp/setup-packer@main 24 | id: setup 25 | with: 26 | version: 1.11.0-beta 27 | - name: Describe plugin 28 | id: plugin_describe 29 | run: echo "::set-output name=api_version::$(go run . describe | jq -r '.api_version')" 30 | - name: Install packer-pdc 31 | run: go get github.com/hashicorp/packer-plugin-sdk/cmd/packer-sdc@latest 32 | - name: Run GoReleaser build 33 | uses: goreleaser/goreleaser-action@v4 34 | with: 35 | version: latest 36 | args: build --single-target --snapshot --clean 37 | env: 38 | PACKER_CI_PROJECT_API_VERSION: ${{ steps.plugin_describe.outputs.api_version }} -------------------------------------------------------------------------------- /.github/workflows/notify-integration-release-via-manual.yaml: -------------------------------------------------------------------------------- 1 | # Manual release workflow is used for deploying documentation updates 2 | # on the specified branch without making an official plugin release. 3 | name: Notify Integration Release (Manual) 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | version: 8 | description: "The release version (semver)" 9 | default: 1.0.0 10 | required: false 11 | branch: 12 | description: "A branch or SHA" 13 | default: 'master' 14 | required: false 15 | jobs: 16 | notify-release: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout this repo 20 | uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 21 | with: 22 | ref: ${{ github.event.inputs.branch }} 23 | # Ensure that Docs are Compiled 24 | - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 25 | - shell: bash 26 | run: make generate 27 | - shell: bash 28 | run: | 29 | if [[ -z "$(git status -s)" ]]; then 30 | echo "OK" 31 | else 32 | echo "Docs have been updated, but the compiled docs have not been committed." 33 | echo "Run 'make generate', and commit the result to resolve this error." 34 | exit 1 35 | fi 36 | # Perform the Release 37 | - name: Checkout integration-release-action 38 | uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 39 | with: 40 | repository: hashicorp/integration-release-action 41 | path: ./integration-release-action 42 | - name: Notify Release 43 | uses: ./integration-release-action 44 | with: 45 | # The integration identifier will be used by the Packer team to register the integration 46 | # the expected format is packer// 47 | integration_identifier: "packer/veertuinc/veertu-anka" 48 | release_version: ${{ github.event.inputs.version }} 49 | release_sha: ${{ github.event.inputs.branch }} 50 | github_token: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /.github/workflows/notify-integration-release-via-tag.yaml: -------------------------------------------------------------------------------- 1 | name: Notify Integration Release (Tag) 2 | on: 3 | push: 4 | tags: 5 | - '*.*.*' # Proper releases 6 | jobs: 7 | strip-version: 8 | runs-on: ubuntu-latest 9 | outputs: 10 | packer-version: ${{ steps.strip.outputs.packer-version }} 11 | steps: 12 | - name: Strip leading v from version tag 13 | id: strip 14 | env: 15 | REF: ${{ github.ref_name }} 16 | run: | 17 | echo "packer-version=$(echo "$REF" | sed -E 's/v?([0-9]+\.[0-9]+\.[0-9]+)/\1/')" >> "$GITHUB_OUTPUT" 18 | notify-release: 19 | needs: 20 | - strip-version 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout this repo 24 | uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 25 | with: 26 | ref: ${{ github.ref }} 27 | # Ensure that Docs are Compiled 28 | - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 29 | - shell: bash 30 | run: make generate 31 | - shell: bash 32 | run: | 33 | if [[ -z "$(git status -s)" ]]; then 34 | echo "OK" 35 | else 36 | echo "Docs have been updated, but the compiled docs have not been committed." 37 | echo "Run 'make generate', and commit the result to resolve this error." 38 | exit 1 39 | fi 40 | # Perform the Release 41 | - name: Checkout integration-release-action 42 | uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 43 | with: 44 | repository: hashicorp/integration-release-action 45 | path: ./integration-release-action 46 | - name: Notify Release 47 | uses: ./integration-release-action 48 | with: 49 | # The integration identifier will be used by the Packer team to register the integration 50 | # the expected format is packer// 51 | integration_identifier: "packer/veertuinc/veertu-anka" 52 | release_version: ${{ needs.strip-version.outputs.packer-version }} 53 | release_sha: ${{ github.ref }} 54 | github_token: ${{ secrets.GITHUB_TOKEN }} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | example.json 2 | crash.log 3 | packer-plugin-veertu-anka 4 | mapstructure-to-hcl2 5 | dist/ 6 | pkg 7 | bin 8 | packer-plugin* 9 | 10 | # generated code 11 | .gon.hcl 12 | 13 | # The following was created by https://www.gitignore.io 14 | 15 | ### Vagrant ### 16 | .vagrant/ 17 | 18 | 19 | ### Go ### 20 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 21 | *.o 22 | *.a 23 | *.so 24 | 25 | # Folders 26 | _obj 27 | _test 28 | 29 | # Architecture specific extensions/prefixes 30 | *.[568vq] 31 | [568vq].out 32 | 33 | *.cgo1.go 34 | *.cgo2.c 35 | _cgo_defun.c 36 | _cgo_gotypes.go 37 | _cgo_export.* 38 | 39 | _testmain.go 40 | 41 | *.exe 42 | *.test 43 | *.prof 44 | 45 | .DS_Store 46 | install-anka-packer*.log 47 | 48 | docs.zip -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | env: 4 | - CGO_ENABLED=0 5 | before: 6 | hooks: 7 | - make go.lint 8 | - make go.test 9 | - make go.hcl2spec 10 | - make install-packer-sdc 11 | 12 | builds: 13 | # A separated build to run the packer-plugins-check only once for a linux_amd64 binary 14 | - id: build1 15 | mod_timestamp: '{{ .CommitTimestamp }}' 16 | hooks: 17 | post: 18 | - cmd: bash -c "chmod +x {{ .ProjectName }}_v{{ .Version }}_{{ .Env.PACKER_CI_PROJECT_API_VERSION }}_{{ .Os }}_{{ .Arch }}" 19 | dir: "{{ dir .Path }}" 20 | - cmd: bash -c "make install && packer plugins installed" 21 | - cmd: bash -c "cp -rfp {{ dir .Path }}/{{ .ProjectName }}_v{{ .Version }}_{{ .Env.PACKER_CI_PROJECT_API_VERSION }}_{{ .Os }}_{{ .Arch }} ./{{ .ProjectName }}" 22 | - cmd: bash -c "make validate-examples" 23 | - cmd: bash -c "packer-sdc plugin-check {{ .ProjectName }}" 24 | - cmd: bash -c "rm -f ./{{ .ProjectName }}" 25 | flags: 26 | - -trimpath #removes all file system paths from the compiled executable 27 | ldflags: 28 | - '-s -w -X main.version={{ .Version }} -X main.commit={{ .Commit }}' 29 | goos: 30 | - darwin 31 | goarch: 32 | - arm64 33 | binary: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.PACKER_CI_PROJECT_API_VERSION }}_{{ .Os }}_{{ .Arch }}' 34 | no_unique_dist_dir: true 35 | - id: build2 36 | mod_timestamp: '{{ .CommitTimestamp }}' 37 | flags: 38 | - -trimpath #removes all file system paths from the compiled executable 39 | ldflags: 40 | - '-s -w -X main.version={{ .Version }} -X main.commit={{ .Commit }}' 41 | goos: 42 | - linux 43 | goarch: 44 | - amd64 45 | binary: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.PACKER_CI_PROJECT_API_VERSION }}_{{ .Os }}_{{ .Arch }}' 46 | no_unique_dist_dir: true 47 | - id: build3 48 | mod_timestamp: '{{ .CommitTimestamp }}' 49 | flags: 50 | - -trimpath #removes all file system paths from the compiled executable 51 | ldflags: 52 | - '-s -w -X main.version={{ .Version }} -X main.commit={{ .Commit }}' 53 | goos: 54 | - darwin 55 | goarch: 56 | - amd64 57 | binary: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.PACKER_CI_PROJECT_API_VERSION }}_{{ .Os }}_{{ .Arch }}' 58 | no_unique_dist_dir: true 59 | archives: 60 | - format: zip 61 | files: 62 | - none* 63 | name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Env.PACKER_CI_PROJECT_API_VERSION }}_{{ .Os }}_{{ .Arch }}' 64 | # checksum: 65 | # name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 66 | # algorithm: sha256 67 | # signs: 68 | # - artifacts: checksum 69 | # args: 70 | # # if you are using this is in a GitHub action or some other automated pipeline, you 71 | # # need to pass the batch flag to indicate its not interactive. 72 | # - "--batch" 73 | # - "--local-user" 74 | # - "{{ .Env.GPG_FINGERPRINT }}" 75 | # - "--output" 76 | # - "${signature}" 77 | # - "--detach-sign" 78 | # - "${artifact}" 79 | release: 80 | # If you want to manually examine the release before its live, uncomment this line: 81 | # draft: true 82 | # As part of the release doc files are included as a separate deliverable for consumption by Packer.io. 83 | # To include a separate docs.zip uncomment the extra_files config and the docs.zip command hook above. 84 | #extra_files: 85 | #- glob: ./docs.zip 86 | disable: true 87 | 88 | changelog: 89 | disable: true 90 | -------------------------------------------------------------------------------- /.web-docs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Packer](https://www.packer.io/) Plugin for building images that work with [Veertu's Anka macOS Virtualization tool](https://veertu.com/). 2 | 3 | ### Installation 4 | 5 | To install this plugin, copy and paste this code into your Packer configuration, then run [`packer init`](https://www.packer.io/docs/commands/init). 6 | 7 | ```hcl 8 | packer { 9 | required_plugins { 10 | veertu-anka = { 11 | version = "= v4.0.0" 12 | source = "github.com/veertuinc/veertu-anka" 13 | } 14 | } 15 | } 16 | ``` 17 | 18 | Alternatively, you can use `packer plugins install` to manage installation of this plugin. 19 | 20 | ```sh 21 | $ packer plugins install github.com/veertuinc/veertu-anka 22 | ``` 23 | 24 | ### Components 25 | ~> For use with the post-processor, it's important to use `anka registry add` to [set your default registry on the machine building your templates/tags](https://docs.veertu.com/anka/apple/command-line-reference/#registry-add). 26 | 27 | #### Builders 28 | - [veertu-anka-vm-clone](/packer/integrations/veertuinc/veertu-anka/latest/components/builder/clone) - Packer builder is able to clone existing Anka VM Templates for use with the [Anka Virtualization](https://veertu.com/technology/) package and the [Anka Build Cloud](https://veertu.com/anka-build/). The builder takes a source VM name, clones it, and then runs any provisioning necessary on the new VM Template before stopping or suspending it. 29 | - [veertu-anka-vm-create- ](/packer/integrations/veertuinc/veertu-anka/latest/components/builder/create) Packer builder is able to create new Anka VM Templates for use with the 30 | [Anka Virtualization](https://veertu.com/technology/) package and the [Anka Build Cloud](https://veertu.com/anka-build/). The builder takes the path to macOS installer .app 31 | and installs that macOS version inside of an Anka VM Template. 32 | 33 | #### Post-Processors 34 | - [veertu-anka-registry-push](/packer/integrations/veertuinc/veertu-anka/latest/components/post-processor/veertu-anka-registry-push) Packer Post Processor is able to push your created Anka VM templates to 35 | the [Anka Build Cloud Registry](https://veertu.com/anka-build/) through the [Anka Virtualization](https://veertu.com/technology/) package. 36 | -------------------------------------------------------------------------------- /.web-docs/components/builder/vm-clone/README.md: -------------------------------------------------------------------------------- 1 | Type: `veertu-anka-vm-clone` 2 | 3 | The `veertu-anka-vm-clone` Packer builder is able to clone existing Anka VM Templates for use with the [Anka Virtualization](https://veertu.com/technology/) package and the [Anka Build Cloud](https://veertu.com/anka-build/). The builder takes a source VM name, clones it, and then runs any provisioning necessary on the new VM Template before stopping or suspending it. 4 | 5 | The builder does _not_ manage templates. Once a template is created, it is up 6 | to you to use it or delete it. 7 | 8 | ## Important Notes 9 | 10 | **In Anka 3.0** we now require a tagged source VM before cloning in order to share the underlying .ank image and optimize disk space. If your source VM is not tagged yet, we will assign one . **We highly recommend pushing this VM Template/Tag to your registry so [disk usage is optimized](https://docs.veertu.com/anka/apple/getting-started/creating-your-first-vm/#disk-optimization).** 11 | 12 | ## Configuration Reference 13 | 14 | There are many configuration options available for the builder. They are segmented below into two categories: required and optional parameters. 15 | 16 | ### _**Required Configuration**_ 17 | 18 | * `source_vm_name` (String) The VM to clone for provisioning, either stopped or suspended. 19 | 20 | * `type` (String) Must be `veertu-anka-vm-clone`. 21 | 22 | ### _**Optional Configuration**_ 23 | 24 | * `vm_name` (String) The name for the VM that is created. 25 | 26 | > Generated using the source_vm_name if not provided: (`{{ source_vm_name }}-{10RandomChars}`). 27 | 28 | * `vcpu_count` (String) The number of vCPU cores, defaults to `2`. 29 | 30 | * `ram_size` (String) The size in "[0-9]+G" format, defaults to `2G`. 31 | 32 | * `disk_size` (String) The size in "[0-9]+G" format, defaults to `25G`. 33 | 34 | > We will automatically resize the internal disk for you by executing `diskutil apfs resizeContainer disk0s2 0` inside of the VM. 35 | 36 | * `stop_vm` (Boolean) Whether or not to stop the vm after it has been created, defaults to false. 37 | 38 | * `display_controller` (string) The display controller to set (run `anka modify VMNAME set display --help` to see available options). 39 | 40 | * `always_fetch` (Boolean) Always pull the source VM from the registry. Defaults to false. 41 | 42 | * `boot_delay` (String) The time to wait before running packer provisioner commands, defaults to `7s`. 43 | 44 | * `cacert` (String) Path to a CA Root certificate. 45 | 46 | * `cert` (String) Path to your node's client certificate to use for registry communication (if certificate authorization is enabled). 47 | 48 | * `insecure` (Boolean) Skip TLS verification. 49 | 50 | * `key` (String) Path to your node's client certificate key, if the `cert` certificate doesn't contain one, to use for registry communication (if certificate authorization is enabled). 51 | 52 | * `hw_uuid` (String) (Anka 2 only) The Hardware UUID you wish to set (usually generated with `uuidgen`). 53 | 54 | * `port_forwarding_rules` (Struct) 55 | 56 | > If port forwarding rules are already set and you want to not have them fail the packer build, use `packer build --force`. 57 | 58 | * `port_forwarding_guest_port` (Int) 59 | * `port_forwarding_host_port` (Int) 60 | * `port_forwarding_rule_name` (String) 61 | 62 | * `registry-path` (String) The registry URL (will use your default configuration if not set). 63 | 64 | * `remote` (String) The registry name (will use your default configuration if not set). 65 | 66 | > This takes priority in Anka 3 and `registry-path` will be ignored. 67 | 68 | * `source_vm_tag` (String) Specify the tag of the VM we want to clone instead of using the default. Also the tag to target when pulling from the registry (defaults to latest tag). 69 | 70 | * `update_addons` (Boolean) (Anka 2 only) Update the vm addons. Defaults to false. 71 | 72 | * `use_anka_cp` (Boolean) Use built in anka cp command. Defaults to false. 73 | 74 | ## Example 75 | 76 | Here is an example that uses the file and shell provisioners. 77 | 78 | ```hcl 79 | 80 | variable "source_vm_name" { 81 | type = string 82 | default = "anka-packer-base-macos" 83 | } 84 | 85 | variable "vm_name" { 86 | type = string 87 | default = "anka-packer-from-source" 88 | } 89 | 90 | source "veertu-anka-vm-clone" "clone" { 91 | vm_name = "${var.vm_name}" 92 | source_vm_name = "${var.source_vm_name}" 93 | } 94 | 95 | build { 96 | sources = [ 97 | "source.veertu-anka-vm-clone.clone", 98 | ] 99 | provisioner "file" { 100 | destination = "/private/tmp/" 101 | source = "./examples/ansible" 102 | } 103 | provisioner "shell" { 104 | inline = [ 105 | "[[ ! -d /tmp/ansible ]] && exit 100", 106 | "touch /tmp/ansible/test1" 107 | ] 108 | } 109 | provisioner "file" { 110 | destination = "./" 111 | direction = "download" 112 | source = "/private/tmp/ansible/test1" 113 | } 114 | provisioner "shell-local" { 115 | inline = [ 116 | "[[ ! -f ./test1 ]] && exit 200", 117 | "rm -f ./test1" 118 | ] 119 | } 120 | } 121 | 122 | ``` 123 | -------------------------------------------------------------------------------- /.web-docs/components/builder/vm-create/README.md: -------------------------------------------------------------------------------- 1 | Type: `veertu-anka-vm-create` 2 | 3 | **Packer 3.x will no longer support Anka 2.x. You can still however use the Packer 2.x release for support.** 4 | 5 | The `veertu-anka-vm-create` Packer builder is able to create new Anka VM Templates for use with the 6 | [Anka Virtualization](https://veertu.com/technology/) package and the [Anka Build Cloud](https://veertu.com/anka-build/). The builder takes the path to macOS installer .app 7 | and installs that macOS version inside of an Anka VM Template. 8 | 9 | The builder does _not_ manage templates. Once a template is created, it is up 10 | to you to use it or delete it. 11 | 12 | ## Configuration Reference 13 | 14 | There are many configuration options available for the builder. They are 15 | segmented below into two categories: required and optional parameters. 16 | 17 | ### Required Configuration 18 | 19 | * `installer` (String) The path to a macOS installer. This process takes about 20 minutes. 20 | - Starting in 3.1.2: This can also be set to 'latest' or a specific macOS version in order to have Anka attempt downloading the installer for you (`vm_name` will be set to `anka-packer-base-${installer}`). 21 | 22 | * `type` (String) Must be `veertu-anka-vm-create`. 23 | 24 | ### Optional Configuration 25 | 26 | * `vm_name` (String) The name for the VM that is created. One is generated with installer data if not provided (`anka-packer-base-{{ installer.OSVersion }}-{{ installer.BundlerVersion }}`). 27 | 28 | * `vcpu_count` (String) The number of vCPU cores, defaults to `2`. 29 | 30 | > This change gears us up for Anka 3.0 release when cpu_count will be vcpu_count. For now this is still CPU and not vCPU. 31 | 32 | * `ram_size` (String) The size in "[0-9]+G" format, defaults to `4G`. 33 | 34 | * `disk_size` (String) The size in "[0-9]+G" format, defaults to `40G`. 35 | 36 | > We will automatically resize the internal disk for you by executing `diskutil apfs resizeContainer disk0s2 0` inside of the VM 37 | 38 | * `stop_vm` (Boolean) Whether or not to stop the vm after it has been created, defaults to false. 39 | 40 | * `use_anka_cp` (Boolean) Use built in anka cp command. You shouldn't need this option. Defaults to false. 41 | 42 | * `anka_password` (String) Sets the password for the vm. Can also be set with `ANKA_DEFAULT_PASSWD` env var. Defaults to `admin`. 43 | 44 | * `anka_user` (String) Sets the username for the vm. Can also be set with `ANKA_DEFAULT_USER` env var. Defaults to `anka`. 45 | 46 | * `boot_delay` (String) The time to wait before running packer provisioner commands, defaults to `7s`. 47 | 48 | * `log_level` (String) The log level for Anka. This currently only supports `debug` and is only useful for VM creation failures. 49 | 50 | * `hw_uuid` (String) (Anka 2 only) The Hardware UUID you wish to set (usually generated with `uuidgen`). 51 | 52 | * `port_forwarding_rules` (Struct) 53 | 54 | > If port forwarding rules are already set and you want to not have them fail the packer build, use `packer build --force`. 55 | 56 | * `port_forwarding_guest_port` (Int) 57 | * `port_forwarding_host_port` (Int) 58 | * `port_forwarding_rule_name` (String) 59 | 60 | * `display_controller` (string) The display controller to set (run `anka modify VMNAME set display --help` to see available options). 61 | 62 | ## Example 63 | 64 | Here is an example: 65 | 66 | ```hcl 67 | 68 | variable "vm_name" { 69 | type = string 70 | default = "anka-packer-base-macos" 71 | } 72 | 73 | variable "installer" { 74 | type = string 75 | default = "/Applications/Install macOS Big Sur.app/" 76 | } 77 | 78 | variable "vcpu_count" { 79 | type = string 80 | default = "" 81 | } 82 | 83 | source "veertu-anka-vm-create" "base" { 84 | installer = "${var.installer}" 85 | vm_name = "${var.vm_name}" 86 | vcpu_count = "${var.vcpu_count}" 87 | } 88 | 89 | build { 90 | sources = [ 91 | "source.veertu-anka-vm-create.base" 92 | ] 93 | 94 | provisioner "shell" { 95 | inline = [ 96 | "echo hello world", 97 | "echo llamas rock" 98 | ] 99 | } 100 | } 101 | 102 | ``` 103 | -------------------------------------------------------------------------------- /.web-docs/components/post-processor/anka-registry-push/README.md: -------------------------------------------------------------------------------- 1 | Type: `veertu-anka-registry-push` 2 | 3 | The `veertu-anka-registry-push` Packer Post Processor is able to push your created Anka VM templates to the [Anka Build Cloud Registry](https://veertu.com/anka-build/) through the [Anka Virtualization](https://veertu.com/technology/) package. 4 | 5 | This post-processor is part of the [Veertu Anka plugin](https://github.com/veertuinc/packer-plugin-veertu-anka). To install this plugin using `packer init`, add the following Packer block to your hcl template: 6 | 7 | ```hcl 8 | packer { 9 | required_plugins { 10 | veertu-anka = { 11 | version = "= 3.2.0" 12 | source = "github.com/veertuinc/veertu-anka" 13 | } 14 | } 15 | } 16 | ``` 17 | 18 | ## Configuration Reference 19 | 20 | There are many configuration options available for the post-processor. They are 21 | segmented below into two categories: required and optional parameters. 22 | 23 | ### _**Required Configuration**_ 24 | 25 | * `type` (String) **Must be `veertu-anka-registry-push`; no other types** 26 | 27 | ### _**Optional Configuration**_ 28 | 29 | * `cacert` (String) Path to a CA Root certificate. 30 | 31 | * `cert` (String) Path to your node certificate (if certificate authority is enabled). 32 | 33 | * `description` (String) The description of the tag. 34 | 35 | * `insecure` (Boolean) Skip TLS verification. 36 | 37 | * `key` (String) Path to your node certificate key if the client/node certificate doesn't contain one. 38 | 39 | * `local` (Boolean) Assign a tag to your local template and avoid pushing to the Registry. 40 | 41 | * `remote` (String) The registry URL or name to target for registry operation (will use your default configuration if not set). 42 | 43 | * `remote_vm` (String) The name of a registry template you want to push the local template onto. 44 | 45 | * `tag` (String) The name of the tag to push (will default as 'latest' if not set). 46 | 47 | * `force` (Boolean) Whether or not to forcefully push, regardless of a tag already existing 48 | 49 | ## Other 50 | 51 | When using `packer build -force`, the post-processor will issue a [revert API call](https://docs.veertu.com/anka/anka-build-cloud/working-with-registry-and-api/#revert) to remove the existing tag before pushing the new. 52 | 53 | ## Example 54 | 55 | Here is an example that uses the file and shell provisioners. 56 | 57 | ```hcl 58 | 59 | variable "source_vm_name" { 60 | type = string 61 | default = "anka-packer-base-macos" 62 | } 63 | 64 | variable "vm_name" { 65 | type = string 66 | default = "anka-packer-from-source" 67 | } 68 | 69 | source "veertu-anka-vm-clone" "clone" { 70 | vm_name = "${var.vm_name}" 71 | source_vm_name = "${var.source_vm_name}" 72 | } 73 | 74 | build { 75 | sources = [ 76 | "source.veertu-anka-vm-clone.clone", 77 | ] 78 | 79 | provisioner "file" { 80 | destination = "/private/tmp/" 81 | source = "./examples/ansible" 82 | } 83 | provisioner "shell" { 84 | inline = [ 85 | "[[ ! -d /tmp/ansible ]] && exit 100", 86 | "touch /tmp/ansible/test1" 87 | ] 88 | } 89 | 90 | post-processor "veertu-anka-registry-push" { 91 | tag = "v2" 92 | } 93 | } 94 | 95 | ``` 96 | -------------------------------------------------------------------------------- /.web-docs/scripts/compile-to-webdocs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | # Converts the folder name that the component documentation file 7 | # is stored in into the integration slug of the component. 8 | componentTypeFromFolderName() { 9 | if [[ "$1" = "builders" ]]; then 10 | echo "builder" 11 | elif [[ "$1" = "provisioners" ]]; then 12 | echo "provisioner" 13 | elif [[ "$1" = "post-processors" ]]; then 14 | echo "post-processor" 15 | elif [[ "$1" = "datasources" ]]; then 16 | echo "data-source" 17 | else 18 | echo "" 19 | fi 20 | } 21 | 22 | # $1: The content to adjust links 23 | # $2: The organization of the integration 24 | rewriteLinks() { 25 | local result="$1" 26 | local organization="$2" 27 | 28 | urlSegment="([^/]+)" 29 | urlAnchor="(#[^/]+)" 30 | 31 | # Rewrite Component Index Page links to the Integration root page. 32 | # 33 | # (\1) (\2) (\3) 34 | # /packer/plugins/datasources/amazon#anchor-tag--> 35 | # /packer/integrations/hashicorp/amazon#anchor-tag 36 | local find="\(\/packer\/plugins\/$urlSegment\/$urlSegment$urlAnchor?\)" 37 | local replace="\(\/packer\/integrations\/$organization\/\2\3\)" 38 | result="$(echo "$result" | sed -E "s/$find/$replace/g")" 39 | 40 | 41 | # Rewrite Component links to the Integration component page 42 | # 43 | # (\1) (\2) (\3) (\4) 44 | # /packer/plugins/datasources/amazon/parameterstore#anchor-tag --> 45 | # /packer/integrations/{organization}/amazon/latest/components/datasources/parameterstore 46 | local find="\(\/packer\/plugins\/$urlSegment\/$urlSegment\/$urlSegment$urlAnchor?\)" 47 | local replace="\(\/packer\/integrations\/$organization\/\2\/latest\/components\/\1\/\3\4\)" 48 | result="$(echo "$result" | sed -E "s/$find/$replace/g")" 49 | 50 | # Rewrite the Component URL segment from the Packer Plugin format 51 | # to the Integrations format 52 | result="$(echo "$result" \ 53 | | sed "s/\/datasources\//\/data-source\//g" \ 54 | | sed "s/\/builders\//\/builder\//g" \ 55 | | sed "s/\/post-processors\//\/post-processor\//g" \ 56 | | sed "s/\/provisioners\//\/provisioner\//g" \ 57 | )" 58 | 59 | echo "$result" 60 | } 61 | 62 | # $1: Docs Dir 63 | # $2: Web Docs Dir 64 | # $3: Component File 65 | # $4: The org of the integration 66 | processComponentFile() { 67 | local docsDir="$1" 68 | local webDocsDir="$2" 69 | local componentFile="$3" 70 | 71 | local escapedDocsDir="$(echo "$docsDir" | sed 's/\//\\\//g' | sed 's/\./\\\./g')" 72 | local componentTypeAndSlug="$(echo "$componentFile" | sed "s/$escapedDocsDir\///g" | sed 's/\.mdx//g')" 73 | 74 | # Parse out the Component Slug & Component Type 75 | local componentSlug="$(echo "$componentTypeAndSlug" | cut -d'/' -f 2)" 76 | local componentType="$(componentTypeFromFolderName "$(echo "$componentTypeAndSlug" | cut -d'/' -f 1)")" 77 | if [[ "$componentType" = "" ]]; then 78 | echo "Failed to process '$componentFile', unexpected folder name." 79 | echo "Documentation for components must be stored in one of:" 80 | echo "builders, provisioners, post-processors, datasources" 81 | exit 1 82 | fi 83 | 84 | 85 | # Calculate the location of where this file will ultimately go 86 | local webDocsFolder="$webDocsDir/components/$componentType/$componentSlug" 87 | mkdir -p "$webDocsFolder" 88 | local webDocsFile="$webDocsFolder/README.md" 89 | local webDocsFileTmp="$webDocsFolder/README.md.tmp" 90 | 91 | # Copy over the file to its webDocsFile location 92 | cp "$componentFile" "$webDocsFile" 93 | 94 | # Remove the Header 95 | local lastMetadataLine="$(grep -n -m 2 '^\-\-\-' "$componentFile" | tail -n1 | cut -d':' -f1)" 96 | cat "$webDocsFile" | tail -n +"$(($lastMetadataLine+2))" > "$webDocsFileTmp" 97 | mv "$webDocsFileTmp" "$webDocsFile" 98 | 99 | # Remove the top H1, as this will be added automatically on the web 100 | cat "$webDocsFile" | tail -n +3 > "$webDocsFileTmp" 101 | mv "$webDocsFileTmp" "$webDocsFile" 102 | 103 | # Rewrite Links 104 | rewriteLinks "$(cat "$webDocsFile")" "$4" > "$webDocsFileTmp" 105 | mv "$webDocsFileTmp" "$webDocsFile" 106 | } 107 | 108 | # Compiles the Packer SDC compiled docs folder down 109 | # to a integrations-compliant folder (web docs) 110 | # 111 | # $1: The directory of the plugin 112 | # $2: The directory of the SDC compiled docs files 113 | # $3: The output directory to place the web-docs files 114 | # $4: The org of the integration 115 | compileWebDocs() { 116 | local docsDir="$1/$2" 117 | local webDocsDir="$1/$3" 118 | 119 | echo "Compiling MDX docs in '$2' to Markdown in '$3'..." 120 | # Create the web-docs directory if it hasn't already been created 121 | mkdir -p "$webDocsDir" 122 | 123 | # Copy the README over 124 | cp "$docsDir/README.md" "$webDocsDir/README.md" 125 | 126 | # Process all MDX component files (exclude index files, which are unsupported) 127 | for file in $(find "$docsDir" | grep "$docsDir/.*/.*\.mdx" | grep --invert-match "index.mdx"); do 128 | processComponentFile "$docsDir" "$webDocsDir" "$file" "$4" 129 | done 130 | } 131 | 132 | compileWebDocs "$1" "$2" "$3" "$4" 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Veertu Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | WORKDIR := $(shell pwd) 2 | LATEST-GIT-SHA := $(shell git rev-parse HEAD) 3 | VERSION := $(shell cat VERSION) 4 | FLAGS := -X main.version=$(VERSION) -X main.commit=$(LATEST-GIT-SHA) 5 | BIN := packer-plugin-veertu-anka 6 | ARCH := $(shell arch) 7 | ifeq ($(ARCH), i386) 8 | ARCH = amd64 9 | endif 10 | PACKER_CI_PROJECT_API_VERSION?=$(shell go run . describe 2>/dev/null | jq -r '.api_version') 11 | OS_TYPE ?= $(shell uname -s | tr '[:upper:]' '[:lower:]') 12 | BIN_FULL ?= dist/$(BIN)_v$(VERSION)_$(PACKER_CI_PROJECT_API_VERSION)_$(OS_TYPE)_$(ARCH) 13 | HASHICORP_PACKER_PLUGIN_SDK_VERSION?=$(shell go list -m github.com/hashicorp/packer-plugin-sdk | cut -d " " -f2) 14 | export PATH := $(shell go env GOPATH)/bin:$(PATH) 15 | 16 | .PHONY: go.lint validate-examples go.test test clean anka.clean-images 17 | 18 | .DEFAULT_GOAL := help 19 | 20 | all: clean go.releaser anka.clean-images anka.clean-clones generate install 21 | 22 | #help: @ List available tasks on this project 23 | help: 24 | @grep -h -E '[a-zA-Z\.\-]+:.*?@ .*$$' $(MAKEFILE_LIST) | sort | tr -d '#' | awk 'BEGIN {FS = ":.*?@ "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 25 | 26 | #go.lint: @ Run `golangci-lint run` against the current code 27 | go.lint: 28 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sudo sh -s -- -b /usr/local/bin v1.40.1 29 | golangci-lint run --fast 30 | 31 | #go.test: @ Install modules, mockgen, generate mocks, and run `go test` against the current tests 32 | # removed: go mod tidy -go=1.16 && go mod tidy -go=1.17 33 | go.test: 34 | go mod tidy 35 | go install github.com/golang/mock/mockgen@v1.6.0 36 | mockgen -source=util/util.go -destination=mocks/util_mock.go -package=mocks 37 | mockgen -source=client/client.go -destination=mocks/client_mock.go -package=mocks 38 | go test -v builder/anka/*.go 39 | go test -v post-processor/ankaregistry/*.go 40 | 41 | #go.build: @ Run `go build` to generate the binary 42 | go.build: 43 | GOARCH=$(ARCH) go build $(RACE) -ldflags "$(FLAGS)" -o $(BIN_FULL) 44 | chmod +x dist/$(BIN)* 45 | 46 | #go.releaser @ Run goreleaser release --clean for current version 47 | go.releaser: 48 | git tag -d "$(VERSION)" 2>/dev/null || true 49 | git tag -a "$(VERSION)" -m "Version $(VERSION)" 50 | echo "LATEST TAG: $$(git describe --tags --abbrev=0)" 51 | PACKER_CI_PROJECT_API_VERSION=$(PACKER_CI_PROJECT_API_VERSION) goreleaser release --verbose --clean 52 | 53 | #validate-examples: @ Run `packer validate` against example packer definitions using the built package 54 | validate-examples: 55 | cp -rfp $(WORKDIR)/examples /tmp/ 56 | for file in $$(ls $(WORKDIR)/examples/ | grep hcl); do echo $$file; packer validate $(WORKDIR)/examples/$$file; done 57 | 58 | #install: @ run packer plugin install 59 | install: 60 | pwd; 61 | ls -laht ./ 62 | ls -alht $(WORKDIR)/$(BIN_FULL) 63 | packer plugins install --path $(WORKDIR)/$(BIN_FULL) "github.com/veertuinc/veertu-anka" 64 | 65 | #uninstall: @ run packer plugin uninstaller 66 | uninstall: 67 | packer plugins remove github.com/veertuinc/veertu-anka || true 68 | 69 | #build-and-install: @ Run make targets to setup the initialize the binary 70 | build-and-install: 71 | $(MAKE) clean 72 | $(MAKE) go.build 73 | $(MAKE) go.hcl2spec 74 | $(MAKE) install 75 | 76 | #build-linux: @ Run go.build for Linux 77 | build-linux: 78 | GOOS=linux OS_TYPE=linux $(MAKE) go.build 79 | 80 | #build-mac: @ Run go.build for macOS 81 | build-mac: 82 | GOOS=darwin OS_TYPE=darwin $(MAKE) go.build 83 | 84 | #create-test: @ Run `packer build` with the default .pkr.hcl file 85 | create-test: lint install 86 | PACKER_LOG=1 packer build $(WORKDIR)/examples/create-from-installer.pkr.hcl 87 | 88 | #clean: @ Remove the plugin binary 89 | clean: 90 | $(MAKE) uninstall 91 | rm -f docs.zip 92 | rm -rf dist 93 | 94 | #anka.clean-images: @ Remove all anka images with `anka delete` 95 | anka.clean-images: 96 | anka --machine-readable list | jq -r '.body[].name' | grep anka-packer | xargs -n1 anka delete --yes 97 | 98 | #anka.clean-clones: @ Remove all anka clones with `anka delete` 99 | anka.clean-clones: 100 | anka --machine-readable list | jq -r '.body[].name' | grep anka-packer | grep -v base | xargs -n1 anka delete --yes 101 | 102 | #anka.wipe-anka: @ Remove all anka images from the local library 103 | anka.wipe-anka: 104 | -rm -rf ~/Library/Application\ Support/Veertu 105 | -rm -rf ~/.anka 106 | 107 | #install-packer-sdc: @ Install the hashicorp packer sdc 108 | install-packer-sdc: ## Install packer sofware development command 109 | @go install github.com/hashicorp/packer-plugin-sdk/cmd/packer-sdc@${HASHICORP_PACKER_PLUGIN_SDK_VERSION} 110 | 111 | #go.hcl2spec: @ Run `go generate` to generate hcl2 config specs 112 | go.hcl2spec: install-packer-sdc 113 | GOOS=$(OS_TYPE) go generate builder/anka/config.go 114 | GOOS=$(OS_TYPE) go generate post-processor/ankaregistry/post-processor.go 115 | 116 | #generate: @ Generate 117 | generate: install-packer-sdc go.hcl2spec 118 | @rm -rf .docs 119 | @packer-sdc renderdocs -src docs -partials docs-partials/ -dst .docs/ 120 | @./.web-docs/scripts/compile-to-webdocs.sh "." ".docs" ".web-docs" "veertuinc" 121 | @rm -r ".docs" 122 | 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Packer Plugin for Anka 2 | 3 | This is a [Packer](https://www.packer.io/) Plugin for building images that work with [Veertu's Anka macOS Virtualization tool](https://veertu.com/). 4 | 5 | - For use with the post-processor, it's important to use `anka registry add` to [set your default registry on the machine building your templates/tags](https://docs.veertu.com/anka/apple/command-line-reference/#registry-add). 6 | 7 | 8 | ## Compatibility 9 | 10 | | Packer Version | Anka Packer Plugin Version | 11 | | --- | --- | 12 | | below 1.7.0 | < 1.8.0 | 13 | | 1.7.0 and above | >= 2.0.0 | 14 | | >= 1.11.0 | >= v4.0.0 | 15 | 16 | | Packer Plugin Version | Anka CLI Version | 17 | | --- | --- | 18 | | 2.x | 2.x | 19 | | >= 3.x | >= 3.0.0 | 20 | 21 | ## Installing with `packer init` 22 | 23 | 1. Add a packer block to your .pkr.hcl like this: 24 | 25 | ``` 26 | packer { 27 | required_plugins { 28 | veertu-anka = { 29 | version = "= v4.0.0" 30 | source = "github.com/veertuinc/veertu-anka" 31 | } 32 | } 33 | } 34 | ``` 35 | 36 | 2. Then run `packer init {HCL file name}` 37 | 3. Run your `packer build` command with your hcl template 38 | 39 | ## Installing from Binary 40 | 41 | 1. [Install Packer v1.8 or newer](https://www.packer.io/downloads). 42 | 2. [Install Veertu Anka](https://veertu.com/download-anka-build/). 43 | 3. Download the [latest release](https://github.com/veertuinc/packer-plugin-veertu-anka/releases) for your host environment. 44 | 4. Unzip the plugin binaries to a location where Packer will detect them at run-time, such as any of the following: 45 | * The directory where the packer binary is. 46 | * The `~/.config/packer/plugins` directory. 47 | * The current working directory. 48 | 5. Rename the binary file to `packer-plugin-veertu-anka`. 49 | 6. Run your `packer build` command with your hcl template. 50 | 51 | ## Documentation 52 | 53 | | Builders | Post Processors | 54 | | --- | --- | 55 | | [[ veertu-anka-vm-create ]](./docs/builders/vm-create.mdx) | [[ veertu-anka-registry-push ]](./docs/post-processors/anka-registry-push.mdx) | 56 | | [[ veertu-anka-vm-clone ]](./docs/builders/vm-clone.mdx) | | 57 | 58 | ## Usage 59 | 60 | > Currently file provisioners do not support ~ or \$HOME in the destination paths. Please use absolute or relative paths. 61 | 62 | The most basic pkr.hcl file you can build from is: 63 | 64 | ```hcl 65 | source "veertu-anka-vm-create" "anka-packer-base-macos" { 66 | installer = "/Applications/Install macOS Big Sur.app/" 67 | vm_name = "anka-packer-base-macos" 68 | } 69 | 70 | build { 71 | sources = [ 72 | "source.veertu-anka-vm-create.anka-packer-base-macos" 73 | ] 74 | 75 | post-processor "veertu-anka-registry-push" { 76 | tag = "veertu-registry-push-test" 77 | } 78 | } 79 | ``` 80 | 81 | This will create a "base" VM template using the `.app` or `.ipsw` you specified in `installer = "/Applications/Install macOS Big Sur.app/"` with the name `anka-packer-base-macos`. Once the VM has been successfully created, it will push that VM to your default registry with the tag `veertu-registry-push-test`. 82 | 83 | > If you don't specify `vm_name`, we will obtain it from the installer and create a name like `anka-packer-base-12.6-21G115`. 84 | 85 | > **However, hw_uuid, port_forwarding_rules, and several other configuration settings are ignored for the created "base" vm.** We recommend using the `veertu-anka-vm-clone` builder to modify these values. 86 | 87 | You can also skip the creation of the base VM template and use an existing VM template: 88 | 89 | ```hcl 90 | source "veertu-anka-vm-clone" "anka-packer-from-source" { 91 | vm_name = "anka-packer-from-source" 92 | source_vm_name = "anka-packer-base-macos" 93 | always_fetch = true 94 | } 95 | 96 | build { 97 | sources = [ 98 | "source.veertu-anka-vm-clone.anka-packer-from-source", 99 | ] 100 | } 101 | ``` 102 | 103 | This will check to see if the VM template/tag exists locally, and if not, pull it from the registry: 104 | 105 | ```bash 106 | ❯ PKR_VAR_source_vm_tag="v1" PACKER_LOG=1 packer build -var 'source_vm_name=anka-packer-base-macos' examples/clone-existing-with-port-forwarding-rules.pkr.hcl 107 | . . . 108 | 2021/04/07 14:11:52 packer-plugin-veertu-anka plugin: 2021/04/07 14:11:52 Searching for anka-packer-base-macos locally... 109 | 2021/04/07 14:11:52 packer-plugin-veertu-anka plugin: 2021/04/07 14:11:52 Executing anka --machine-readable show anka-packer-base-macos 110 | 2021/04/07 14:11:53 packer-plugin-veertu-anka plugin: 2021/04/07 14:11:53 Could not find anka-packer-base-macos locally, looking in anka registry... 111 | 2021/04/07 14:11:53 packer-plugin-veertu-anka plugin: 2021/04/07 14:11:53 Executing anka --machine-readable registry pull --tag v1 anka-packer-base-macos 112 | ``` 113 | 114 | > Within your `.pkrvars.hcl` files, you can utilize `variable` blocks and then assign them values using the command line `packer build -var 'foo=bar'` or as environment variables `PKR_VAR_foo=bar` https://www.packer.io/docs/templates/hcl_templates/variables#assigning-values-to-build-variables 115 | 116 | This will clone `anka-packer-base-macos` to a new VM and, if there are set configurations, make them. 117 | 118 | > Check out the [examples directory](./examples) to see how port-forwarding and other options are used. 119 | 120 | ### Build Variables 121 | 122 | Packer allows for the exposure of build variables which connects information related to the artifact that was built. Those variables can then be accessed by `post-processors` and `provisioners`. 123 | 124 | The variables we expose are: 125 | 126 | * `VMName`: name of the artifact vm 127 | * `OSVersion`: OS version from which the artifact was created 128 | * eg. 10.15.7 129 | * `DarwinVersion`: Darwin version that is compatible with the current OS version 130 | * eg. 19.6.0 131 | 132 | ```hcl 133 | locals { 134 | source_vm_name = "anka-packer-base-11.2-16.4.06" 135 | } 136 | 137 | source "veertu-anka-vm-clone" "anka-macos-from-source" { 138 | "source_vm_name": "${local.source_vm_name}", 139 | "vm_name": "anka-macos-from-source" 140 | } 141 | 142 | build { 143 | sources = [ 144 | "source.veertu-anka-vm-clone.anka-macos-from-source" 145 | ] 146 | 147 | provisioner "shell" { 148 | inline = [ 149 | "echo vm_name is ${build.VMName}", 150 | "echo os_version is ${build.OSVersion}", 151 | "echo darwin_version is ${build.DarwinVersion}" 152 | ] 153 | } 154 | } 155 | ``` 156 | 157 | --- 158 | 159 | ## Development 160 | 161 | You will need a recent golang installed and setup. See `go.mod` for which version is expected. 162 | 163 | We use [gomock](https://github.com/golang/mock) to quickly and reliably mock our interfaces for testing. This allows us to easily test when we expect logic to be called without having to rewrite golang standard library functions with custom mock logic. To generate one of these mocked interfaces, installed the mockgen binary by following the link provided and then run the `make go.test`. 164 | 165 | - You must install `packer-sdc` to generate docs and HCL2spec. 166 | 167 | ### Run the plugin locally 168 | 169 | ```bash 170 | go run -ldflags="-X main.version=$(cat VERSION) -X main.commit=$(git rev-parse HEAD)" main.go 171 | ``` 172 | 173 | ### Building, Linting, and Testing 174 | 175 | ```bash 176 | make all && make install 177 | ``` 178 | 179 | 184 | 185 | When testing with an example HCL: 186 | 187 | ```bash 188 | ANKA_LOG_LEVEL=debug ANKA_DELETE_LOGS=0 PACKER_LOG=1 packer build examples/create-from-installer.pkr.hcl 189 | ``` 190 | 191 | To test the post processor you will need an active vpn connection that can reach an anka registry. You can setup an anka registry by either adding the registry locally with: 192 | 193 | ```bash 194 | anka registry add 195 | ``` 196 | 197 | -or- 198 | 199 | You can setup the `create-from-installer-with-post-processing.pkr.hcl` with the correct registry values and update the make target `anka.test` to use that .pkr.hcl file and run: 200 | 201 | ```bash 202 | make create-test 203 | ``` 204 | 205 | [Packer Builder]: https://www.packer.io/docs/extending/custom-builders.html 206 | [Veertu Anka]: https://veertu.com/ 207 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 4.0.0 -------------------------------------------------------------------------------- /VERSIONING.md: -------------------------------------------------------------------------------- 1 | # How our versioning works 2 | 3 | > We do **not** use a strict semantic versioning pattern 4 | 5 | We use a well known pattern: MAJOR.MINOR.PATCH 6 | 7 | MAJOR changes are for versions that break backwards compatibility 8 | MINOR are for new features and bug fixes, while maintaining backwards compatibility 9 | PATCH are for bug fixes only, while maintaining backwards compatibility 10 | 11 | For some projects, we add a short git-sha to the version string, denoted with a '-' (ex. 3.1.0-ab2d12). 12 | This appendix has no meaning for precedence or does not mean anything regarding changes implemented. -------------------------------------------------------------------------------- /builder/anka/artifact.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Artifact represents an Anka image as the result of a Packer build. 8 | type Artifact struct { 9 | vmName string 10 | vmId string 11 | StateData map[string]interface{} 12 | } 13 | 14 | // BuilderId returns the unique builder id. 15 | func (*Artifact) BuilderId() string { 16 | return BuilderId 17 | } 18 | 19 | // Destroy destroys the image represented by the artifact. 20 | func (a *Artifact) Destroy() error { 21 | return errors.New("Destroy not implemented") 22 | } 23 | 24 | // Files returns the files represented by the artifact. 25 | func (a *Artifact) Files() []string { 26 | return nil 27 | } 28 | 29 | // Id returns the VM UUID. 30 | func (a *Artifact) Id() string { 31 | return a.vmId 32 | } 33 | 34 | // State allows the caller to ask for builder specific state information 35 | // relating to the artifact instance. 36 | func (a *Artifact) State(name string) interface{} { 37 | return a.StateData[name] 38 | } 39 | 40 | // String returns the string representation of the artifact. 41 | func (a *Artifact) String() string { 42 | return a.vmName 43 | } 44 | -------------------------------------------------------------------------------- /builder/anka/builder.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/hashicorp/hcl/v2/hcldec" 10 | "github.com/hashicorp/packer-plugin-sdk/communicator" 11 | "github.com/hashicorp/packer-plugin-sdk/multistep" 12 | "github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps" 13 | "github.com/hashicorp/packer-plugin-sdk/packer" 14 | "github.com/hashicorp/packer-plugin-sdk/packerbuilderdata" 15 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 16 | "github.com/veertuinc/packer-plugin-veertu-anka/util" 17 | ) 18 | 19 | // BuilderId is the unique ID for this builder. 20 | const BuilderId = "packer.veertu-anka" 21 | 22 | // Builder represents a Packer Builder. 23 | type Builder struct { 24 | config *Config 25 | runner multistep.Runner 26 | } 27 | 28 | // Prepare processes the build configuration parameters. 29 | func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { 30 | generatedData := []string{"VMName", "OSVersion", "DarwinVersion"} 31 | 32 | c, errs := NewConfig(raws...) 33 | if errs != nil { 34 | return nil, nil, errs 35 | } 36 | b.config = c 37 | 38 | return generatedData, nil, nil 39 | } 40 | 41 | // Run executes an Anka Packer build and returns a packer.Artifact 42 | func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { 43 | ankaClient := &client.AnkaClient{} 44 | util := &util.AnkaUtil{} 45 | 46 | // Setup the state bag and initial state for the steps 47 | state := new(multistep.BasicStateBag) 48 | state.Put("config", b.config) 49 | state.Put("hook", hook) 50 | state.Put("ui", ui) 51 | state.Put("client", ankaClient) 52 | state.Put("util", util) 53 | 54 | generatedData := &packerbuilderdata.GeneratedData{State: state} 55 | 56 | steps := []multistep.Step{ 57 | &StepTempDir{}, 58 | } 59 | 60 | switch b.config.PackerConfig.PackerBuilderType { 61 | case "veertu-anka-vm-create": 62 | steps = append(steps, &StepCreateVM{}) 63 | case "veertu-anka-vm-clone": 64 | steps = append(steps, &StepCloneVM{}) 65 | default: 66 | return nil, errors.New("wrong type for builder. must be of type clone or create") 67 | } 68 | 69 | steps = append(steps, 70 | &StepStartVM{}, 71 | &communicator.StepConnect{ 72 | Config: &b.config.Comm, 73 | CustomConnect: map[string]multistep.Step{ 74 | "anka": &StepConnectAnka{}, 75 | }, 76 | Host: func(state multistep.StateBag) (string, error) { 77 | return "", errors.New("No host implemented for anka builder (which is ok)") 78 | }, 79 | }, 80 | &StepSetGeneratedData{ 81 | GeneratedData: generatedData, 82 | }, 83 | &commonsteps.StepProvision{}, 84 | ) 85 | 86 | // Run! 87 | b.runner = commonsteps.NewRunner(steps, b.config.PackerConfig, ui) 88 | b.runner.Run(ctx, state) 89 | 90 | // If there was an error, return that 91 | rawErr, ok := state.GetOk("error") 92 | if ok { 93 | return nil, rawErr.(error) 94 | } 95 | 96 | // If it was cancelled, then just return 97 | _, ok = state.GetOk(multistep.StateCancelled) 98 | if ok { 99 | return nil, nil 100 | } 101 | 102 | // Check we can describe the VM 103 | descr, err := ankaClient.Describe(state.Get("vm_name").(string)) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | license, err := ankaClient.License() 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | if license.LicenseType == "com.veertu.anka.develop" { 114 | log.Printf("developer license present, can only stop vms: https://docs.veertu.com/anka/licensing/#anka-license-feature-differences") 115 | b.config.StopVM = true 116 | } 117 | 118 | if b.config.StopVM { 119 | ui.Say(fmt.Sprintf("Stopping VM %s", descr.Name)) 120 | 121 | err := ankaClient.Stop(client.StopParams{VMName: descr.Name}) 122 | if err != nil { 123 | return nil, err 124 | } 125 | } else { 126 | ui.Say(fmt.Sprintf("Suspending VM %s", descr.Name)) 127 | 128 | err := ankaClient.Suspend(client.SuspendParams{VMName: descr.Name}) 129 | if err != nil { 130 | return nil, err 131 | } 132 | } 133 | 134 | // No errors, must've worked 135 | return &Artifact{ 136 | vmId: descr.UUID, 137 | vmName: descr.Name, 138 | StateData: map[string]interface{}{"generated_data": generatedData.State.Get("generated_data")}, 139 | }, nil 140 | } 141 | 142 | // ConfigSpec returns an HCL spec of the config 143 | func (b *Builder) ConfigSpec() hcldec.ObjectSpec { 144 | return b.config.FlatMapstructure().HCL2Spec() 145 | } 146 | -------------------------------------------------------------------------------- /builder/anka/builder_test.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func testConfig() map[string]interface{} { 8 | return map[string]interface{}{ 9 | "type": "veertu-anka-create-vm", 10 | "installer": "/Applications/Install macOS Big Sur.app", 11 | "disk_size": 80, 12 | "vm_name": "test-prepare-anka-create", 13 | } 14 | } 15 | 16 | func TestBuilderPrepare(t *testing.T) { 17 | var b Builder 18 | 19 | c := testConfig() 20 | 21 | if _, _, err := b.Prepare(c); err != nil { 22 | t.Fatalf("Unexpected error: %s", err) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /builder/anka/communicator.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "path" 12 | "path/filepath" 13 | 14 | "github.com/hashicorp/packer-plugin-sdk/packer" 15 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 16 | ) 17 | 18 | // Communicator initializes what is shared between anka and packer 19 | type Communicator struct { 20 | Config *Config 21 | Client client.Client 22 | HostDir string 23 | VMDir string 24 | VMName string 25 | FuseAvailable bool 26 | } 27 | 28 | // Start runs the actual anka commands 29 | func (c *Communicator) Start(ctx context.Context, remote *packer.RemoteCmd) error { 30 | log.Printf("Communicator Start: %s", remote.Command) 31 | 32 | runner := client.NewRunner(client.RunParams{ 33 | VMName: c.VMName, 34 | Command: []string{remote.Command}, 35 | Volume: "", 36 | Stdout: remote.Stdout, 37 | Stderr: remote.Stderr, 38 | Stdin: remote.Stdin, 39 | }) 40 | 41 | if err := runner.Start(); err != nil { 42 | return err 43 | } 44 | 45 | go func() { 46 | exitCode, err := runner.Wait() 47 | if err != nil { 48 | log.Printf("Runner exited with error: %v", err) 49 | } 50 | 51 | remote.SetExited(exitCode) 52 | }() 53 | 54 | return nil 55 | } 56 | 57 | // Upload uploads the source file to the destination 58 | func (c *Communicator) Upload(dst string, src io.Reader, fi *os.FileInfo) error { 59 | log.Printf("Uploading file to VM: %s", dst) 60 | 61 | tempfile, err := ioutil.TempFile(c.HostDir, "upload") 62 | if err != nil { 63 | return err 64 | } 65 | 66 | defer os.Remove(tempfile.Name()) 67 | defer tempfile.Close() 68 | 69 | log.Printf("Copying from reader to %s", tempfile.Name()) 70 | 71 | w, err := io.Copy(tempfile, src) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | if fi != nil { 77 | _ = tempfile.Chmod((*fi).Mode()) 78 | } 79 | 80 | if !c.FuseAvailable { 81 | err = c.Client.Copy(client.CopyParams{ 82 | Src: tempfile.Name(), 83 | Dst: c.VMName + ":" + dst, 84 | }) 85 | } else { 86 | _, err = c.Client.Run(client.RunParams{ 87 | VMName: c.VMName, 88 | Command: []string{"cp", path.Base(tempfile.Name()), dst}, 89 | Volume: c.HostDir, 90 | }) 91 | } 92 | 93 | log.Printf("Copied %d bytes from %s to %s", w, tempfile.Name(), dst) 94 | 95 | return err 96 | } 97 | 98 | // UploadDir uploads the source directory to the destination 99 | func (c *Communicator) UploadDir(dst string, src string, exclude []string) error { 100 | 101 | if c.FuseAvailable { 102 | td, err := ioutil.TempDir(c.HostDir, "dirupload") 103 | if err != nil { 104 | return err 105 | } 106 | 107 | defer os.RemoveAll(td) 108 | 109 | walkFn := func(path string, info os.FileInfo, err error) error { 110 | if err != nil { 111 | return err 112 | } 113 | 114 | relpath, err := filepath.Rel(src, path) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | hostpath := filepath.Join(td, relpath) 120 | 121 | if info.IsDir() { 122 | return os.MkdirAll(hostpath, info.Mode()) 123 | } 124 | 125 | if info.Mode()&os.ModeSymlink == os.ModeSymlink { 126 | dest, err := os.Readlink(path) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | return os.Symlink(dest, hostpath) 132 | } 133 | 134 | src, err := os.Open(path) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | defer src.Close() 140 | 141 | dst, err := os.Create(hostpath) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | defer dst.Close() 147 | 148 | log.Printf("Copying %s to %s", src.Name(), dst.Name()) 149 | if _, err := io.Copy(dst, src); err != nil { 150 | return err 151 | } 152 | 153 | si, err := src.Stat() 154 | if err != nil { 155 | return err 156 | } 157 | 158 | return dst.Chmod(si.Mode()) 159 | } 160 | 161 | err = filepath.Walk(src, walkFn) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | containerDst := dst 167 | if src[len(src)-1] != '/' { 168 | containerDst = filepath.Join(dst, filepath.Base(src)) 169 | } 170 | 171 | log.Printf("from %#v to %#v", td, containerDst) 172 | 173 | command := fmt.Sprintf("set -e; mkdir -p %s; command cp -R %s/* %s", 174 | containerDst, filepath.Base(td), containerDst, 175 | ) 176 | 177 | _, err = c.Client.Run(client.RunParams{ 178 | VMName: c.VMName, 179 | Command: []string{"bash", "-c", command}, 180 | Volume: c.HostDir, 181 | }) 182 | 183 | return err 184 | } 185 | 186 | return c.Client.Copy(client.CopyParams{ 187 | Src: src, 188 | Dst: c.VMName + ":" + dst, 189 | }) 190 | } 191 | 192 | // Download copies the file from the source to the destination 193 | func (c *Communicator) Download(src string, dst io.Writer) error { 194 | log.Printf("Downloading file from VM: %s", src) 195 | 196 | tempfile, err := ioutil.TempFile(c.HostDir, "download") 197 | if err != nil { 198 | return err 199 | } 200 | 201 | defer os.Remove(tempfile.Name()) 202 | defer tempfile.Close() 203 | 204 | if !c.FuseAvailable { 205 | err := c.Client.Copy(client.CopyParams{ 206 | Src: c.VMName + ":" + src, 207 | Dst: tempfile.Name(), 208 | }) 209 | if err != nil { 210 | return err 211 | } 212 | } 213 | 214 | log.Printf("Copying from %s to writer", tempfile.Name()) 215 | 216 | w, err := io.Copy(dst, tempfile) 217 | if err != nil { 218 | return err 219 | } 220 | 221 | if c.FuseAvailable { 222 | _, err := c.Client.Run(client.RunParams{ 223 | VMName: c.VMName, 224 | Command: []string{"cp", src, "./" + path.Base(tempfile.Name())}, 225 | Volume: c.HostDir, 226 | }) 227 | if err != nil { 228 | return err 229 | } 230 | } 231 | 232 | log.Printf("Copied %d bytes", w) 233 | 234 | return nil 235 | } 236 | 237 | // DownloadDir copies the source directory to the destination 238 | func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error { 239 | 240 | if c.FuseAvailable { 241 | return errors.New("communicator.DownloadDir isn't implemented") 242 | } 243 | 244 | return c.Client.Copy(client.CopyParams{ 245 | Src: c.VMName + ":" + src, 246 | Dst: dst, 247 | }) 248 | } 249 | -------------------------------------------------------------------------------- /builder/anka/config.go: -------------------------------------------------------------------------------- 1 | //go:generate packer-sdc mapstructure-to-hcl2 -type Config,PortForwardingRule 2 | 3 | package anka 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/common" 12 | "github.com/hashicorp/packer-plugin-sdk/communicator" 13 | "github.com/hashicorp/packer-plugin-sdk/packer" 14 | "github.com/hashicorp/packer-plugin-sdk/template/config" 15 | "github.com/hashicorp/packer-plugin-sdk/template/interpolate" 16 | "github.com/mitchellh/mapstructure" 17 | "github.com/veertuinc/packer-plugin-veertu-anka/util" 18 | ) 19 | 20 | const defaultBootDelay = "7s" 21 | 22 | // PortForwardingRule defines the requirements for port forwarding 23 | type PortForwardingRule struct { 24 | PortForwardingGuestPort int `mapstructure:"port_forwarding_guest_port"` 25 | PortForwardingHostPort int `mapstructure:"port_forwarding_host_port"` 26 | PortForwardingRuleName string `mapstructure:"port_forwarding_rule_name"` 27 | } 28 | 29 | // Config initializes the builders using mapstructure which decodes 30 | // generic map values from either the json or hcl2 config files provided 31 | type Config struct { 32 | common.PackerConfig `mapstructure:",squash"` 33 | Comm communicator.Config `mapstructure:",squash"` 34 | 35 | AnkaLogLevel string `mapstructure:"log_level"` 36 | AnkaUser string `mapstructure:"anka_user"` 37 | AnkaPassword string `mapstructure:"anka_password"` 38 | 39 | Installer string `mapstructure:"installer"` 40 | SourceVMName string `mapstructure:"source_vm_name"` 41 | SourceVMTag string `mapstructure:"source_vm_tag"` 42 | 43 | VMName string `mapstructure:"vm_name"` 44 | DiskSize string `mapstructure:"disk_size"` 45 | RAMSize string `mapstructure:"ram_size"` 46 | VCPUCount string `mapstructure:"vcpu_count"` 47 | 48 | AlwaysFetch bool `mapstructure:"always_fetch"` 49 | 50 | UpdateAddons bool `mapstructure:"update_addons"` 51 | 52 | Remote string `mapstructure:"remote"` 53 | NodeCertPath string `mapstructure:"cert"` 54 | NodeKeyPath string `mapstructure:"key"` 55 | CaRootPath string `mapstructure:"cacert"` 56 | IsInsecure bool `mapstructure:"insecure"` 57 | 58 | PortForwardingRules []PortForwardingRule `mapstructure:"port_forwarding_rules"` 59 | 60 | HWUUID string `mapstructure:"hw_uuid,omitempty"` 61 | BootDelay string `mapstructure:"boot_delay"` 62 | UseAnkaCP bool `mapstructure:"use_anka_cp"` 63 | DisplayController string `mapstructure:"display_controller,omitempty"` 64 | 65 | StopVM bool `mapstructure:"stop_vm"` 66 | 67 | HostArch string `mapstructure:"host_arch,omitempty"` 68 | 69 | ctx interpolate.Context //nolint:structcheck 70 | } 71 | 72 | // NewConfig generates a machine readable config from the generic map values above 73 | // and provides an inital setup values and scrubs the data for any mistakes 74 | func NewConfig(raws ...interface{}) (*Config, error) { 75 | var c Config 76 | util := util.AnkaUtil{} 77 | 78 | var md mapstructure.Metadata 79 | err := config.Decode(&c, &config.DecodeOpts{ 80 | Metadata: &md, 81 | Interpolate: true, 82 | }, raws...) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | if c.BootDelay == "" { 88 | c.BootDelay = defaultBootDelay 89 | } 90 | 91 | if c.AnkaPassword != "" { 92 | os.Setenv("ANKA_DEFAULT_PASSWD", c.AnkaPassword) 93 | } 94 | 95 | if c.AnkaUser != "" { 96 | os.Setenv("ANKA_DEFAULT_USER", c.AnkaUser) 97 | } 98 | 99 | if c.AnkaLogLevel == "debug" { // allow debug logging for anka; useful for 3.1 click script troubleshooting 100 | os.Setenv("ANKA_LOG_LEVEL", "debug") 101 | } 102 | 103 | var errs *packer.MultiError 104 | 105 | if c.Comm.Type == "" { 106 | c.Comm.Type = "anka" 107 | } 108 | 109 | c.HostArch = runtime.GOARCH 110 | 111 | if c.Installer == "" && c.SourceVMName == "" { 112 | errs = packer.MultiErrorAppend(errs, errors.New("installer or source_vm_name must be specified")) 113 | } 114 | 115 | if c.Installer != "" && c.SourceVMName != "" { 116 | errs = packer.MultiErrorAppend(errs, errors.New("cannot specify both an installer and source_vm_name")) 117 | } 118 | 119 | if c.SourceVMName != "" && strings.ContainsAny(c.SourceVMName, " \n") { 120 | errs = packer.MultiErrorAppend(errs, errors.New("source_vm_name name contains spaces")) 121 | } 122 | 123 | if len(c.PortForwardingRules) > 0 { 124 | for index, rule := range c.PortForwardingRules { 125 | if rule.PortForwardingGuestPort == 0 { 126 | errs = packer.MultiErrorAppend(errs, errors.New("guest port is required")) 127 | } 128 | if rule.PortForwardingRuleName == "" { 129 | c.PortForwardingRules[index].PortForwardingRuleName = util.RandSeq(10) 130 | } 131 | } 132 | } 133 | 134 | if errs != nil && len(errs.Errors) > 0 { 135 | return nil, errs 136 | } 137 | 138 | return &c, nil 139 | } 140 | -------------------------------------------------------------------------------- /builder/anka/step_clone_vm.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | 9 | "github.com/hashicorp/packer-plugin-sdk/multistep" 10 | "github.com/hashicorp/packer-plugin-sdk/packer" 11 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 12 | "github.com/veertuinc/packer-plugin-veertu-anka/common" 13 | "github.com/veertuinc/packer-plugin-veertu-anka/util" 14 | ) 15 | 16 | // StepCloneVM will be used to run the clone step for any 'vm-clone' builder types 17 | type StepCloneVM struct { 18 | client client.Client 19 | vmName string 20 | } 21 | 22 | // Run clones a vm from a source vm either from an anka registry or locally 23 | func (s *StepCloneVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 24 | config := state.Get("config").(*Config) 25 | ui := state.Get("ui").(packer.Ui) 26 | ankaUtil := state.Get("util").(util.Util) 27 | onError := func(err error) multistep.StepAction { 28 | return ankaUtil.StepError(ui, state, err) 29 | } 30 | 31 | doPull := config.AlwaysFetch 32 | 33 | sourceVMTag := fmt.Sprintf("%s tag", config.SourceVMTag) 34 | if config.SourceVMTag == "" { 35 | sourceVMTag = "latest tag" 36 | } 37 | 38 | s.client = state.Get("client").(client.Client) 39 | s.vmName = config.VMName 40 | 41 | if s.vmName == "" { 42 | s.vmName = fmt.Sprintf("%s-%s", config.SourceVMName, ankaUtil.RandSeq(10)) 43 | } 44 | 45 | state.Put("vm_name", s.vmName) 46 | 47 | if config.PackerForce { 48 | exists, err := s.client.Exists(s.vmName) 49 | if err != nil { 50 | return onError(err) 51 | } 52 | if exists { 53 | ui.Say(fmt.Sprintf("Deleting existing virtual machine %s", s.vmName)) 54 | 55 | err = s.client.Delete(client.DeleteParams{VMName: s.vmName}) 56 | if err != nil { 57 | return onError(err) 58 | } 59 | } 60 | } 61 | 62 | if !config.AlwaysFetch { 63 | log.Printf("Searching for %s locally...", config.SourceVMName) 64 | 65 | sourceExists, err := s.client.Exists(config.SourceVMName) 66 | if err != nil { 67 | return onError(err) 68 | } 69 | if !sourceExists { 70 | log.Printf("Could not find %s locally, looking in anka registry...", config.SourceVMName) 71 | 72 | doPull = true 73 | } 74 | } 75 | 76 | if doPull { 77 | ui.Say(fmt.Sprintf("Pulling source VM %s with %s from Anka Registry", config.SourceVMName, sourceVMTag)) 78 | 79 | registryParams := client.RegistryParams{ 80 | Remote: config.Remote, 81 | NodeCertPath: config.NodeCertPath, 82 | NodeKeyPath: config.NodeKeyPath, 83 | CaRootPath: config.CaRootPath, 84 | IsInsecure: config.IsInsecure, 85 | HostArch: config.HostArch, 86 | } 87 | 88 | registryPullParams := client.RegistryPullParams{ 89 | VMID: config.SourceVMName, 90 | Tag: config.SourceVMTag, 91 | Local: false, 92 | Shrink: false, 93 | } 94 | 95 | err := s.client.RegistryPull(registryParams, registryPullParams) 96 | if err != nil { 97 | return onError(fmt.Errorf("failed to pull vm %s with %s from registry (make sure to add it as the default: https://docs.veertu.com/anka/intel/command-line-reference/#registry-add)", config.SourceVMName, sourceVMTag)) 98 | } 99 | } 100 | 101 | sourceShow, err := s.client.Show(config.SourceVMName) 102 | if err != nil { 103 | return onError(err) 104 | } 105 | 106 | // Check arch of host 107 | // // If arm64, ensure the source has a local tag 108 | if !doPull { 109 | if sourceShow.Version == "" { 110 | if config.HostArch == "arm64" { 111 | ui.Say("Preparing source VM by creating a local tag (necessary in Anka 3 to optimize disk usage of clones)") 112 | pushParams := client.RegistryPushParams{ 113 | Tag: fmt.Sprintf("local-tag-%s", ankaUtil.RandSeq(10)), 114 | RemoteVM: "", 115 | Local: true, 116 | Force: false, 117 | VMID: config.SourceVMName, 118 | } 119 | s.client.RegistryPush(client.RegistryParams{HostArch: config.HostArch}, pushParams) 120 | } 121 | } 122 | } 123 | 124 | ui.Say(fmt.Sprintf("Cloning source VM %s into a new virtual machine: %s", sourceShow.Name, s.vmName)) 125 | 126 | err = s.client.Clone(client.CloneParams{VMName: s.vmName, SourceUUID: sourceShow.UUID}) 127 | if err != nil { 128 | return onError(err) 129 | } 130 | 131 | clonedShow, err := s.client.Show(s.vmName) 132 | if err != nil { 133 | return onError(err) 134 | } 135 | 136 | err = s.modifyVMResources(clonedShow, config, ui, ankaUtil) 137 | if err != nil { 138 | return onError(err) 139 | } 140 | 141 | err = s.modifyVMProperties(clonedShow, config, ui) 142 | if err != nil { 143 | return onError(err) 144 | } 145 | 146 | if config.UpdateAddons { 147 | ui.Say(fmt.Sprintf("Updating guest addons for %s", s.vmName)) 148 | 149 | err := s.client.UpdateAddons(s.vmName) 150 | if err != nil { 151 | return onError(err) 152 | } 153 | } 154 | 155 | return multistep.ActionContinue 156 | } 157 | 158 | // Cleanup will delete the vm if there happens to be an error and handle anything failed states 159 | func (s *StepCloneVM) Cleanup(state multistep.StateBag) { 160 | ui := state.Get("ui").(packer.Ui) 161 | 162 | log.Println("Cleaning up clone VM step") 163 | if s.vmName == "" { 164 | return 165 | } 166 | 167 | _, halted := state.GetOk(multistep.StateHalted) 168 | _, canceled := state.GetOk(multistep.StateCancelled) 169 | errorObj := state.Get("error") 170 | switch errorObj.(type) { 171 | case *common.VMAlreadyExistsError: 172 | return 173 | case *common.VMNotFoundException: 174 | return 175 | default: 176 | if halted || canceled { 177 | ui.Say(fmt.Sprintf("Deleting VM %s", s.vmName)) 178 | 179 | err := s.client.Delete(client.DeleteParams{VMName: s.vmName}) 180 | if err != nil { 181 | ui.Error(fmt.Sprint(err)) 182 | } 183 | 184 | return 185 | } 186 | } 187 | } 188 | 189 | func (s *StepCloneVM) modifyVMResources(showResponse client.ShowResponse, config *Config, ui packer.Ui, util util.Util) error { 190 | stopParams := client.StopParams{ 191 | VMName: showResponse.Name, 192 | } 193 | 194 | if config.DiskSize != "" { 195 | diskSizeBytes, err := util.ConvertDiskSizeToBytes(config.DiskSize) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | if diskSizeBytes > showResponse.HardDrive { 201 | err := s.client.Stop(stopParams) 202 | if err != nil { 203 | return err 204 | } 205 | 206 | ui.Say(fmt.Sprintf("Modifying VM %s disk size to %s", showResponse.Name, config.DiskSize)) 207 | 208 | err = s.client.Modify(showResponse.Name, "set", "hard-drive", "-s", config.DiskSize) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | // Resize the inner VM disk too with diskutil 214 | _, err = s.client.Run(client.RunParams{ 215 | VMName: showResponse.Name, 216 | Command: []string{"diskutil", "apfs", "resizeContainer", "disk0s2", "0"}, 217 | }) 218 | if err != nil { 219 | return err 220 | } 221 | 222 | // Prevent 'VM is already running' error 223 | err = s.client.Stop(stopParams) 224 | if err != nil { 225 | return err 226 | } 227 | } 228 | 229 | if diskSizeBytes < showResponse.HardDrive { 230 | return fmt.Errorf("Shrinking VM disks is not allowed! Source VM Disk Size (bytes): %v", showResponse.HardDrive) 231 | } 232 | } 233 | 234 | if config.RAMSize != "" && config.RAMSize != showResponse.RAM { 235 | err := s.client.Stop(stopParams) 236 | if err != nil { 237 | return err 238 | } 239 | 240 | ui.Say(fmt.Sprintf("Modifying VM %s RAM to %s", showResponse.Name, config.RAMSize)) 241 | 242 | err = s.client.Modify(showResponse.Name, "set", "ram", config.RAMSize) 243 | if err != nil { 244 | return err 245 | } 246 | } 247 | 248 | if config.VCPUCount != "" { 249 | stringVCPUCount, err := strconv.ParseInt(config.VCPUCount, 10, 32) 250 | if err != nil { 251 | return err 252 | } 253 | 254 | if int(stringVCPUCount) != showResponse.VCPUCores { 255 | err := s.client.Stop(stopParams) 256 | if err != nil { 257 | return err 258 | } 259 | 260 | ui.Say(fmt.Sprintf("Modifying VM %s VCPU core count to %v", showResponse.Name, stringVCPUCount)) 261 | 262 | err = s.client.Modify(showResponse.Name, "set", "cpu", "-c", strconv.Itoa(int(stringVCPUCount))) 263 | if err != nil { 264 | return err 265 | } 266 | } 267 | } 268 | 269 | return nil 270 | } 271 | 272 | func (s *StepCloneVM) modifyVMProperties(showResponse client.ShowResponse, config *Config, ui packer.Ui) error { 273 | stopParams := client.StopParams{ 274 | VMName: showResponse.Name, 275 | } 276 | 277 | if len(config.PortForwardingRules) > 0 { 278 | describeResponse, err := s.client.Describe(showResponse.Name) 279 | if err != nil { 280 | return err 281 | } 282 | existingForwardedPorts := make(map[int]struct{}) 283 | for _, existingNetworkCard := range describeResponse.NetworkCards { 284 | for _, existingPortForwardingRule := range existingNetworkCard.PortForwardingRules { 285 | existingForwardedPorts[existingPortForwardingRule.HostPort] = struct{}{} 286 | } 287 | } 288 | for _, wantedPortForwardingRule := range config.PortForwardingRules { 289 | ui.Say(fmt.Sprintf("Ensuring %s port-forwarding (Guest Port: %s, Host Port: %s, Rule Name: %s)", showResponse.Name, strconv.Itoa(wantedPortForwardingRule.PortForwardingGuestPort), strconv.Itoa(wantedPortForwardingRule.PortForwardingHostPort), wantedPortForwardingRule.PortForwardingRuleName)) 290 | if _, ok := existingForwardedPorts[wantedPortForwardingRule.PortForwardingHostPort]; ok { 291 | if wantedPortForwardingRule.PortForwardingHostPort > 0 { 292 | ui.Error(fmt.Sprintf("Found an existing host port rule (%s)! Skipping without setting...", strconv.Itoa(wantedPortForwardingRule.PortForwardingHostPort))) 293 | continue 294 | } 295 | } 296 | err := s.client.Stop(stopParams) 297 | if err != nil { 298 | return err 299 | } 300 | err = s.client.Modify(showResponse.Name, "add", "port-forwarding", "--host-port", strconv.Itoa(wantedPortForwardingRule.PortForwardingHostPort), "--guest-port", strconv.Itoa(wantedPortForwardingRule.PortForwardingGuestPort), wantedPortForwardingRule.PortForwardingRuleName) 301 | if !config.PackerConfig.PackerForce { 302 | if err != nil { 303 | return err 304 | } 305 | } 306 | } 307 | } 308 | 309 | if config.HWUUID != "" { 310 | err := s.client.Stop(stopParams) 311 | if err != nil { 312 | return err 313 | } 314 | ui.Say(fmt.Sprintf("Modifying VM custom-variable hw.uuid to %s", config.HWUUID)) 315 | err = s.client.Modify(showResponse.Name, "set", "custom-variable", "hw.uuid", config.HWUUID) 316 | if err != nil { 317 | return err 318 | } 319 | } 320 | 321 | if config.DisplayController != "" { 322 | err := s.client.Stop(stopParams) 323 | if err != nil { 324 | return err 325 | } 326 | ui.Say(fmt.Sprintf("Modifying VM display controller to %s", config.DisplayController)) 327 | err = s.client.Modify(showResponse.Name, "set", "display", "-c", config.DisplayController) 328 | if err != nil { 329 | return err 330 | } 331 | } 332 | 333 | return nil 334 | } 335 | -------------------------------------------------------------------------------- /builder/anka/step_connect_anka.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/packer-plugin-sdk/multistep" 7 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 8 | ) 9 | 10 | // StepConnectAnka attaches the anka builder to the communicator 11 | type StepConnectAnka struct{} 12 | 13 | // Run will add the ank client to the communicator and expose that via the state bag 14 | func (s *StepConnectAnka) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 15 | config := state.Get("config").(*Config) 16 | client := state.Get("client").(client.Client) 17 | tempDir := state.Get("temp_dir").(string) 18 | vmName := state.Get("vm_name").(string) 19 | 20 | comm := &Communicator{ 21 | Config: config, 22 | Client: client, 23 | HostDir: tempDir, 24 | VMDir: "/packer-files", 25 | VMName: vmName, 26 | FuseAvailable: false, 27 | } 28 | 29 | if config.UseAnkaCP { 30 | comm.FuseAvailable = false 31 | } else { 32 | comm.FuseAvailable = client.FuseAvailable(vmName) 33 | } 34 | 35 | state.Put("communicator", comm) 36 | return multistep.ActionContinue 37 | } 38 | 39 | // Cleanup will run when any error happens 40 | // nothing to do here since it just exposes a communicator 41 | func (s *StepConnectAnka) Cleanup(state multistep.StateBag) { 42 | } 43 | -------------------------------------------------------------------------------- /builder/anka/step_create_vm.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "regexp" 8 | "strconv" 9 | 10 | "github.com/hashicorp/packer-plugin-sdk/multistep" 11 | "github.com/hashicorp/packer-plugin-sdk/packer" 12 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 13 | "github.com/veertuinc/packer-plugin-veertu-anka/common" 14 | "github.com/veertuinc/packer-plugin-veertu-anka/util" 15 | ) 16 | 17 | // StepCreateVM will be used to run the create step for an 'vm-create' builder types 18 | type StepCreateVM struct { 19 | client client.Client 20 | vmName string 21 | } 22 | 23 | // Run creates a new vm from a local installer app 24 | func (s *StepCreateVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 25 | config := state.Get("config").(*Config) 26 | ui := state.Get("ui").(packer.Ui) 27 | ankaUtil := state.Get("util").(util.Util) 28 | onError := func(err error) multistep.StepAction { 29 | return ankaUtil.StepError(ui, state, err) 30 | } 31 | 32 | s.client = state.Get("client").(client.Client) 33 | s.vmName = config.VMName 34 | 35 | if s.vmName == "" { 36 | matchInstaller, err := regexp.Match(".app(/?)$|.ipsw(/?)$", []byte(config.Installer)) 37 | if err != nil { 38 | return onError(err) 39 | } 40 | if matchInstaller { 41 | if config.HostArch == "arm64" { 42 | installerData := util.InstallerIPSWPlist{} 43 | installerData, err = ankaUtil.ObtainMacOSVersionFromInstallerIPSW(config.Installer) 44 | if err != nil { 45 | return onError(err) 46 | } 47 | s.vmName = fmt.Sprintf("anka-packer-base-%s-%s", installerData.ProductVersion, installerData.ProductBuildVersion) 48 | } else { 49 | installerData := util.InstallerAppPlist{} 50 | installerData, err = ankaUtil.ObtainMacOSVersionFromInstallerApp(config.Installer) 51 | if err != nil { 52 | return onError(err) 53 | } 54 | s.vmName = fmt.Sprintf("anka-packer-base-%s-%s", installerData.OSVersion, installerData.BundlerVersion) 55 | } 56 | } else { 57 | s.vmName = fmt.Sprintf("anka-packer-base-%s", config.Installer) 58 | } 59 | } 60 | 61 | state.Put("vm_name", s.vmName) 62 | 63 | if config.PackerForce { 64 | exists, err := s.client.Exists(s.vmName) 65 | if err != nil { 66 | return onError(err) 67 | } 68 | if exists { 69 | ui.Say(fmt.Sprintf("Deleting existing virtual machine %s", s.vmName)) 70 | 71 | err = s.client.Delete(client.DeleteParams{VMName: s.vmName}) 72 | if err != nil { 73 | return onError(err) 74 | } 75 | } 76 | } 77 | 78 | err = s.createFromInstaller(ui, config) 79 | if err != nil { 80 | return onError(err) 81 | } 82 | 83 | createdShow, err := s.client.Show(s.vmName) 84 | if err != nil { 85 | return onError(err) 86 | } 87 | 88 | err = s.modifyVMProperties(createdShow, config, ui) 89 | if err != nil { 90 | return onError(err) 91 | } 92 | 93 | return multistep.ActionContinue 94 | } 95 | 96 | func (s *StepCreateVM) createFromInstaller(ui packer.Ui, config *Config) error { 97 | ui.Say(fmt.Sprintf("Creating a new VM Template (%s) from installer, this will take a while", s.vmName)) 98 | 99 | outputStream := make(chan string) 100 | 101 | go func() { 102 | for msg := range outputStream { 103 | ui.Say(msg) 104 | } 105 | }() 106 | 107 | createParams := client.CreateParams{ 108 | Installer: config.Installer, 109 | Name: s.vmName, 110 | DiskSize: config.DiskSize, 111 | VCPUCount: config.VCPUCount, 112 | RAMSize: config.RAMSize, 113 | } 114 | 115 | createdVMUUID, err := s.client.Create(createParams, outputStream) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | ui.Say(fmt.Sprintf("VM %s was created (%s)", s.vmName, createdVMUUID)) 121 | 122 | close(outputStream) 123 | 124 | return nil 125 | } 126 | 127 | func (s *StepCreateVM) modifyVMProperties(showResponse client.ShowResponse, config *Config, ui packer.Ui) error { 128 | stopParams := client.StopParams{ 129 | VMName: showResponse.Name, 130 | } 131 | 132 | if len(config.PortForwardingRules) > 0 { 133 | describeResponse, err := s.client.Describe(showResponse.Name) 134 | if err != nil { 135 | return err 136 | } 137 | existingForwardedPorts := make(map[int]struct{}) 138 | for _, existingNetworkCard := range describeResponse.NetworkCards { 139 | for _, existingPortForwardingRule := range existingNetworkCard.PortForwardingRules { 140 | existingForwardedPorts[existingPortForwardingRule.HostPort] = struct{}{} 141 | } 142 | } 143 | for _, wantedPortForwardingRule := range config.PortForwardingRules { 144 | ui.Say(fmt.Sprintf("Ensuring %s port-forwarding (Guest Port: %s, Host Port: %s, Rule Name: %s)", showResponse.Name, strconv.Itoa(wantedPortForwardingRule.PortForwardingGuestPort), strconv.Itoa(wantedPortForwardingRule.PortForwardingHostPort), wantedPortForwardingRule.PortForwardingRuleName)) 145 | if _, ok := existingForwardedPorts[wantedPortForwardingRule.PortForwardingHostPort]; ok { 146 | if wantedPortForwardingRule.PortForwardingHostPort > 0 { 147 | ui.Error(fmt.Sprintf("Found an existing host port rule (%s)! Skipping without setting...", strconv.Itoa(wantedPortForwardingRule.PortForwardingHostPort))) 148 | continue 149 | } 150 | } 151 | err := s.client.Stop(stopParams) 152 | if err != nil { 153 | return err 154 | } 155 | err = s.client.Modify(showResponse.Name, "add", "port-forwarding", "--host-port", strconv.Itoa(wantedPortForwardingRule.PortForwardingHostPort), "--guest-port", strconv.Itoa(wantedPortForwardingRule.PortForwardingGuestPort), wantedPortForwardingRule.PortForwardingRuleName) 156 | if !config.PackerConfig.PackerForce { 157 | if err != nil { 158 | return err 159 | } 160 | } 161 | } 162 | } 163 | 164 | if config.HWUUID != "" { 165 | err := s.client.Stop(stopParams) 166 | if err != nil { 167 | return err 168 | } 169 | ui.Say(fmt.Sprintf("Modifying VM custom-variable hw.uuid to %s", config.HWUUID)) 170 | err = s.client.Modify(showResponse.Name, "set", "custom-variable", "hw.uuid", config.HWUUID) 171 | if err != nil { 172 | return err 173 | } 174 | } 175 | 176 | if config.DisplayController != "" { 177 | err := s.client.Stop(stopParams) 178 | if err != nil { 179 | return err 180 | } 181 | ui.Say(fmt.Sprintf("Modifying VM display controller to %s", config.DisplayController)) 182 | err = s.client.Modify(showResponse.Name, "set", "display", "-c", config.DisplayController) 183 | if err != nil { 184 | return err 185 | } 186 | } 187 | 188 | return nil 189 | } 190 | 191 | // Cleanup will delete the vm if there happens to be an error and handle anything failed states 192 | func (s *StepCreateVM) Cleanup(state multistep.StateBag) { 193 | ui := state.Get("ui").(packer.Ui) 194 | 195 | log.Println("Cleaning up create VM step") 196 | if s.vmName == "" { 197 | return 198 | } 199 | 200 | _, halted := state.GetOk(multistep.StateHalted) 201 | _, canceled := state.GetOk(multistep.StateCancelled) 202 | errorObj := state.Get("error") 203 | switch errorObj.(type) { 204 | case *common.VMAlreadyExistsError: 205 | return 206 | case *common.VMNotFoundException: 207 | return 208 | default: 209 | if halted || canceled { 210 | ui.Say(fmt.Sprintf("Deleting VM %s", s.vmName)) 211 | 212 | err := s.client.Delete(client.DeleteParams{VMName: s.vmName}) 213 | if err != nil { 214 | ui.Error(fmt.Sprint(err)) 215 | } 216 | return 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /builder/anka/step_create_vm_test.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/golang/mock/gomock" 11 | "github.com/hashicorp/packer-plugin-sdk/common" 12 | "github.com/hashicorp/packer-plugin-sdk/multistep" 13 | "github.com/hashicorp/packer-plugin-sdk/packer" 14 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 15 | mocks "github.com/veertuinc/packer-plugin-veertu-anka/mocks" 16 | "github.com/veertuinc/packer-plugin-veertu-anka/util" 17 | "gotest.tools/v3/assert" 18 | ) 19 | 20 | var ( 21 | createdShowResponse client.ShowResponse 22 | createdDescribeResponse client.DescribeResponse 23 | ) 24 | 25 | func TestCreateVMRun(t *testing.T) { 26 | 27 | createdVMUUID := "abcd-efgh-1234-5678" 28 | 29 | mockCtrl := gomock.NewController(t) 30 | defer mockCtrl.Finish() 31 | ankaClient := mocks.NewMockClient(mockCtrl) 32 | ankaUtil := mocks.NewMockUtil(mockCtrl) 33 | 34 | step := StepCreateVM{} 35 | ui := packer.TestUi(t) 36 | ctx := context.Background() 37 | state := new(multistep.BasicStateBag) 38 | InstallerInfo := util.InstallerAppPlist{ 39 | OSVersion: "11.2", 40 | BundlerVersion: "16.4.06", 41 | } 42 | 43 | state.Put("ui", ui) 44 | state.Put("client", ankaClient) 45 | state.Put("util", ankaUtil) 46 | 47 | err = json.Unmarshal(json.RawMessage(`{ "Name": "anka-packer-base-11.2-16.4.06", "UUID": "1234-hijk-abcdef-5678" }`), &createdShowResponse) 48 | if err != nil { 49 | t.Fail() 50 | } 51 | 52 | err = json.Unmarshal(json.RawMessage(`{ }`), &createdDescribeResponse) 53 | if err != nil { 54 | t.Fail() 55 | } 56 | 57 | t.Run("create vm", func(t *testing.T) { 58 | config := &Config{ 59 | DiskSize: "500G", 60 | VCPUCount: "32G", 61 | RAMSize: "16G", 62 | Installer: "/fake/InstallApp.app/", 63 | VMName: "foo", 64 | PackerConfig: common.PackerConfig{ 65 | PackerBuilderType: "veertu-anka-vm-create", 66 | }, 67 | } 68 | 69 | state.Put("config", config) 70 | 71 | step.vmName = config.VMName 72 | state.Put("vm_name", step.vmName) 73 | 74 | createParams := client.CreateParams{ 75 | Installer: config.Installer, 76 | Name: step.vmName, 77 | DiskSize: config.DiskSize, 78 | VCPUCount: config.VCPUCount, 79 | RAMSize: config.RAMSize, 80 | } 81 | 82 | gomock.InOrder( 83 | ankaClient.EXPECT().Create(createParams, gomock.Any()).Return(createdVMUUID, nil).Times(1), 84 | ankaClient.EXPECT().Show(step.vmName).Return(createdShowResponse, nil).Times(1), 85 | ) 86 | 87 | mockui := packer.MockUi{} 88 | mockui.Say(fmt.Sprintf("Creating a new VM Template (%s) from installer, this will take a while", step.vmName)) 89 | mockui.Say(fmt.Sprintf("VM %s was created (%s)", step.vmName, createdVMUUID)) 90 | 91 | stepAction := step.Run(ctx, state) 92 | assert.Equal(t, mockui.SayMessages[0].Message, "Creating a new VM Template (foo) from installer, this will take a while") 93 | assert.Equal(t, mockui.SayMessages[1].Message, "VM foo was created (abcd-efgh-1234-5678)") 94 | assert.Equal(t, multistep.ActionContinue, stepAction) 95 | }) 96 | 97 | t.Run("create vm without .app or ipsw", func(t *testing.T) { 98 | config := &Config{ 99 | DiskSize: "500G", 100 | VCPUCount: "32G", 101 | RAMSize: "16G", 102 | Installer: "13.5", 103 | PackerConfig: common.PackerConfig{ 104 | PackerBuilderType: "veertu-anka-vm-create", 105 | }, 106 | } 107 | 108 | state.Put("config", config) 109 | 110 | step.vmName = "anka-packer-base-13.5" 111 | state.Put("vm_name", step.vmName) 112 | 113 | createParams := client.CreateParams{ 114 | Installer: config.Installer, 115 | Name: step.vmName, 116 | DiskSize: config.DiskSize, 117 | VCPUCount: config.VCPUCount, 118 | RAMSize: config.RAMSize, 119 | } 120 | 121 | gomock.InOrder( 122 | ankaClient.EXPECT().Create(createParams, gomock.Any()).Return(createdVMUUID, nil).Times(1), 123 | ankaClient.EXPECT().Show(step.vmName).Return(createdShowResponse, nil).Times(1), 124 | ) 125 | 126 | mockui := packer.MockUi{} 127 | mockui.Say(fmt.Sprintf("Creating a new VM Template (%s) from installer, this will take a while", step.vmName)) 128 | mockui.Say(fmt.Sprintf("VM %s was created (%s)", step.vmName, createdVMUUID)) 129 | 130 | stepAction := step.Run(ctx, state) 131 | assert.Equal(t, mockui.SayMessages[0].Message, fmt.Sprintf("Creating a new VM Template (%s) from installer, this will take a while", step.vmName)) 132 | assert.Equal(t, mockui.SayMessages[1].Message, fmt.Sprintf("VM %s was created (abcd-efgh-1234-5678)", step.vmName)) 133 | assert.Equal(t, multistep.ActionContinue, stepAction) 134 | }) 135 | 136 | t.Run("create vm with packer force", func(t *testing.T) { 137 | config := &Config{ 138 | DiskSize: "500G", 139 | VCPUCount: "32G", 140 | RAMSize: "16G", 141 | Installer: "/fake/InstallApp.app/", 142 | VMName: "foo", 143 | PackerConfig: common.PackerConfig{ 144 | PackerForce: true, 145 | PackerBuilderType: "veertu-anka-vm-create", 146 | }, 147 | } 148 | 149 | state.Put("config", config) 150 | 151 | step.vmName = config.VMName 152 | state.Put("vm_name", step.vmName) 153 | 154 | createParams := client.CreateParams{ 155 | Installer: config.Installer, 156 | Name: step.vmName, 157 | DiskSize: config.DiskSize, 158 | VCPUCount: config.VCPUCount, 159 | RAMSize: config.RAMSize, 160 | } 161 | 162 | gomock.InOrder( 163 | ankaClient.EXPECT().Exists(step.vmName).Return(true, nil).Times(1), 164 | ankaClient.EXPECT().Delete(client.DeleteParams{VMName: step.vmName}).Return(nil).Times(1), 165 | ankaClient.EXPECT().Create(createParams, gomock.Any()).Return(createdVMUUID, nil).Times(1), 166 | ankaClient.EXPECT().Show(step.vmName).Return(createdShowResponse, nil).Times(1), 167 | ) 168 | 169 | mockui := packer.MockUi{} 170 | mockui.Say(fmt.Sprintf("Deleting existing virtual machine %s", step.vmName)) 171 | mockui.Say(fmt.Sprintf("Creating a new VM Template (%s) from installer, this will take a while", step.vmName)) 172 | mockui.Say(fmt.Sprintf("VM %s was created (%s)", step.vmName, createdVMUUID)) 173 | 174 | stepAction := step.Run(ctx, state) 175 | assert.Equal(t, mockui.SayMessages[0].Message, "Deleting existing virtual machine foo") 176 | assert.Equal(t, mockui.SayMessages[1].Message, "Creating a new VM Template (foo) from installer, this will take a while") 177 | assert.Equal(t, mockui.SayMessages[2].Message, "VM foo was created (abcd-efgh-1234-5678)") 178 | assert.Equal(t, multistep.ActionContinue, stepAction) 179 | }) 180 | 181 | t.Run("create vm and installer app does not exist", func(t *testing.T) { 182 | config := &Config{ 183 | DiskSize: "500G", 184 | VCPUCount: "32G", 185 | RAMSize: "16G", 186 | Installer: "/does/not/exist/InstallApp.app/", 187 | VMName: "foo", 188 | PackerConfig: common.PackerConfig{ 189 | PackerBuilderType: "veertu-anka-vm-create", 190 | }, 191 | } 192 | 193 | state.Put("config", config) 194 | 195 | step.vmName = config.VMName 196 | state.Put("vm_name", step.vmName) 197 | 198 | createParams := client.CreateParams{ 199 | Installer: config.Installer, 200 | Name: step.vmName, 201 | DiskSize: config.DiskSize, 202 | VCPUCount: config.VCPUCount, 203 | RAMSize: config.RAMSize, 204 | } 205 | 206 | gomock.InOrder( 207 | ankaClient.EXPECT(). 208 | Create(createParams, gomock.Any()). 209 | Return(createdVMUUID, fmt.Errorf("installer app does not exist at %q", config.Installer)). 210 | Times(1), 211 | ankaUtil.EXPECT(). 212 | StepError(ui, state, fmt.Errorf("installer app does not exist at %q", config.Installer)). 213 | Return(multistep.ActionHalt). 214 | Times(1), 215 | ) 216 | 217 | stepAction := step.Run(ctx, state) 218 | assert.Equal(t, multistep.ActionHalt, stepAction) 219 | }) 220 | 221 | t.Run("create vm when no vm_name is provided in config", func(t *testing.T) { 222 | config := &Config{ 223 | DiskSize: "500G", 224 | VCPUCount: "32G", 225 | RAMSize: "16G", 226 | Installer: "/fake/InstallApp.app/", 227 | PackerConfig: common.PackerConfig{ 228 | PackerBuilderType: "veertu-anka-vm-create", 229 | }, 230 | } 231 | 232 | state.Put("config", config) 233 | 234 | step.vmName = fmt.Sprintf("anka-packer-base-%s-%s", InstallerInfo.OSVersion, InstallerInfo.BundlerVersion) 235 | state.Put("vm_name", step.vmName) 236 | 237 | createParams := client.CreateParams{ 238 | Installer: config.Installer, 239 | Name: step.vmName, 240 | DiskSize: config.DiskSize, 241 | VCPUCount: config.VCPUCount, 242 | RAMSize: config.RAMSize, 243 | } 244 | 245 | gomock.InOrder( 246 | ankaUtil.EXPECT().ObtainMacOSVersionFromInstallerApp(config.Installer).Return(InstallerInfo, nil).Times(1), 247 | ankaClient.EXPECT().Create(createParams, gomock.Any()).Return(createdVMUUID, nil).Times(1), 248 | ankaClient.EXPECT().Show(step.vmName).Return(createdShowResponse, nil).Times(1), 249 | ) 250 | 251 | mockui := packer.MockUi{} 252 | mockui.Say(fmt.Sprintf("Creating a new VM Template (%s) from installer, this will take a while", step.vmName)) 253 | mockui.Say(fmt.Sprintf("VM %s was created (%s)", step.vmName, createdVMUUID)) 254 | 255 | stepAction := step.Run(ctx, state) 256 | assert.Equal(t, mockui.SayMessages[0].Message, "Creating a new VM Template (anka-packer-base-11.2-16.4.06) from installer, this will take a while") 257 | assert.Equal(t, mockui.SayMessages[1].Message, "VM anka-packer-base-11.2-16.4.06 was created (abcd-efgh-1234-5678)") 258 | assert.Equal(t, multistep.ActionContinue, stepAction) 259 | }) 260 | 261 | t.Run("create vm with modify vm properties", func(t *testing.T) { 262 | 263 | err = json.Unmarshal(json.RawMessage(`{ "Name": "anka-packer-base-latest", "UUID": "1234-hijk-abcdef-5678" }`), &createdShowResponse) 264 | if err != nil { 265 | t.Fail() 266 | } 267 | 268 | var config Config 269 | err = json.Unmarshal(json.RawMessage(` 270 | { 271 | "PortForwardingRules": [ 272 | { 273 | "PortForwardingGuestPort": 8080, 274 | "PortForwardingHostPort": 80, 275 | "PortForwardingRuleName": "rule1" 276 | } 277 | ], 278 | "DiskSize": "500G", 279 | "VCPUCount": "32G", 280 | "RAMSize": "16G", 281 | "Installer": "latest", 282 | "HWUUID": "abcdefgh", 283 | "DisplayController": "pg" 284 | } 285 | `), &config) 286 | if err != nil { 287 | t.Fail() 288 | } 289 | 290 | config.PackerConfig = common.PackerConfig{ 291 | PackerBuilderType: "veertu-anka-vm-create", 292 | } 293 | 294 | state.Put("config", config) 295 | 296 | step.vmName = fmt.Sprintf("anka-packer-base-%s", config.Installer) 297 | state.Put("vm_name", step.vmName) 298 | 299 | createParams := client.CreateParams{ 300 | Installer: config.Installer, 301 | Name: step.vmName, 302 | DiskSize: config.DiskSize, 303 | VCPUCount: config.VCPUCount, 304 | RAMSize: config.RAMSize, 305 | } 306 | 307 | stopParams := client.StopParams{ 308 | VMName: createdShowResponse.Name, 309 | } 310 | 311 | state.Put("config", &config) 312 | 313 | gomock.InOrder( 314 | ankaClient.EXPECT().Create(createParams, gomock.Any()).Return(createdVMUUID, nil).Times(1), 315 | ankaClient.EXPECT().Show(step.vmName).Return(createdShowResponse, nil).Times(1), 316 | ankaClient.EXPECT().Describe(step.vmName).Return(client.DescribeResponse{}, nil).Times(1), 317 | ankaClient.EXPECT().Stop(stopParams).Return(nil).Times(1), 318 | ankaClient.EXPECT(). 319 | Modify(createdShowResponse.Name, "add", "port-forwarding", "--host-port", strconv.Itoa(config.PortForwardingRules[0].PortForwardingHostPort), "--guest-port", strconv.Itoa(config.PortForwardingRules[0].PortForwardingGuestPort), "rule1"). 320 | Return(nil). 321 | Times(1), 322 | ankaClient.EXPECT().Stop(stopParams).Return(nil).Times(1), 323 | ankaClient.EXPECT().Modify(createdShowResponse.Name, "set", "custom-variable", "hw.uuid", config.HWUUID).Return(nil).Times(1), 324 | ankaClient.EXPECT().Stop(stopParams).Return(nil).Times(1), 325 | ankaClient.EXPECT().Modify(createdShowResponse.Name, "set", "display", "-c", config.DisplayController).Return(nil).Times(1), 326 | ) 327 | 328 | mockui := packer.MockUi{} 329 | mockui.Say(fmt.Sprintf("Creating a new VM Template (%s) from installer, this will take a while", step.vmName)) 330 | mockui.Say(fmt.Sprintf("VM %s was created (%s)", step.vmName, createdVMUUID)) 331 | mockui.Say(fmt.Sprintf("Ensuring %s port-forwarding (Guest Port: %s, Host Port: %s, Rule Name: %s)", createdShowResponse.Name, strconv.Itoa(config.PortForwardingRules[0].PortForwardingGuestPort), strconv.Itoa(config.PortForwardingRules[0].PortForwardingHostPort), config.PortForwardingRules[0].PortForwardingRuleName)) 332 | mockui.Say(fmt.Sprintf("Modifying VM custom-variable hw.uuid to %s", config.HWUUID)) 333 | mockui.Say(fmt.Sprintf("Modifying VM display controller to %s", config.DisplayController)) 334 | 335 | stepAction := step.Run(ctx, state) 336 | assert.Equal(t, mockui.SayMessages[0].Message, "Creating a new VM Template (anka-packer-base-latest) from installer, this will take a while") 337 | assert.Equal(t, mockui.SayMessages[1].Message, "VM anka-packer-base-latest was created (abcd-efgh-1234-5678)") 338 | assert.Equal(t, multistep.ActionContinue, stepAction) 339 | 340 | }) 341 | } 342 | -------------------------------------------------------------------------------- /builder/anka/step_set_generated_data.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "log" 7 | "strings" 8 | 9 | "github.com/hashicorp/packer-plugin-sdk/multistep" 10 | "github.com/hashicorp/packer-plugin-sdk/packerbuilderdata" 11 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 12 | ) 13 | 14 | var ( 15 | darwinBuffer, osBuffer bytes.Buffer 16 | ) 17 | 18 | type StepSetGeneratedData struct { 19 | client client.Client 20 | vmName string 21 | GeneratedData *packerbuilderdata.GeneratedData 22 | } 23 | 24 | func (s *StepSetGeneratedData) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { 25 | log.Printf("Exposing build contextual variables...") 26 | 27 | s.client = state.Get("client").(client.Client) 28 | s.vmName = state.Get("vm_name").(string) 29 | darwinVersion := client.RunParams{ 30 | Command: []string{"/usr/bin/uname", "-r"}, 31 | VMName: s.vmName, 32 | Stdout: &darwinBuffer, 33 | } 34 | osVersion := client.RunParams{ 35 | Command: []string{"/usr/bin/sw_vers", "-productVersion"}, 36 | VMName: s.vmName, 37 | Stdout: &osBuffer, 38 | } 39 | 40 | _, err := s.client.Run(darwinVersion) 41 | if err != nil { 42 | return multistep.ActionHalt 43 | } 44 | 45 | _, osErr := s.client.Run(osVersion) 46 | if osErr != nil { 47 | return multistep.ActionHalt 48 | } 49 | 50 | s.GeneratedData.Put("VMName", s.vmName) 51 | s.GeneratedData.Put("OSVersion", strings.TrimSpace(osBuffer.String())) 52 | s.GeneratedData.Put("DarwinVersion", strings.TrimSpace(darwinBuffer.String())) 53 | 54 | return multistep.ActionContinue 55 | } 56 | 57 | // Cleanup will run whenever there are any errors. 58 | // No cleanup needs to happen here 59 | func (s *StepSetGeneratedData) Cleanup(_ multistep.StateBag) { 60 | } 61 | -------------------------------------------------------------------------------- /builder/anka/step_set_generated_data_test.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | "github.com/hashicorp/packer-plugin-sdk/multistep" 10 | "github.com/hashicorp/packer-plugin-sdk/packer" 11 | "github.com/hashicorp/packer-plugin-sdk/packerbuilderdata" 12 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 13 | mocks "github.com/veertuinc/packer-plugin-veertu-anka/mocks" 14 | "gotest.tools/v3/assert" 15 | ) 16 | 17 | func TestSetGeneratedDataRun(t *testing.T) { 18 | mockCtrl := gomock.NewController(t) 19 | defer mockCtrl.Finish() 20 | ankaClient := mocks.NewMockClient(mockCtrl) 21 | ankaUtil := mocks.NewMockUtil(mockCtrl) 22 | 23 | state := new(multistep.BasicStateBag) 24 | step := StepSetGeneratedData{ 25 | vmName: "foo-11.2-16.4.06", 26 | GeneratedData: &packerbuilderdata.GeneratedData{State: state}, 27 | } 28 | ui := packer.TestUi(t) 29 | ctx := context.Background() 30 | darwinVersion := client.RunParams{ 31 | Command: []string{"/usr/bin/uname", "-r"}, 32 | VMName: step.vmName, 33 | Stdout: &bytes.Buffer{}, 34 | } 35 | osv := client.RunParams{ 36 | Command: []string{"/usr/bin/sw_vers", "-productVersion"}, 37 | VMName: step.vmName, 38 | Stdout: &bytes.Buffer{}, 39 | } 40 | 41 | state.Put("ui", ui) 42 | state.Put("client", ankaClient) 43 | state.Put("util", ankaUtil) 44 | 45 | t.Run("expose variables", func(t *testing.T) { 46 | state.Put("vm_name", step.vmName) 47 | 48 | gomock.InOrder( 49 | ankaClient.EXPECT().Run(darwinVersion).Times(1), 50 | ankaClient.EXPECT().Run(osv).Times(1), 51 | ) 52 | 53 | stepAction := step.Run(ctx, state) 54 | assert.Equal(t, multistep.ActionContinue, stepAction) 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /builder/anka/step_start_vm.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/hashicorp/packer-plugin-sdk/multistep" 9 | "github.com/hashicorp/packer-plugin-sdk/packer" 10 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 11 | "github.com/veertuinc/packer-plugin-veertu-anka/util" 12 | ) 13 | 14 | // StepStartVM will start the created/cloned vms 15 | type StepStartVM struct{} 16 | 17 | // Run will call the necessary client commands to start the vm 18 | func (s *StepStartVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 19 | config := state.Get("config").(*Config) 20 | ui := state.Get("ui").(packer.Ui) 21 | util := state.Get("util").(util.Util) 22 | onError := func(err error) multistep.StepAction { 23 | return util.StepError(ui, state, err) 24 | } 25 | cmdClient := state.Get("client").(client.Client) 26 | vmName := state.Get("vm_name").(string) 27 | 28 | err := cmdClient.Start(client.StartParams{ 29 | VMName: vmName, 30 | }) 31 | if err != nil { 32 | return onError(err) 33 | } 34 | 35 | if config.BootDelay != "" { 36 | d, err := time.ParseDuration(config.BootDelay) 37 | if err != nil { 38 | return onError(err) 39 | } 40 | 41 | ui.Say(fmt.Sprintf("Waiting for %s for clone to boot", d)) 42 | time.Sleep(d) 43 | } 44 | 45 | return multistep.ActionContinue 46 | } 47 | 48 | // Cleanup will run when errors occur 49 | // Nothing to do here since this just this step just starts vms 50 | func (s *StepStartVM) Cleanup(state multistep.StateBag) { 51 | } 52 | -------------------------------------------------------------------------------- /builder/anka/step_start_vm_test.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/golang/mock/gomock" 9 | "github.com/hashicorp/packer-plugin-sdk/multistep" 10 | "github.com/hashicorp/packer-plugin-sdk/packer" 11 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 12 | mocks "github.com/veertuinc/packer-plugin-veertu-anka/mocks" 13 | "gotest.tools/v3/assert" 14 | ) 15 | 16 | func TestStartVMRun(t *testing.T) { 17 | mockCtrl := gomock.NewController(t) 18 | defer mockCtrl.Finish() 19 | ankaClient := mocks.NewMockClient(mockCtrl) 20 | ankaUtil := mocks.NewMockUtil(mockCtrl) 21 | 22 | step := StepStartVM{} 23 | ui := packer.TestUi(t) 24 | ctx := context.Background() 25 | state := new(multistep.BasicStateBag) 26 | 27 | state.Put("ui", ui) 28 | state.Put("client", ankaClient) 29 | state.Put("util", ankaUtil) 30 | state.Put("vm_name", "foo") 31 | 32 | t.Run("start vm", func(t *testing.T) { 33 | config := &Config{} 34 | 35 | state.Put("config", config) 36 | 37 | ankaClient.EXPECT().Start(client.StartParams{VMName: "foo"}).Return(nil).Times(1) 38 | 39 | stepAction := step.Run(ctx, state) 40 | assert.Equal(t, multistep.ActionContinue, stepAction) 41 | }) 42 | 43 | t.Run("start vm with boot delay", func(t *testing.T) { 44 | config := &Config{ 45 | BootDelay: "1s", 46 | } 47 | 48 | mockui := packer.MockUi{} 49 | mockui.Say(fmt.Sprintf("Waiting for %s for clone to boot", config.BootDelay)) 50 | 51 | state.Put("config", config) 52 | 53 | ankaClient.EXPECT().Start(client.StartParams{VMName: "foo"}).Return(nil).Times(1) 54 | 55 | stepAction := step.Run(ctx, state) 56 | assert.Equal(t, mockui.SayMessages[0].Message, "Waiting for 1s for clone to boot") 57 | assert.Equal(t, multistep.ActionContinue, stepAction) 58 | }) 59 | 60 | t.Run("start vm but fail to start", func(t *testing.T) { 61 | config := &Config{} 62 | 63 | state.Put("config", config) 64 | 65 | gomock.InOrder( 66 | ankaClient.EXPECT().Start(client.StartParams{VMName: "foo"}).Return(fmt.Errorf("failed to start vm %s", "foo")).Times(1), 67 | ankaUtil.EXPECT().StepError(ui, state, fmt.Errorf("failed to start vm %s", "foo")).Return(multistep.ActionHalt).Times(1), 68 | ) 69 | 70 | stepAction := step.Run(ctx, state) 71 | assert.Equal(t, multistep.ActionHalt, stepAction) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /builder/anka/step_temp_dir.go: -------------------------------------------------------------------------------- 1 | package anka 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/hashicorp/packer-plugin-sdk/multistep" 10 | "github.com/hashicorp/packer-plugin-sdk/packer" 11 | "github.com/veertuinc/packer-plugin-veertu-anka/util" 12 | ) 13 | 14 | var ( 15 | err error 16 | tempdir string 17 | ) 18 | 19 | // StepTempDir creates a temporary directory that we use in order to 20 | // share data with the anka vm over the communicator. 21 | type StepTempDir struct { 22 | tempDir string 23 | } 24 | 25 | // Run will create the temporary directory used by the anka vm 26 | func (s *StepTempDir) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { 27 | ui := state.Get("ui").(packer.Ui) 28 | util := state.Get("util").(util.Util) 29 | onError := func(err error) multistep.StepAction { 30 | return util.StepError(ui, state, err) 31 | } 32 | 33 | ui.Say("Creating a temporary directory for sharing data...") 34 | 35 | configTmpDir, err := util.ConfigTmpDir() 36 | if err != nil { 37 | err := fmt.Errorf("Error making temp dir: %s", err) 38 | return onError(err) 39 | } 40 | 41 | tempdir, err = ioutil.TempDir(configTmpDir, "packer-anka") 42 | if err != nil { 43 | return onError(err) 44 | } 45 | 46 | s.tempDir = tempdir 47 | state.Put("temp_dir", s.tempDir) 48 | 49 | return multistep.ActionContinue 50 | } 51 | 52 | // Cleanup is run when errors occur 53 | // Will cleanup the temporary directory if something fails 54 | func (s *StepTempDir) Cleanup(state multistep.StateBag) { 55 | os.RemoveAll(s.tempDir) 56 | } 57 | -------------------------------------------------------------------------------- /builder/anka/test-fixtures/onecakes/strawberry: -------------------------------------------------------------------------------- 1 | strawberry! 2 | -------------------------------------------------------------------------------- /builder/anka/test-fixtures/scripts/script1.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo hello world from script1 3 | echo script1 >> /tmp/provisioner_log -------------------------------------------------------------------------------- /builder/anka/test-fixtures/scripts/script2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo hello world from script2 3 | echo script2 >> /tmp/provisioner_log -------------------------------------------------------------------------------- /client/cli.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | func runAnkaCommand(args ...string) (MachineReadableOutput, error) { 13 | return runCommandStreamer(nil, args...) 14 | } 15 | 16 | func runCommandStreamer(outputStreamer chan string, args ...string) (MachineReadableOutput, error) { 17 | 18 | cmdArgs := append([]string{"--machine-readable"}, args...) 19 | 20 | log.Printf("Executing anka %s", strings.Join(cmdArgs, " ")) 21 | 22 | cmd := exec.Command("anka", cmdArgs...) 23 | 24 | for _, e := range os.Environ() { // Ensure that ANKA_ environment variables from the host are available when executing anka commands 25 | pair := strings.SplitN(e, "=", 2) 26 | key := pair[0] 27 | val := pair[1] 28 | if strings.HasPrefix(key, "ANKA_") || strings.HasPrefix(key, "PATH") { 29 | cmd.Env = append([]string{key + "=" + val}, cmd.Env...) 30 | } 31 | } 32 | 33 | outPipe, err := cmd.StdoutPipe() 34 | if err != nil { 35 | return MachineReadableOutput{}, err 36 | } 37 | 38 | if outputStreamer == nil { 39 | cmd.Stderr = cmd.Stdout 40 | } 41 | 42 | err = cmd.Start() 43 | if err != nil { 44 | return MachineReadableOutput{}, err 45 | } 46 | 47 | outScanner := bufio.NewScanner(outPipe) 48 | outScanner.Split(customSplit) 49 | 50 | for outScanner.Scan() { 51 | out := outScanner.Text() 52 | 53 | if outputStreamer != nil { 54 | outputStreamer <- out 55 | } 56 | } 57 | 58 | scannerErr := outScanner.Err() 59 | if scannerErr == nil { 60 | return MachineReadableOutput{}, errors.New("missing machine readable output") 61 | } 62 | 63 | _, ok := scannerErr.(customErr) 64 | if !ok { 65 | return MachineReadableOutput{}, err 66 | } 67 | 68 | finalOutput := scannerErr.Error() 69 | 70 | parsed, err := parseOutput([]byte(finalOutput)) 71 | if err != nil { 72 | return MachineReadableOutput{}, err 73 | } 74 | 75 | cmd.Wait() 76 | 77 | err = parsed.GetError() 78 | if err != nil { 79 | return MachineReadableOutput{}, err 80 | } 81 | 82 | return parsed, nil 83 | } 84 | 85 | func runRegistryCommand(registryParams RegistryParams, args ...string) (MachineReadableOutput, error) { 86 | cmdArgs := []string{"registry"} 87 | 88 | if registryParams.Remote != "" { 89 | cmdArgs = append(cmdArgs, "--remote", registryParams.Remote) 90 | } 91 | 92 | if registryParams.NodeCertPath != "" { 93 | cmdArgs = append(cmdArgs, "--cert", registryParams.NodeCertPath) 94 | } 95 | 96 | if registryParams.NodeKeyPath != "" { 97 | cmdArgs = append(cmdArgs, "--key", registryParams.NodeKeyPath) 98 | } 99 | 100 | if registryParams.CaRootPath != "" { 101 | cmdArgs = append(cmdArgs, "--cacert", registryParams.CaRootPath) 102 | } 103 | 104 | if registryParams.IsInsecure { 105 | cmdArgs = append(cmdArgs, "--insecure") 106 | } 107 | 108 | cmdArgs = append(cmdArgs, args...) 109 | 110 | return runAnkaCommand(cmdArgs...) 111 | } 112 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | const ( 9 | statusOK = "OK" 10 | ) 11 | 12 | type Client interface { 13 | Create(params CreateParams, outputStreamer chan string) (string, error) 14 | Clone(params CloneParams) error 15 | Copy(params CopyParams) error 16 | Delete(params DeleteParams) error 17 | Describe(vmName string) (DescribeResponse, error) 18 | Exists(vmName string) (bool, error) 19 | License() (LicenseResponse, error) 20 | Modify(vmName string, command string, property string, flags ...string) error 21 | RegistryList(registryParams RegistryParams) ([]RegistryListResponse, error) 22 | RegistryListRepos() ([]RegistryRemote, error) 23 | RegistryPull(registryParams RegistryParams, pullParams RegistryPullParams) error 24 | RegistryPush(registryParams RegistryParams, pushParams RegistryPushParams) error 25 | RegistryRevert(url string, id string) error 26 | Run(params RunParams) (int, error) 27 | Show(vmName string) (ShowResponse, error) 28 | Start(params StartParams) error 29 | Stop(params StopParams) error 30 | Suspend(params SuspendParams) error 31 | UpdateAddons(vmName string) error 32 | Version() (VersionResponse, error) 33 | FuseAvailable(vmName string) bool 34 | } 35 | 36 | type AnkaClient struct { 37 | } 38 | 39 | type MachineReadableError struct { 40 | *MachineReadableOutput 41 | } 42 | 43 | func (ae MachineReadableError) Error() string { 44 | return ae.Message 45 | } 46 | 47 | type MachineReadableOutput struct { 48 | Status string `json:"status"` 49 | Body json.RawMessage 50 | Message string `json:"message"` 51 | Code int `json:"code"` 52 | ExceptionType string `json:"exception_type"` 53 | } 54 | 55 | func (parsed *MachineReadableOutput) GetError() error { 56 | if parsed.Status != statusOK { 57 | return MachineReadableError{parsed} 58 | } 59 | return nil 60 | } 61 | 62 | func parseOutput(output []byte) (MachineReadableOutput, error) { 63 | var parsed MachineReadableOutput 64 | if err := json.Unmarshal(output, &parsed); err != nil { 65 | return parsed, err 66 | } 67 | 68 | return parsed, nil 69 | } 70 | 71 | func dropCR(data []byte) []byte { 72 | if len(data) > 0 && data[len(data)-1] == '\r' { 73 | return data[0 : len(data)-1] 74 | } 75 | return data 76 | } 77 | 78 | func customSplit(data []byte, atEOF bool) (advance int, token []byte, err error) { 79 | // A tiny spin off on ScanLines 80 | 81 | if atEOF && len(data) == 0 { 82 | return 0, nil, nil 83 | } 84 | if i := bytes.IndexByte(data, '\n'); i >= 0 { 85 | return i + 1, dropCR(data[0:i]), nil 86 | } 87 | if atEOF { // Machine readable data is parsed here 88 | out := dropCR(data) 89 | return len(data), out, customErr{data: out} 90 | } 91 | return 0, nil, nil 92 | } 93 | 94 | type customErr struct { 95 | data []byte 96 | } 97 | 98 | func (e customErr) Error() string { 99 | return string(e.data) 100 | } 101 | -------------------------------------------------------------------------------- /client/client_core.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os/exec" 10 | 11 | "github.com/veertuinc/packer-plugin-veertu-anka/common" 12 | ) 13 | 14 | const ( 15 | AnkaNameAlreadyExistsErrorCode = 18 16 | AnkaVMNotFoundExceptionErrorCode = 3 17 | ) 18 | 19 | // https://docs.veertu.com/anka/intel/command-line-reference/#clone 20 | type CloneParams struct { 21 | VMName string 22 | SourceUUID string 23 | } 24 | 25 | func (c *AnkaClient) Clone(params CloneParams) error { 26 | _, err := runAnkaCommand("clone", params.SourceUUID, params.VMName) 27 | if err != nil { 28 | merr, ok := err.(MachineReadableError) 29 | if ok { 30 | if merr.Code == AnkaNameAlreadyExistsErrorCode { 31 | return &common.VMAlreadyExistsError{} 32 | } 33 | } 34 | return err 35 | } 36 | 37 | return nil 38 | } 39 | 40 | // https://docs.veertu.com/anka/intel/command-line-reference/#cp 41 | type CopyParams struct { 42 | Src string 43 | Dst string 44 | } 45 | 46 | func (c *AnkaClient) Copy(params CopyParams) error { 47 | _, err := runAnkaCommand("cp", "-pRLf", params.Src, params.Dst) 48 | return err 49 | } 50 | 51 | type CreateParams struct { 52 | Name string 53 | Installer string 54 | OpticalDrive string 55 | RAMSize string 56 | DiskSize string 57 | VCPUCount string 58 | } 59 | 60 | func (c *AnkaClient) Create(params CreateParams, outputStreamer chan string) (string, error) { 61 | 62 | args := []string{ 63 | "create", 64 | "--app", params.Installer, 65 | } 66 | 67 | if params.DiskSize != "" { 68 | args = append(args, "--disk-size", params.DiskSize) 69 | } 70 | if params.VCPUCount != "" { 71 | args = append(args, "--cpu-count", params.VCPUCount) 72 | } 73 | if params.RAMSize != "" { 74 | args = append(args, "--ram-size", params.RAMSize) 75 | } 76 | 77 | args = append(args, params.Name) 78 | 79 | output, err := runCommandStreamer(outputStreamer, args...) 80 | createdVMUUID := bytes.NewBuffer(output.Body).String() 81 | if err != nil { 82 | return createdVMUUID, err 83 | } 84 | 85 | return createdVMUUID, nil 86 | } 87 | 88 | // https://docs.veertu.com/anka/intel/command-line-reference/#delete 89 | type DeleteParams struct { 90 | VMName string 91 | } 92 | 93 | func (c *AnkaClient) Delete(params DeleteParams) error { 94 | args := []string{ 95 | "delete", 96 | "--yes", 97 | } 98 | 99 | args = append(args, params.VMName) 100 | 101 | _, err := runAnkaCommand(args...) 102 | return err 103 | } 104 | 105 | // https://docs.veertu.com/anka/intel/command-line-reference/#describe 106 | type DescribeResponse struct { 107 | Name string `json:"name"` 108 | Version int `json:"version"` 109 | UUID string `json:"uuid"` 110 | VCPU struct { 111 | Cores int `json:"cores"` 112 | Threads int `json:"threads"` 113 | } `json:"cpu"` 114 | RAM string `json:"ram"` 115 | Usb struct { 116 | Tablet int `json:"tablet"` 117 | Kbd int `json:"kbd"` 118 | Host interface{} `json:"host"` 119 | Location interface{} `json:"location"` 120 | PciSlot int `json:"pci_slot"` 121 | Mouse int `json:"mouse"` 122 | } `json:"usb"` 123 | OpticalDrives []interface{} `json:"optical_drives"` 124 | HardDrives []struct { 125 | Controller string `json:"controller"` 126 | PciSlot int `json:"pci_slot"` 127 | File string `json:"file"` 128 | } `json:"hard_drives"` 129 | NetworkCards []struct { 130 | Index int `json:"index"` 131 | Mode string `json:"mode"` 132 | MacAddress string `json:"mac_address"` 133 | PortForwardingRules []struct { 134 | GuestPort int `json:"guest_port"` 135 | RuleName string `json:"rule_name"` 136 | Protocol string `json:"protocol"` 137 | HostIP string `json:"host_ip"` 138 | HostPort int `json:"host_port"` 139 | } `json:"port_forwarding_rules"` 140 | PciSlot int `json:"pci_slot"` 141 | Type string `json:"type"` 142 | } `json:"network_cards"` 143 | Smbios struct { 144 | Type string `json:"type"` 145 | } `json:"smbios"` 146 | Smc struct { 147 | Type string `json:"type"` 148 | } `json:"smc"` 149 | Nvram bool `json:"nvram"` 150 | Firmware struct { 151 | Type string `json:"type"` 152 | } `json:"firmware"` 153 | Display struct { 154 | Headless int `json:"headless"` 155 | FrameBuffer struct { 156 | PciSlot int `json:"pci_slot"` 157 | VncPort int `json:"vnc_port"` 158 | Height int `json:"height"` 159 | Width json.Number `json:"width"` // json.Number to work around bug in 2.5.7 where width existed twice and was both an int and string 160 | VncIP string `json:"vnc_ip"` 161 | Password string `json:"password"` 162 | } `json:"frame_buffer"` 163 | } `json:"display"` 164 | } 165 | 166 | func (c *AnkaClient) Describe(vmName string) (DescribeResponse, error) { 167 | var response DescribeResponse 168 | 169 | output, err := runAnkaCommand("describe", vmName) 170 | if err != nil { 171 | return response, err 172 | } 173 | 174 | err = json.Unmarshal(output.Body, &response) 175 | if err != nil { 176 | return response, err 177 | } 178 | 179 | return response, nil 180 | } 181 | 182 | // https://docs.veertu.com/anka/intel/command-line-reference/#show 183 | func (c *AnkaClient) Exists(vmName string) (bool, error) { 184 | _, err := c.Show(vmName) 185 | if err == nil { 186 | return true, nil 187 | } 188 | 189 | switch err.(type) { 190 | // case *json.UnmarshalTypeError: 191 | case *common.VMNotFoundException: 192 | return false, nil 193 | } 194 | 195 | return false, err 196 | } 197 | 198 | // https://docs.veertu.com/anka/intel/command-line-reference/#license 199 | type LicenseResponse struct { 200 | LicenseType string `json:"license_type"` 201 | Status string `json:"status"` 202 | } 203 | 204 | func (c *AnkaClient) License() (LicenseResponse, error) { 205 | var response LicenseResponse 206 | 207 | output, err := runAnkaCommand("license", "show") 208 | if err != nil { 209 | return response, err 210 | } 211 | 212 | err = json.Unmarshal(output.Body, &response) 213 | if err != nil { 214 | return response, err 215 | } 216 | 217 | return response, nil 218 | } 219 | 220 | // https://docs.veertu.com/anka/intel/command-line-reference/#modify 221 | func (c *AnkaClient) Modify(vmName string, command string, property string, flags ...string) error { 222 | ankaCommand := []string{"modify", vmName, command, property} 223 | ankaCommand = append(ankaCommand, flags...) 224 | 225 | output, err := runAnkaCommand(ankaCommand...) 226 | if err != nil { 227 | return err 228 | } 229 | if output.Status != "OK" { 230 | log.Print("Error executing modify command: ", output.ExceptionType, " ", output.Message) 231 | return fmt.Errorf(output.Message) 232 | } 233 | 234 | return nil 235 | } 236 | 237 | // https://docs.veertu.com/anka/intel/command-line-reference/#run 238 | type RunParams struct { 239 | VMName string 240 | Volume string 241 | WaitForNetworking bool 242 | WaitForTimeSync bool 243 | Command []string 244 | Stdin io.Reader 245 | Stdout, Stderr io.Writer 246 | Debug bool 247 | User string 248 | FuseAvailable bool 249 | } 250 | 251 | func (c *AnkaClient) Run(params RunParams) (int, error) { 252 | runner := NewRunner(params) 253 | 254 | err := runner.Start() 255 | if err != nil { 256 | return 1, err 257 | } 258 | 259 | log.Printf("Waiting for command to run") 260 | return runner.Wait() 261 | } 262 | 263 | // https://docs.veertu.com/anka/intel/command-line-reference/#show 264 | type ShowResponse struct { 265 | UUID string `json:"uuid"` 266 | Name string `json:"name"` 267 | VCPUCores int `json:"cpu_cores"` 268 | RAM string `json:"ram"` 269 | ImageID string `json:"image_id"` 270 | Status string `json:"status"` 271 | HardDrive uint64 `json:"hard_drive"` 272 | Version string `json:"version"` 273 | } 274 | 275 | func (sr ShowResponse) IsRunning() bool { 276 | return sr.Status == "running" 277 | } 278 | 279 | func (sr ShowResponse) IsStopped() bool { 280 | return sr.Status == "stopped" 281 | } 282 | 283 | func (sr ShowResponse) IsSuspended() bool { 284 | return sr.Status == "suspended" 285 | } 286 | 287 | func (c *AnkaClient) Show(vmName string) (ShowResponse, error) { 288 | var response ShowResponse 289 | 290 | output, err := runAnkaCommand("show", vmName) 291 | if err != nil { 292 | merr, ok := err.(MachineReadableError) 293 | if ok { 294 | if merr.Code == AnkaVMNotFoundExceptionErrorCode { 295 | return response, &common.VMNotFoundException{} 296 | } 297 | } 298 | 299 | return response, err 300 | } 301 | 302 | err = json.Unmarshal(output.Body, &response) 303 | if err != nil { 304 | return response, err 305 | } 306 | 307 | return response, nil 308 | } 309 | 310 | // https://docs.veertu.com/anka/intel/command-line-reference/#start 311 | type StartParams struct { 312 | VMName string 313 | } 314 | 315 | func (c *AnkaClient) Start(params StartParams) error { 316 | _, err := runAnkaCommand("start", params.VMName) 317 | return err 318 | } 319 | 320 | // https://docs.veertu.com/anka/intel/command-line-reference/#stop 321 | type StopParams struct { 322 | VMName string 323 | Force bool 324 | } 325 | 326 | func (c *AnkaClient) Stop(params StopParams) error { 327 | args := []string{ 328 | "stop", 329 | } 330 | 331 | if params.Force { 332 | args = append(args, "--force") 333 | } 334 | 335 | args = append(args, params.VMName) 336 | // Check if it's suspended, and do a run to start, then graceful stop 337 | showResponse, err := c.Show(params.VMName) 338 | if err != nil { 339 | return err 340 | } 341 | if showResponse.IsSuspended() { 342 | _, err = c.Run(RunParams{ 343 | VMName: params.VMName, 344 | WaitForNetworking: true, 345 | WaitForTimeSync: true, 346 | Command: []string{"true"}, 347 | }) 348 | } 349 | if err != nil { 350 | return err 351 | } 352 | 353 | _, err = runAnkaCommand(args...) 354 | return err 355 | } 356 | 357 | // https://docs.veertu.com/anka/intel/command-line-reference/#suspend 358 | type SuspendParams struct { 359 | VMName string 360 | } 361 | 362 | func (c *AnkaClient) Suspend(params SuspendParams) error { 363 | _, err := runAnkaCommand("suspend", params.VMName) 364 | return err 365 | } 366 | 367 | // https://docs.veertu.com/anka/intel/command-line-reference/#start 368 | func (c *AnkaClient) UpdateAddons(vmName string) error { 369 | args := []string{"start", "--update-addons", vmName} 370 | 371 | _, err := runAnkaCommand(args...) 372 | return err 373 | } 374 | 375 | // https://docs.veertu.com/anka/intel/command-line-reference/#version 376 | type VersionResponse struct { 377 | Status string `json:"status"` 378 | Body VersionResponseBody `json:"body"` 379 | } 380 | 381 | type VersionResponseBody struct { 382 | Product string `json:"product"` 383 | Version string `json:"version"` 384 | Build string `json:"build"` 385 | } 386 | 387 | func (c *AnkaClient) Version() (VersionResponse, error) { 388 | var response VersionResponse 389 | 390 | out, err := exec.Command("anka", "--machine-readable", "version").Output() 391 | if err != nil { 392 | return response, err 393 | } 394 | 395 | err = json.Unmarshal([]byte(out), &response) 396 | return response, err 397 | } 398 | 399 | func (c *AnkaClient) FuseAvailable(vmName string) bool { 400 | exitCode, _ := c.Run(RunParams{ 401 | VMName: vmName, 402 | Command: []string{"kextstat | grep \"com.veertu.filesystems.vtufs\" &>/dev/null"}, 403 | }) 404 | return exitCode == 0 405 | } 406 | -------------------------------------------------------------------------------- /client/client_registry.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | 11 | "github.com/hashicorp/packer-plugin-sdk/net" 12 | ) 13 | 14 | // Run command against the registry 15 | type RegistryParams struct { 16 | Remote string 17 | NodeCertPath string 18 | NodeKeyPath string 19 | CaRootPath string 20 | IsInsecure bool 21 | HostArch string 22 | } 23 | 24 | // https://docs.veertu.com/anka/intel/command-line-reference/#registry-list 25 | type RegistryListResponse struct { 26 | Latest string `json:"latest"` 27 | ID string `json:"id"` 28 | Name string `json:"name"` 29 | } 30 | 31 | func (c *AnkaClient) RegistryList(registryParams RegistryParams) ([]RegistryListResponse, error) { 32 | var response []RegistryListResponse 33 | 34 | output, err := runRegistryCommand(registryParams, "list") 35 | if err != nil { 36 | return nil, err 37 | } 38 | if output.Status != "OK" { 39 | log.Print("Error executing registry list command: ", output.ExceptionType, " ", output.Message) 40 | return nil, fmt.Errorf(output.Message) 41 | } 42 | 43 | err = json.Unmarshal(output.Body, &response) 44 | if err != nil { 45 | return response, err 46 | } 47 | 48 | return response, nil 49 | } 50 | 51 | type RegistryRemote struct { 52 | Default bool `json:"default"` 53 | Url string `json:"url"` 54 | Name string `json:"name"` 55 | } 56 | 57 | func (c *AnkaClient) RegistryListRepos() ([]RegistryRemote, error) { 58 | var response []RegistryRemote 59 | 60 | output, err := runRegistryCommand(RegistryParams{}, "list-repos") 61 | if err != nil { 62 | return response, err 63 | } 64 | if output.Status != "OK" { 65 | log.Print("Error executing 'registry list-repos' command: ", output.ExceptionType, " ", output.Message) 66 | return response, fmt.Errorf(output.Message) 67 | } 68 | err = json.Unmarshal(output.Body, &response) 69 | if err != nil { 70 | return response, err 71 | } 72 | 73 | return response, nil 74 | } 75 | 76 | type RegistryPullParams struct { 77 | VMID string 78 | Tag string 79 | Local bool 80 | Shrink bool 81 | } 82 | 83 | func (c *AnkaClient) RegistryPull(registryParams RegistryParams, pullParams RegistryPullParams) error { 84 | cmdArgs := []string{"pull"} 85 | 86 | if pullParams.Tag != "" { 87 | cmdArgs = append(cmdArgs, "--tag", pullParams.Tag) 88 | } 89 | 90 | if pullParams.Local { 91 | cmdArgs = append(cmdArgs, "--local") 92 | 93 | if pullParams.Shrink { 94 | cmdArgs = append(cmdArgs, "--shrink") 95 | } 96 | } 97 | 98 | cmdArgs = append(cmdArgs, pullParams.VMID) 99 | 100 | output, err := runRegistryCommand(registryParams, cmdArgs...) 101 | if err != nil { 102 | return err 103 | } 104 | if output.Status != "OK" { 105 | return fmt.Errorf(output.Message) 106 | } 107 | 108 | return nil 109 | } 110 | 111 | // https://docs.veertu.com/anka/intel/command-line-reference/#registry-push 112 | type RegistryPushParams struct { 113 | VMID string 114 | Tag string 115 | Description string 116 | RemoteVM string 117 | Local bool 118 | Force bool 119 | } 120 | 121 | func (c *AnkaClient) RegistryPush(registryParams RegistryParams, pushParams RegistryPushParams) error { 122 | cmdArgs := []string{"push"} 123 | 124 | if pushParams.Tag != "" { 125 | cmdArgs = append(cmdArgs, "--tag", pushParams.Tag) 126 | } 127 | 128 | if pushParams.Description != "" { 129 | cmdArgs = append(cmdArgs, "--description", pushParams.Description) // quotes aren't needed 130 | } 131 | 132 | if pushParams.RemoteVM != "" { 133 | cmdArgs = append(cmdArgs, "--remote-vm", pushParams.RemoteVM) 134 | } 135 | 136 | if pushParams.Local { 137 | cmdArgs = append(cmdArgs, "--local") 138 | } 139 | 140 | if pushParams.Force { 141 | cmdArgs = append(cmdArgs, "--force") 142 | } 143 | 144 | cmdArgs = append(cmdArgs, pushParams.VMID) 145 | 146 | output, err := runRegistryCommand(registryParams, cmdArgs...) 147 | if err != nil { 148 | return err 149 | } 150 | if output.Status != "OK" { 151 | return fmt.Errorf(output.Message) 152 | } 153 | 154 | return nil 155 | } 156 | 157 | // https://docs.veertu.com/anka/anka-build-cloud/working-with-registry-and-api/#revert 158 | func (c *AnkaClient) RegistryRevert(url string, id string) error { 159 | response, err := registryRESTRequest("DELETE", fmt.Sprintf("%s/registry/revert?id=%s", url, id), nil) 160 | if err != nil { 161 | return err 162 | } 163 | if response.Status != statusOK { 164 | return fmt.Errorf("failed to revert VM on registry: %s", response.Message) 165 | } 166 | 167 | return nil 168 | } 169 | 170 | func registryRESTRequest(method string, url string, body io.Reader) (MachineReadableOutput, error) { 171 | request, err := http.NewRequest(method, url, body) 172 | if err != nil { 173 | return MachineReadableOutput{}, err 174 | } 175 | 176 | httpClient := net.HttpClientWithEnvironmentProxy() 177 | 178 | resp, err := httpClient.Do(request) 179 | if err != nil { 180 | return MachineReadableOutput{}, err 181 | } 182 | 183 | if resp.StatusCode == 200 { 184 | defer resp.Body.Close() 185 | 186 | body, err := ioutil.ReadAll(resp.Body) 187 | if err != nil { 188 | return MachineReadableOutput{}, err 189 | } 190 | 191 | return parseOutput(body) 192 | } 193 | 194 | return MachineReadableOutput{}, fmt.Errorf("unsupported http response code: %d", resp.StatusCode) 195 | } 196 | -------------------------------------------------------------------------------- /client/runner.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | "time" 9 | 10 | "github.com/hashicorp/packer-plugin-sdk/packer" 11 | ) 12 | 13 | type Runner struct { 14 | params RunParams 15 | cmd *exec.Cmd 16 | started time.Time 17 | } 18 | 19 | func NewRunner(params RunParams) *Runner { 20 | args := []string{} 21 | 22 | if params.Debug { 23 | args = append(args, "--log-level", "debug") 24 | } 25 | 26 | if params.Stdout == nil { 27 | params.Stdout = os.Stdout 28 | } 29 | 30 | if params.Stderr == nil { 31 | params.Stderr = os.Stderr 32 | } 33 | 34 | args = append(args, "run") 35 | 36 | if params.Volume != "" { 37 | args = append(args, "-v", params.Volume) 38 | } else { 39 | args = append(args, "-n") 40 | } 41 | 42 | if params.WaitForNetworking { 43 | args = append(args, "--wait-network") 44 | } 45 | 46 | if params.WaitForTimeSync { 47 | args = append(args, "--wait-time") 48 | } 49 | 50 | args = append(args, params.VMName) 51 | args = append(args, "sh") 52 | args = append(args, "-s") 53 | 54 | cmd := exec.Command("anka", args...) 55 | cmd.Stdout = params.Stdout 56 | cmd.Stderr = params.Stderr 57 | 58 | return &Runner{ 59 | params: params, 60 | cmd: cmd, 61 | } 62 | } 63 | 64 | func (r *Runner) Start() error { 65 | log.Printf("Starting command: %s", strings.Join(r.cmd.Args, " ")) 66 | r.started = time.Now() 67 | 68 | stdin, err := r.cmd.StdinPipe() 69 | if err != nil { 70 | return err 71 | } 72 | 73 | defer stdin.Close() 74 | 75 | err = r.cmd.Start() 76 | if err != nil { 77 | return err 78 | } 79 | 80 | cmdString := strings.Join(r.params.Command, " ") 81 | 82 | log.Print("Executing: ", cmdString) 83 | 84 | _, err = stdin.Write([]byte(cmdString)) 85 | return err 86 | } 87 | 88 | func (r *Runner) Wait() (int, error) { 89 | err := r.cmd.Wait() 90 | 91 | log.Printf("Command finished in %s %v", time.Since(r.started), err) 92 | 93 | return getExitCode(err), err 94 | } 95 | 96 | // GetExitCode extracts an exit code from an error where the platform supports it, 97 | // otherwise returns 0 for no error and 1 for an error 98 | func getExitCode(err error) int { 99 | if err == nil { 100 | return 0 101 | } 102 | 103 | eerr, ok := err.(*exec.ExitError) 104 | if ok { 105 | code := eerr.ExitCode() 106 | if code == 125 { 107 | code = packer.CmdDisconnect 108 | } 109 | 110 | return code 111 | } 112 | 113 | return 1 114 | } 115 | -------------------------------------------------------------------------------- /common/errors.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // VMAlreadyExistsError returns the vm already exists error 4 | type VMAlreadyExistsError struct{} 5 | 6 | func (obj *VMAlreadyExistsError) Error() string { 7 | return "vm already exists" 8 | } 9 | 10 | // VMNotFoundException returns the vm not found error 11 | type VMNotFoundException struct{} 12 | 13 | func (obj *VMNotFoundException) Error() string { 14 | return "vm not found" 15 | } 16 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Packer](https://www.packer.io/) Plugin for building images that work with [Veertu's Anka macOS Virtualization tool](https://veertu.com/). 2 | 3 | ### Installation 4 | 5 | To install this plugin, copy and paste this code into your Packer configuration, then run [`packer init`](https://www.packer.io/docs/commands/init). 6 | 7 | ```hcl 8 | packer { 9 | required_plugins { 10 | veertu-anka = { 11 | version = "= v4.0.0" 12 | source = "github.com/veertuinc/veertu-anka" 13 | } 14 | } 15 | } 16 | ``` 17 | 18 | Alternatively, you can use `packer plugins install` to manage installation of this plugin. 19 | 20 | ```sh 21 | $ packer plugins install github.com/veertuinc/veertu-anka 22 | ``` 23 | 24 | ### Components 25 | ~> For use with the post-processor, it's important to use `anka registry add` to [set your default registry on the machine building your templates/tags](https://docs.veertu.com/anka/apple/command-line-reference/#registry-add). 26 | 27 | #### Builders 28 | - [veertu-anka-vm-clone](/packer/integrations/veertuinc/veertu-anka/latest/components/builder/clone) - Packer builder is able to clone existing Anka VM Templates for use with the [Anka Virtualization](https://veertu.com/technology/) package and the [Anka Build Cloud](https://veertu.com/anka-build/). The builder takes a source VM name, clones it, and then runs any provisioning necessary on the new VM Template before stopping or suspending it. 29 | - [veertu-anka-vm-create- ](/packer/integrations/veertuinc/veertu-anka/latest/components/builder/create) Packer builder is able to create new Anka VM Templates for use with the 30 | [Anka Virtualization](https://veertu.com/technology/) package and the [Anka Build Cloud](https://veertu.com/anka-build/). The builder takes the path to macOS installer .app 31 | and installs that macOS version inside of an Anka VM Template. 32 | 33 | #### Post-Processors 34 | - [veertu-anka-registry-push](/packer/integrations/veertuinc/veertu-anka/latest/components/post-processor/veertu-anka-registry-push) Packer Post Processor is able to push your created Anka VM templates to 35 | the [Anka Build Cloud Registry](https://veertu.com/anka-build/) through the [Anka Virtualization](https://veertu.com/technology/) package. 36 | -------------------------------------------------------------------------------- /docs/builders/vm-clone.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | description: | 3 | The veertu-anka-vm-clone Packer builder is able to clone existing Anka VM Templates for use with Anka Virtualization and the Anka Build Cloud. The builder takes a source VM name, clones it, and then runs any provisioning necessary on the new VM Template before stopping or suspending it. 4 | page_title: Anka VM Clone - Builders 5 | nav_title: VM Clone 6 | --- 7 | 8 | # Anka VM Clone Builder 9 | 10 | Type: `veertu-anka-vm-clone` 11 | 12 | The `veertu-anka-vm-clone` Packer builder is able to clone existing Anka VM Templates for use with the [Anka Virtualization](https://veertu.com/technology/) package and the [Anka Build Cloud](https://veertu.com/anka-build/). The builder takes a source VM name, clones it, and then runs any provisioning necessary on the new VM Template before stopping or suspending it. 13 | 14 | The builder does _not_ manage templates. Once a template is created, it is up 15 | to you to use it or delete it. 16 | 17 | ## Important Notes 18 | 19 | **In Anka 3.0** we now require a tagged source VM before cloning in order to share the underlying .ank image and optimize disk space. If your source VM is not tagged yet, we will assign one . **We highly recommend pushing this VM Template/Tag to your registry so [disk usage is optimized](https://docs.veertu.com/anka/apple/getting-started/creating-your-first-vm/#disk-optimization).** 20 | 21 | ## Configuration Reference 22 | 23 | There are many configuration options available for the builder. They are segmented below into two categories: required and optional parameters. 24 | 25 | ### _**Required Configuration**_ 26 | 27 | * `source_vm_name` (String) The VM to clone for provisioning, either stopped or suspended. 28 | 29 | * `type` (String) Must be `veertu-anka-vm-clone`. 30 | 31 | ### _**Optional Configuration**_ 32 | 33 | * `vm_name` (String) The name for the VM that is created. 34 | 35 | > Generated using the source_vm_name if not provided: (`{{ source_vm_name }}-{10RandomChars}`). 36 | 37 | * `vcpu_count` (String) The number of vCPU cores, defaults to `2`. 38 | 39 | * `ram_size` (String) The size in "[0-9]+G" format, defaults to `2G`. 40 | 41 | * `disk_size` (String) The size in "[0-9]+G" format, defaults to `25G`. 42 | 43 | > We will automatically resize the internal disk for you by executing `diskutil apfs resizeContainer disk0s2 0` inside of the VM. 44 | 45 | * `stop_vm` (Boolean) Whether or not to stop the vm after it has been created, defaults to false. 46 | 47 | * `display_controller` (string) The display controller to set (run `anka modify VMNAME set display --help` to see available options). 48 | 49 | * `always_fetch` (Boolean) Always pull the source VM from the registry. Defaults to false. 50 | 51 | * `boot_delay` (String) The time to wait before running packer provisioner commands, defaults to `7s`. 52 | 53 | * `cacert` (String) Path to a CA Root certificate. 54 | 55 | * `cert` (String) Path to your node's client certificate to use for registry communication (if certificate authorization is enabled). 56 | 57 | * `insecure` (Boolean) Skip TLS verification. 58 | 59 | * `key` (String) Path to your node's client certificate key, if the `cert` certificate doesn't contain one, to use for registry communication (if certificate authorization is enabled). 60 | 61 | * `hw_uuid` (String) (Anka 2 only) The Hardware UUID you wish to set (usually generated with `uuidgen`). 62 | 63 | * `port_forwarding_rules` (Struct) 64 | 65 | > If port forwarding rules are already set and you want to not have them fail the packer build, use `packer build --force`. 66 | 67 | * `port_forwarding_guest_port` (Int) 68 | * `port_forwarding_host_port` (Int) 69 | * `port_forwarding_rule_name` (String) 70 | 71 | * `registry-path` (String) The registry URL (will use your default configuration if not set). 72 | 73 | * `remote` (String) The registry name (will use your default configuration if not set). 74 | 75 | > This takes priority in Anka 3 and `registry-path` will be ignored. 76 | 77 | * `source_vm_tag` (String) Specify the tag of the VM we want to clone instead of using the default. Also the tag to target when pulling from the registry (defaults to latest tag). 78 | 79 | * `update_addons` (Boolean) (Anka 2 only) Update the vm addons. Defaults to false. 80 | 81 | * `use_anka_cp` (Boolean) Use built in anka cp command. Defaults to false. 82 | 83 | ## Example 84 | 85 | Here is an example that uses the file and shell provisioners. 86 | 87 | ```hcl 88 | 89 | variable "source_vm_name" { 90 | type = string 91 | default = "anka-packer-base-macos" 92 | } 93 | 94 | variable "vm_name" { 95 | type = string 96 | default = "anka-packer-from-source" 97 | } 98 | 99 | source "veertu-anka-vm-clone" "clone" { 100 | vm_name = "${var.vm_name}" 101 | source_vm_name = "${var.source_vm_name}" 102 | } 103 | 104 | build { 105 | sources = [ 106 | "source.veertu-anka-vm-clone.clone", 107 | ] 108 | provisioner "file" { 109 | destination = "/private/tmp/" 110 | source = "./examples/ansible" 111 | } 112 | provisioner "shell" { 113 | inline = [ 114 | "[[ ! -d /tmp/ansible ]] && exit 100", 115 | "touch /tmp/ansible/test1" 116 | ] 117 | } 118 | provisioner "file" { 119 | destination = "./" 120 | direction = "download" 121 | source = "/private/tmp/ansible/test1" 122 | } 123 | provisioner "shell-local" { 124 | inline = [ 125 | "[[ ! -f ./test1 ]] && exit 200", 126 | "rm -f ./test1" 127 | ] 128 | } 129 | } 130 | 131 | ``` 132 | -------------------------------------------------------------------------------- /docs/builders/vm-create.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | description: | 3 | The veertu-anka-vm-create Packer builder is able to create new VM templates for use with 4 | Anka. The builder takes the path to macOS installer .app, installs macOS using that installer inside of an Anka VM Template, and then runs any provisioning necessary on the instance. 5 | page_title: Anka VM Create - Builders 6 | nav_title: VM Create 7 | --- 8 | 9 | # Veertu's Anka VM Create Builder 10 | 11 | Type: `veertu-anka-vm-create` 12 | 13 | **Packer 3.x will no longer support Anka 2.x. You can still however use the Packer 2.x release for support.** 14 | 15 | The `veertu-anka-vm-create` Packer builder is able to create new Anka VM Templates for use with the 16 | [Anka Virtualization](https://veertu.com/technology/) package and the [Anka Build Cloud](https://veertu.com/anka-build/). The builder takes the path to macOS installer .app 17 | and installs that macOS version inside of an Anka VM Template. 18 | 19 | The builder does _not_ manage templates. Once a template is created, it is up 20 | to you to use it or delete it. 21 | 22 | ## Configuration Reference 23 | 24 | There are many configuration options available for the builder. They are 25 | segmented below into two categories: required and optional parameters. 26 | 27 | ### Required Configuration 28 | 29 | * `installer` (String) The path to a macOS installer. This process takes about 20 minutes. 30 | - Starting in 3.1.2: This can also be set to 'latest' or a specific macOS version in order to have Anka attempt downloading the installer for you (`vm_name` will be set to `anka-packer-base-${installer}`). 31 | 32 | * `type` (String) Must be `veertu-anka-vm-create`. 33 | 34 | ### Optional Configuration 35 | 36 | * `vm_name` (String) The name for the VM that is created. One is generated with installer data if not provided (`anka-packer-base-{{ installer.OSVersion }}-{{ installer.BundlerVersion }}`). 37 | 38 | * `vcpu_count` (String) The number of vCPU cores, defaults to `2`. 39 | 40 | > This change gears us up for Anka 3.0 release when cpu_count will be vcpu_count. For now this is still CPU and not vCPU. 41 | 42 | * `ram_size` (String) The size in "[0-9]+G" format, defaults to `4G`. 43 | 44 | * `disk_size` (String) The size in "[0-9]+G" format, defaults to `40G`. 45 | 46 | > We will automatically resize the internal disk for you by executing `diskutil apfs resizeContainer disk0s2 0` inside of the VM 47 | 48 | * `stop_vm` (Boolean) Whether or not to stop the vm after it has been created, defaults to false. 49 | 50 | * `use_anka_cp` (Boolean) Use built in anka cp command. You shouldn't need this option. Defaults to false. 51 | 52 | * `anka_password` (String) Sets the password for the vm. Can also be set with `ANKA_DEFAULT_PASSWD` env var. Defaults to `admin`. 53 | 54 | * `anka_user` (String) Sets the username for the vm. Can also be set with `ANKA_DEFAULT_USER` env var. Defaults to `anka`. 55 | 56 | * `boot_delay` (String) The time to wait before running packer provisioner commands, defaults to `7s`. 57 | 58 | * `log_level` (String) The log level for Anka. This currently only supports `debug` and is only useful for VM creation failures. 59 | 60 | * `hw_uuid` (String) (Anka 2 only) The Hardware UUID you wish to set (usually generated with `uuidgen`). 61 | 62 | * `port_forwarding_rules` (Struct) 63 | 64 | > If port forwarding rules are already set and you want to not have them fail the packer build, use `packer build --force`. 65 | 66 | * `port_forwarding_guest_port` (Int) 67 | * `port_forwarding_host_port` (Int) 68 | * `port_forwarding_rule_name` (String) 69 | 70 | * `display_controller` (string) The display controller to set (run `anka modify VMNAME set display --help` to see available options). 71 | 72 | ## Example 73 | 74 | Here is an example: 75 | 76 | ```hcl 77 | 78 | variable "vm_name" { 79 | type = string 80 | default = "anka-packer-base-macos" 81 | } 82 | 83 | variable "installer" { 84 | type = string 85 | default = "/Applications/Install macOS Big Sur.app/" 86 | } 87 | 88 | variable "vcpu_count" { 89 | type = string 90 | default = "" 91 | } 92 | 93 | source "veertu-anka-vm-create" "base" { 94 | installer = "${var.installer}" 95 | vm_name = "${var.vm_name}" 96 | vcpu_count = "${var.vcpu_count}" 97 | } 98 | 99 | build { 100 | sources = [ 101 | "source.veertu-anka-vm-create.base" 102 | ] 103 | 104 | provisioner "shell" { 105 | inline = [ 106 | "echo hello world", 107 | "echo llamas rock" 108 | ] 109 | } 110 | } 111 | 112 | ``` 113 | -------------------------------------------------------------------------------- /docs/metadata.hcl: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # Details on using this Integration template can be found at https://github.com/hashicorp/integration-template 5 | # Alternatively this metadata.hcl file can be placed under the docs/ subdirectory or any other config subdirectory that 6 | # makes senses for the plugin. 7 | integration { 8 | name = "Anka" 9 | description = "This is a packer plugin for building macOS VM templates and tags using the Anka Virtualization CLI" 10 | identifier = "packer/veertuinc/veertu-anka" 11 | flags = [ 12 | # Remove if the plugin does not conform to the HCP Packer requirements. 13 | # 14 | # Please refer to our docs if you want your plugin to be compatible with 15 | # HCP Packer: https://developer.hashicorp.com/packer/docs/plugins/creation/hcp-support 16 | # "hcp-ready", 17 | # This signals that the plugin is unmaintained and will eventually not be 18 | # working with a future version of Packer. 19 | # 20 | # On the integrations, this will end-up as an icon on the plugin's main card. 21 | # "archived", 22 | ] 23 | docs { 24 | process_docs = true 25 | # We recommend using the default readme_location of just `./README.md` here 26 | # This projects README needs to document the interface of an integration. 27 | # 28 | # If you need a separate README from what you will display on GitHub vs 29 | # what is shown on HashiCorp Developer, this is totally valid, though! 30 | readme_location = "./README.md" 31 | external_url = "https://github.com/veertuinc/packer-plugin-veertu-anka" 32 | } 33 | license { 34 | type = "MIT License" 35 | url = "https://github.com/veertuinc/packer-plugin-veertu-anka/blob/main/LICENSE" 36 | } 37 | component { 38 | type = "builder" 39 | name = "Anka VM Create" 40 | slug = "veertu-anka-vm-create" 41 | } 42 | component { 43 | type = "builder" 44 | name = "Anka VM Clone" 45 | slug = "veertu-anka-vm-clone" 46 | } 47 | component { 48 | type = "provisioner" 49 | name = "Anka Build Cloud Registry Push" 50 | slug = "veertu-anka-registry-push" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/post-processors/anka-registry-push.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | description: | 3 | The veertu-anka-registry-push Packer Post Processor is able to push your created VM template to the Anka Build Cloud Registry. 4 | page_title: Anka Build Cloud Registry Push - Post Processors 5 | nav_title: Anka Registry Push 6 | --- 7 | 8 | # Anka Build Cloud Registry Push Post-Processor 9 | 10 | Type: `veertu-anka-registry-push` 11 | 12 | The `veertu-anka-registry-push` Packer Post Processor is able to push your created Anka VM templates to the [Anka Build Cloud Registry](https://veertu.com/anka-build/) through the [Anka Virtualization](https://veertu.com/technology/) package. 13 | 14 | This post-processor is part of the [Veertu Anka plugin](https://github.com/veertuinc/packer-plugin-veertu-anka). To install this plugin using `packer init`, add the following Packer block to your hcl template: 15 | 16 | ```hcl 17 | packer { 18 | required_plugins { 19 | veertu-anka = { 20 | version = "= 3.2.0" 21 | source = "github.com/veertuinc/veertu-anka" 22 | } 23 | } 24 | } 25 | ``` 26 | 27 | ## Configuration Reference 28 | 29 | There are many configuration options available for the post-processor. They are 30 | segmented below into two categories: required and optional parameters. 31 | 32 | ### _**Required Configuration**_ 33 | 34 | * `type` (String) **Must be `veertu-anka-registry-push`; no other types** 35 | 36 | ### _**Optional Configuration**_ 37 | 38 | * `cacert` (String) Path to a CA Root certificate. 39 | 40 | * `cert` (String) Path to your node certificate (if certificate authority is enabled). 41 | 42 | * `description` (String) The description of the tag. 43 | 44 | * `insecure` (Boolean) Skip TLS verification. 45 | 46 | * `key` (String) Path to your node certificate key if the client/node certificate doesn't contain one. 47 | 48 | * `local` (Boolean) Assign a tag to your local template and avoid pushing to the Registry. 49 | 50 | * `remote` (String) The registry URL or name to target for registry operation (will use your default configuration if not set). 51 | 52 | * `remote_vm` (String) The name of a registry template you want to push the local template onto. 53 | 54 | * `tag` (String) The name of the tag to push (will default as 'latest' if not set). 55 | 56 | * `force` (Boolean) Whether or not to forcefully push, regardless of a tag already existing 57 | 58 | ## Other 59 | 60 | When using `packer build -force`, the post-processor will issue a [revert API call](https://docs.veertu.com/anka/anka-build-cloud/working-with-registry-and-api/#revert) to remove the existing tag before pushing the new. 61 | 62 | ## Example 63 | 64 | Here is an example that uses the file and shell provisioners. 65 | 66 | ```hcl 67 | 68 | variable "source_vm_name" { 69 | type = string 70 | default = "anka-packer-base-macos" 71 | } 72 | 73 | variable "vm_name" { 74 | type = string 75 | default = "anka-packer-from-source" 76 | } 77 | 78 | source "veertu-anka-vm-clone" "clone" { 79 | vm_name = "${var.vm_name}" 80 | source_vm_name = "${var.source_vm_name}" 81 | } 82 | 83 | build { 84 | sources = [ 85 | "source.veertu-anka-vm-clone.clone", 86 | ] 87 | 88 | provisioner "file" { 89 | destination = "/private/tmp/" 90 | source = "./examples/ansible" 91 | } 92 | provisioner "shell" { 93 | inline = [ 94 | "[[ ! -d /tmp/ansible ]] && exit 100", 95 | "touch /tmp/ansible/test1" 96 | ] 97 | } 98 | 99 | post-processor "veertu-anka-registry-push" { 100 | tag = "v2" 101 | } 102 | } 103 | 104 | ``` 105 | -------------------------------------------------------------------------------- /examples/.cicd/should-fail-disk-shrink.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "vm_name" { 7 | type = string 8 | default = "anka-packer-from-source" 9 | } 10 | 11 | source "veertu-anka-vm-clone" "anka-packer-from-source" { 12 | vm_name = "${var.vm_name}" 13 | source_vm_name = "${var.source_vm_name}" 14 | disk_size = "20G" 15 | } 16 | 17 | build { 18 | sources = [ 19 | "source.veertu-anka-vm-clone.anka-packer-from-source", 20 | ] 21 | } -------------------------------------------------------------------------------- /examples/.cicd/should-fail-exit-code.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "vm_name" { 7 | type = string 8 | default = "anka-packer-from-source" 9 | } 10 | 11 | source "veertu-anka-vm-clone" "anka-packer-from-source" { 12 | vm_name = "${var.vm_name}" 13 | source_vm_name = "${var.source_vm_name}" 14 | } 15 | 16 | build { 17 | sources = [ 18 | "source.veertu-anka-vm-clone.anka-packer-from-source", 19 | ] 20 | provisioner "shell" { 21 | inline = [ 22 | "exit 50" 23 | ] 24 | } 25 | } -------------------------------------------------------------------------------- /examples/ansible/ansible.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "variables": { 4 | "source_vm_name": "" 5 | }, 6 | "provisioners": [ 7 | { 8 | "type": "ansible", 9 | "user": "anka", 10 | "ansible_env_vars": [ "ANSIBLE_HOST_KEY_CHECKING=False", "ANSIBLE_SSH_ARGS='-o ForwardAgent=yes -o ControlMaster=auto -o ControlPersist=60s'", "ANSIBLE_NOCOLOR=True" ], 11 | "playbook_file": "./playbook.yml" 12 | } 13 | ], 14 | "builders": [{ 15 | "type": "veertu-anka-vm-clone", 16 | "disk_size": "30G", 17 | "source_vm_name": "{{user `source_vm_name`}}", 18 | "vm_name": "macos-from-{{user `source_vm_name`}}" 19 | }] 20 | } -------------------------------------------------------------------------------- /examples/ansible/playbook.yml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | - hosts: default 4 | gather_facts: no 5 | tasks: 6 | - name: hello 7 | raw: echo "hello ansible and packer" 8 | - name: command module 9 | command: ls -l 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/clone-existing-with-expect-disconnect.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "vm_name" { 7 | type = string 8 | default = "anka-packer-from-source-with-expect-disconnect" 9 | } 10 | 11 | source "veertu-anka-vm-clone" "anka-packer-from-source-with-expect-disconnect" { 12 | vm_name = "${var.vm_name}" 13 | source_vm_name = "${var.source_vm_name}" 14 | } 15 | 16 | build { 17 | sources = [ 18 | "source.veertu-anka-vm-clone.anka-packer-from-source-with-expect-disconnect", 19 | ] 20 | 21 | provisioner "shell" { 22 | inline = [ 23 | "set -x", 24 | "echo PRE REBOOT", 25 | "sleep 5", 26 | "sudo reboot", 27 | "echo SHOULD NOT SEE THIS ECHO" 28 | ] 29 | expect_disconnect = true 30 | } 31 | 32 | provisioner "shell" { 33 | inline = [ 34 | "set -x", 35 | "echo REBOOTED" 36 | ] 37 | } 38 | } -------------------------------------------------------------------------------- /examples/clone-existing-with-file-provisioner.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "vm_name" { 7 | type = string 8 | default = "anka-packer-from-source" 9 | } 10 | 11 | variable "host_path" { 12 | type = string 13 | default = "/tmp/" 14 | } 15 | 16 | source "veertu-anka-vm-clone" "anka-packer-from-source" { 17 | vm_name = "${var.vm_name}" 18 | source_vm_name = "${var.source_vm_name}" 19 | } 20 | 21 | build { 22 | sources = [ 23 | "source.veertu-anka-vm-clone.anka-packer-from-source", 24 | ] 25 | provisioner "file" { 26 | destination = "/private/tmp/" 27 | source = "${var.host_path}/examples/ansible" 28 | } 29 | provisioner "shell" { 30 | inline = [ 31 | "[[ ! -d /tmp/ansible ]] && exit 100", 32 | "touch /tmp/ansible/test1" 33 | ] 34 | } 35 | provisioner "file" { 36 | destination = "./" 37 | direction = "download" 38 | source = "/private/tmp/ansible/test1" 39 | } 40 | provisioner "shell-local" { 41 | inline = [ 42 | "[[ ! -f ./test1 ]] && exit 200", 43 | "rm -f ./test1" 44 | ] 45 | } 46 | } -------------------------------------------------------------------------------- /examples/clone-existing-with-hwuuid.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "hw_uuid" { 7 | type = string 8 | default = "" 9 | } 10 | 11 | variable "vm_name" { 12 | type = string 13 | default = "anka-packer-from-source-with-hwuuid" 14 | } 15 | 16 | source "veertu-anka-vm-clone" "anka-packer-from-source-with-hwuuid" { 17 | vm_name = "${var.vm_name}" 18 | source_vm_name = "${var.source_vm_name}" 19 | hw_uuid = "${var.hw_uuid}" 20 | vcpu_count = 10 21 | } 22 | 23 | build { 24 | sources = [ 25 | "source.veertu-anka-vm-clone.anka-packer-from-source-with-hwuuid", 26 | ] 27 | 28 | provisioner "shell" { 29 | inline = [ 30 | "echo hello world", 31 | "echo llamas rock" 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /examples/clone-existing-with-new-disk-size.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "source_vm_tag" { 7 | type = string 8 | default = "latest" 9 | } 10 | 11 | variable "vm_name" { 12 | type = string 13 | default = "anka-packer-from-source-with-new-disk-size" 14 | } 15 | 16 | source "veertu-anka-vm-clone" "anka-packer-from-source-with-new-disk-size" { 17 | vm_name = "${var.vm_name}" 18 | source_vm_name = "${var.source_vm_name}" 19 | source_vm_tag = "${var.source_vm_tag}" 20 | vcpu_count = 8 21 | ram_size = "10G" 22 | disk_size = "200G" 23 | } 24 | 25 | build { 26 | sources = [ 27 | "source.veertu-anka-vm-clone.anka-packer-from-source-with-new-disk-size", 28 | ] 29 | 30 | provisioner "shell" { 31 | inline = [ 32 | "echo hello world", 33 | "echo llamas rock" 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /examples/clone-existing-with-pg-display_controller.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "display_controller" { 7 | type = string 8 | default = "pg" 9 | } 10 | 11 | variable "vm_name" { 12 | type = string 13 | default = "anka-packer-from-source-with-display_controller" 14 | } 15 | 16 | source "veertu-anka-vm-clone" "anka-packer-from-source-with-display_controller" { 17 | vm_name = "${var.vm_name}" 18 | source_vm_name = "${var.source_vm_name}" 19 | vcpu_count = 10 20 | display_controller = "${var.display_controller}" 21 | stop_vm = true 22 | } 23 | 24 | build { 25 | sources = [ 26 | "source.veertu-anka-vm-clone.anka-packer-from-source-with-display_controller", 27 | ] 28 | 29 | } -------------------------------------------------------------------------------- /examples/clone-existing-with-port-forwarding-rules.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "source_vm_tag" { 7 | type = string 8 | default = "latest" 9 | } 10 | 11 | variable "vm_name" { 12 | type = string 13 | default = "anka-packer-from-source-with-port-rules" 14 | } 15 | 16 | source "veertu-anka-vm-clone" "anka-packer-from-source-with-port-rules" { 17 | vm_name = "${var.vm_name}" 18 | source_vm_name = "${var.source_vm_name}" 19 | source_vm_tag = "${var.source_vm_tag}" 20 | port_forwarding_rules { 21 | port_forwarding_guest_port = 80 22 | port_forwarding_host_port = 12345 23 | port_forwarding_rule_name = "website" 24 | } 25 | port_forwarding_rules { 26 | port_forwarding_guest_port = 8080 27 | } 28 | vcpu_count = 8 29 | ram_size = "10G" 30 | } 31 | 32 | build { 33 | sources = [ 34 | "source.veertu-anka-vm-clone.anka-packer-from-source-with-port-rules", 35 | ] 36 | 37 | provisioner "shell" { 38 | inline = [ 39 | "echo hello world", 40 | "echo llamas rock" 41 | ] 42 | } 43 | } -------------------------------------------------------------------------------- /examples/clone-existing-with-post-processing.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "vm_name" { 7 | type = string 8 | default = "anka-packer-from-source-with-post-processing" 9 | } 10 | 11 | variables { 12 | OSVersion = "" 13 | DarwinVersion = "" 14 | } 15 | 16 | source "veertu-anka-vm-clone" "anka-packer-from-source-with-post-processing" { 17 | vm_name = "${var.vm_name}" 18 | source_vm_name = "${var.source_vm_name}" 19 | } 20 | 21 | build { 22 | sources = [ 23 | "source.veertu-anka-vm-clone.anka-packer-from-source-with-post-processing", 24 | ] 25 | 26 | post-processor "veertu-anka-registry-push" { 27 | tag = "${build.OSVersion}-${build.DarwinVersion}" 28 | description = "Xcode 14.1, Fastlane X.X, Go, Brew, Git" 29 | } 30 | } -------------------------------------------------------------------------------- /examples/clone-existing-with-update_addons.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "vm_name" { 7 | type = string 8 | default = "anka-packer-from-source-with-update_addons" 9 | } 10 | 11 | source "veertu-anka-vm-clone" "anka-packer-from-source-with-update_addons" { 12 | vm_name = "${var.vm_name}" 13 | source_vm_name = "${var.source_vm_name}" 14 | update_addons = true 15 | stop_vm = true 16 | } 17 | 18 | build { 19 | sources = [ 20 | "source.veertu-anka-vm-clone.anka-packer-from-source-with-update_addons", 21 | ] 22 | 23 | provisioner "shell" { 24 | inline = [ 25 | "echo hello world", 26 | "echo llamas rock" 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /examples/clone-existing-with-use-anka-cp.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "vm_name" { 7 | type = string 8 | default = "anka-packer-from-source-with-use-anka-cp" 9 | } 10 | 11 | source "veertu-anka-vm-clone" "anka-packer-from-source-with-use-anka-cp" { 12 | vm_name = "${var.vm_name}" 13 | source_vm_name = "${var.source_vm_name}" 14 | use_anka_cp = true 15 | } 16 | 17 | build { 18 | sources = [ 19 | "source.veertu-anka-vm-clone.anka-packer-from-source-with-use-anka-cp", 20 | ] 21 | 22 | provisioner "shell" { 23 | inline = [ 24 | "echo hello world", 25 | "echo llamas rock" 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /examples/clone-existing.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "source_vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "vm_name" { 7 | type = string 8 | default = "anka-packer-from-source" 9 | } 10 | 11 | source "veertu-anka-vm-clone" "anka-packer-from-source" { 12 | vm_name = "${var.vm_name}" 13 | source_vm_name = "${var.source_vm_name}" 14 | } 15 | 16 | build { 17 | sources = [ 18 | "source.veertu-anka-vm-clone.anka-packer-from-source", 19 | ] 20 | 21 | provisioner "shell" { 22 | inline = [ 23 | "echo hello world", 24 | "echo llamas rock" 25 | ] 26 | } 27 | } -------------------------------------------------------------------------------- /examples/create-from-installer-with-port-forwarding-rules.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "installer" { 7 | type = string 8 | default = "/Applications/Install macOS Big Sur.app/" 9 | } 10 | 11 | variable "vcpu_count" { 12 | type = string 13 | default = "" 14 | } 15 | 16 | source "veertu-anka-vm-create" "base" { 17 | installer = "${var.installer}" 18 | vm_name = "${var.vm_name}" 19 | vcpu_count = "${var.vcpu_count}" 20 | port_forwarding_rules { 21 | port_forwarding_guest_port = 80 22 | port_forwarding_host_port = 12345 23 | port_forwarding_rule_name = "website" 24 | } 25 | port_forwarding_rules { 26 | port_forwarding_guest_port = 8080 27 | } 28 | } 29 | 30 | build { 31 | sources = [ 32 | "source.veertu-anka-vm-create.base" 33 | ] 34 | 35 | provisioner "shell" { 36 | inline = [ 37 | "echo hello world", 38 | "echo llamas rock" 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /examples/create-from-installer-with-post-processing.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos-post-processor" 4 | } 5 | 6 | variable "installer" { 7 | type = string 8 | default = "/Applications/Install macOS Big Sur.app/" 9 | } 10 | 11 | source "veertu-anka-vm-create" "anka-packer-base-macos-post-processor" { 12 | installer = "${var.installer}" 13 | vm_name = "${var.vm_name}" 14 | } 15 | 16 | build { 17 | sources = [ 18 | "source.veertu-anka-vm-create.anka-packer-base-macos-post-processor" 19 | ] 20 | provisioner "shell" { 21 | inline = [ 22 | "echo hello world", 23 | "echo llamas rock" 24 | ] 25 | } 26 | post-processor "veertu-anka-registry-push" { 27 | tag = "veertu-registry-push-test" 28 | } 29 | } -------------------------------------------------------------------------------- /examples/create-from-installer.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "installer" { 7 | type = string 8 | default = "/Applications/Install macOS Big Sur.app/" 9 | } 10 | 11 | variable "vcpu_count" { 12 | type = string 13 | default = "" 14 | } 15 | 16 | source "veertu-anka-vm-create" "base" { 17 | installer = "${var.installer}" 18 | vm_name = "${var.vm_name}" 19 | vcpu_count = "${var.vcpu_count}" 20 | } 21 | 22 | build { 23 | sources = [ 24 | "source.veertu-anka-vm-create.base" 25 | ] 26 | 27 | provisioner "shell" { 28 | inline = [ 29 | "echo hello world", 30 | "echo llamas rock" 31 | ] 32 | } 33 | } -------------------------------------------------------------------------------- /examples/create-stopped-from-installer.pkr.hcl: -------------------------------------------------------------------------------- 1 | variable "vm_name" { 2 | type = string 3 | default = "anka-packer-base-macos" 4 | } 5 | 6 | variable "vcpu_count" { 7 | type = string 8 | default = "" 9 | } 10 | 11 | variable "installer" { 12 | type = string 13 | default = "/Applications/Install macOS Big Sur.app/" 14 | } 15 | 16 | source "veertu-anka-vm-create" "anka-packer-base-macos" { 17 | installer = "${var.installer}" 18 | vm_name = "${var.vm_name}" 19 | vcpu_count = "${var.vcpu_count}" 20 | stop_vm = true 21 | } 22 | 23 | build { 24 | sources = [ 25 | "source.veertu-anka-vm-create.anka-packer-base-macos" 26 | ] 27 | 28 | provisioner "shell" { 29 | inline = [ 30 | "echo hello world", 31 | "echo llamas rock" 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /examples/pull-and-clone-existing.pkr.hcl: -------------------------------------------------------------------------------- 1 | // packer { 2 | // required_plugins { 3 | // veertu-anka = { 4 | // version = "= v3.2.1" 5 | // source = "github.com/veertuinc/veertu-anka" 6 | // } 7 | // } 8 | // } 9 | 10 | variable "source_vm_name" { 11 | type = string 12 | default = "anka-packer-base-macos" 13 | } 14 | 15 | variable "source_vm_tag" { 16 | type = string 17 | default = "" 18 | } 19 | 20 | variable "vm_name" { 21 | type = string 22 | default = "anka-packer-from-source" 23 | } 24 | 25 | source "veertu-anka-vm-clone" "anka-packer-from-source" { 26 | vm_name = "${var.vm_name}" 27 | source_vm_name = "${var.source_vm_name}" 28 | source_vm_tag = "${var.source_vm_tag}" 29 | } 30 | 31 | build { 32 | sources = [ 33 | "source.veertu-anka-vm-clone.anka-packer-from-source", 34 | ] 35 | 36 | provisioner "shell" { 37 | inline = [ 38 | "echo hello world", 39 | "echo llamas rock" 40 | ] 41 | } 42 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/veertuinc/packer-plugin-veertu-anka 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/golang/mock v1.6.0 7 | github.com/groob/plist v0.0.0-20220217120414-63fa881b19a5 8 | github.com/hashicorp/hcl/v2 v2.20.1 9 | github.com/hashicorp/packer-plugin-sdk v0.5.3 10 | github.com/mitchellh/mapstructure v1.5.0 11 | github.com/zclconf/go-cty v1.13.3 12 | gotest.tools/v3 v3.5.1 13 | ) 14 | 15 | require ( 16 | cloud.google.com/go v0.105.0 // indirect 17 | cloud.google.com/go/compute v1.12.1 // indirect 18 | cloud.google.com/go/compute/metadata v0.1.1 // indirect 19 | cloud.google.com/go/iam v0.6.0 // indirect 20 | cloud.google.com/go/storage v1.27.0 // indirect 21 | github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect 22 | github.com/ChrisTrenkamp/goxpath v0.0.0-20210404020558-97928f7e12b6 // indirect 23 | github.com/agext/levenshtein v1.2.3 // indirect 24 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 25 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 26 | github.com/armon/go-metrics v0.4.1 // indirect 27 | github.com/aws/aws-sdk-go v1.44.114 // indirect 28 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect 29 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 30 | github.com/dylanmei/iso8601 v0.1.0 // indirect 31 | github.com/fatih/color v1.14.1 // indirect 32 | github.com/go-jose/go-jose/v3 v3.0.0 // indirect 33 | github.com/gofrs/flock v0.8.1 // indirect 34 | github.com/gofrs/uuid v4.0.0+incompatible // indirect 35 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 36 | github.com/golang/protobuf v1.5.2 // indirect 37 | github.com/google/go-cmp v0.6.0 // indirect 38 | github.com/google/uuid v1.3.0 // indirect 39 | github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect 40 | github.com/googleapis/gax-go/v2 v2.6.0 // indirect 41 | github.com/hashicorp/consul/api v1.25.1 // indirect 42 | github.com/hashicorp/errwrap v1.1.0 // indirect 43 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 44 | github.com/hashicorp/go-getter/gcs/v2 v2.2.1 // indirect 45 | github.com/hashicorp/go-getter/s3/v2 v2.2.1 // indirect 46 | github.com/hashicorp/go-getter/v2 v2.2.1 // indirect 47 | github.com/hashicorp/go-hclog v1.5.0 // indirect 48 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 49 | github.com/hashicorp/go-multierror v1.1.1 // indirect 50 | github.com/hashicorp/go-retryablehttp v0.7.0 // indirect 51 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 52 | github.com/hashicorp/go-safetemp v1.0.0 // indirect 53 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect 54 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 55 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 56 | github.com/hashicorp/go-version v1.6.0 // indirect 57 | github.com/hashicorp/golang-lru v0.5.4 // indirect 58 | github.com/hashicorp/hcl v1.0.0 // indirect 59 | github.com/hashicorp/serf v0.10.1 // indirect 60 | github.com/hashicorp/vault/api v1.10.0 // indirect 61 | github.com/hashicorp/yamux v0.1.1 // indirect 62 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect 63 | github.com/jmespath/go-jmespath v0.4.0 // indirect 64 | github.com/klauspost/compress v1.11.2 // indirect 65 | github.com/kr/fs v0.1.0 // indirect 66 | github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect 67 | github.com/masterzen/winrm v0.0.0-20210623064412-3b76017826b0 // indirect 68 | github.com/mattn/go-colorable v0.1.13 // indirect 69 | github.com/mattn/go-isatty v0.0.17 // indirect 70 | github.com/mitchellh/go-fs v0.0.0-20180402235330-b7b9ca407fff // indirect 71 | github.com/mitchellh/go-homedir v1.1.0 // indirect 72 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 73 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 74 | github.com/mitchellh/iochan v1.0.0 // indirect 75 | github.com/mitchellh/reflectwalk v1.0.0 // indirect 76 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect 77 | github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db // indirect 78 | github.com/pkg/sftp v1.13.2 // indirect 79 | github.com/ryanuber/go-glob v1.0.0 // indirect 80 | github.com/ugorji/go/codec v1.2.6 // indirect 81 | github.com/ulikunitz/xz v0.5.10 // indirect 82 | go.opencensus.io v0.23.0 // indirect 83 | golang.org/x/crypto v0.17.0 // indirect 84 | golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect 85 | golang.org/x/mod v0.13.0 // indirect 86 | golang.org/x/net v0.17.0 // indirect 87 | golang.org/x/oauth2 v0.1.0 // indirect 88 | golang.org/x/sys v0.15.0 // indirect 89 | golang.org/x/term v0.15.0 // indirect 90 | golang.org/x/text v0.14.0 // indirect 91 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect 92 | golang.org/x/tools v0.14.0 // indirect 93 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect 94 | google.golang.org/api v0.101.0 // indirect 95 | google.golang.org/appengine v1.6.7 // indirect 96 | google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect 97 | google.golang.org/grpc v1.50.1 // indirect 98 | google.golang.org/protobuf v1.28.1 // indirect 99 | ) 100 | 101 | replace github.com/zclconf/go-cty => github.com/nywilken/go-cty v1.13.3 // added by packer-sdc fix as noted in github.com/hashicorp/packer-plugin-sdk/issues/187 102 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/hashicorp/packer-plugin-sdk/plugin" 9 | packerSDK "github.com/hashicorp/packer-plugin-sdk/version" 10 | "github.com/veertuinc/packer-plugin-veertu-anka/builder/anka" 11 | "github.com/veertuinc/packer-plugin-veertu-anka/post-processor/ankaregistry" 12 | ) 13 | 14 | var ( 15 | version = "" 16 | commit = "" 17 | ) 18 | 19 | func main() { 20 | pps := plugin.NewSet() 21 | pps.RegisterBuilder("vm-create", new(anka.Builder)) 22 | pps.RegisterBuilder("vm-clone", new(anka.Builder)) 23 | pps.RegisterPostProcessor("registry-push", new(ankaregistry.PostProcessor)) 24 | var pluginVersion = packerSDK.NewPluginVersion(version, "", commit) 25 | pps.SetVersion(pluginVersion) 26 | log.Printf("plugin version: %s", pluginVersion.String()) 27 | err := pps.Run() 28 | if err != nil { 29 | fmt.Fprintln(os.Stderr, err.Error()) 30 | os.Exit(1) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /mocks/client_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: client/client.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | client "github.com/veertuinc/packer-plugin-veertu-anka/client" 12 | ) 13 | 14 | // MockClient is a mock of Client interface. 15 | type MockClient struct { 16 | ctrl *gomock.Controller 17 | recorder *MockClientMockRecorder 18 | } 19 | 20 | // MockClientMockRecorder is the mock recorder for MockClient. 21 | type MockClientMockRecorder struct { 22 | mock *MockClient 23 | } 24 | 25 | // NewMockClient creates a new mock instance. 26 | func NewMockClient(ctrl *gomock.Controller) *MockClient { 27 | mock := &MockClient{ctrl: ctrl} 28 | mock.recorder = &MockClientMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockClient) EXPECT() *MockClientMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Clone mocks base method. 38 | func (m *MockClient) Clone(params client.CloneParams) error { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Clone", params) 41 | ret0, _ := ret[0].(error) 42 | return ret0 43 | } 44 | 45 | // Clone indicates an expected call of Clone. 46 | func (mr *MockClientMockRecorder) Clone(params interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Clone", reflect.TypeOf((*MockClient)(nil).Clone), params) 49 | } 50 | 51 | // Copy mocks base method. 52 | func (m *MockClient) Copy(params client.CopyParams) error { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "Copy", params) 55 | ret0, _ := ret[0].(error) 56 | return ret0 57 | } 58 | 59 | // Copy indicates an expected call of Copy. 60 | func (mr *MockClientMockRecorder) Copy(params interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Copy", reflect.TypeOf((*MockClient)(nil).Copy), params) 63 | } 64 | 65 | // Create mocks base method. 66 | func (m *MockClient) Create(params client.CreateParams, outputStreamer chan string) (string, error) { 67 | m.ctrl.T.Helper() 68 | ret := m.ctrl.Call(m, "Create", params, outputStreamer) 69 | ret0, _ := ret[0].(string) 70 | ret1, _ := ret[1].(error) 71 | return ret0, ret1 72 | } 73 | 74 | // Create indicates an expected call of Create. 75 | func (mr *MockClientMockRecorder) Create(params, outputStreamer interface{}) *gomock.Call { 76 | mr.mock.ctrl.T.Helper() 77 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockClient)(nil).Create), params, outputStreamer) 78 | } 79 | 80 | // Delete mocks base method. 81 | func (m *MockClient) Delete(params client.DeleteParams) error { 82 | m.ctrl.T.Helper() 83 | ret := m.ctrl.Call(m, "Delete", params) 84 | ret0, _ := ret[0].(error) 85 | return ret0 86 | } 87 | 88 | // Delete indicates an expected call of Delete. 89 | func (mr *MockClientMockRecorder) Delete(params interface{}) *gomock.Call { 90 | mr.mock.ctrl.T.Helper() 91 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), params) 92 | } 93 | 94 | // Describe mocks base method. 95 | func (m *MockClient) Describe(vmName string) (client.DescribeResponse, error) { 96 | m.ctrl.T.Helper() 97 | ret := m.ctrl.Call(m, "Describe", vmName) 98 | ret0, _ := ret[0].(client.DescribeResponse) 99 | ret1, _ := ret[1].(error) 100 | return ret0, ret1 101 | } 102 | 103 | // Describe indicates an expected call of Describe. 104 | func (mr *MockClientMockRecorder) Describe(vmName interface{}) *gomock.Call { 105 | mr.mock.ctrl.T.Helper() 106 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Describe", reflect.TypeOf((*MockClient)(nil).Describe), vmName) 107 | } 108 | 109 | // Exists mocks base method. 110 | func (m *MockClient) Exists(vmName string) (bool, error) { 111 | m.ctrl.T.Helper() 112 | ret := m.ctrl.Call(m, "Exists", vmName) 113 | ret0, _ := ret[0].(bool) 114 | ret1, _ := ret[1].(error) 115 | return ret0, ret1 116 | } 117 | 118 | // Exists indicates an expected call of Exists. 119 | func (mr *MockClientMockRecorder) Exists(vmName interface{}) *gomock.Call { 120 | mr.mock.ctrl.T.Helper() 121 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockClient)(nil).Exists), vmName) 122 | } 123 | 124 | // FuseAvailable mocks base method. 125 | func (m *MockClient) FuseAvailable(vmName string) bool { 126 | m.ctrl.T.Helper() 127 | ret := m.ctrl.Call(m, "FuseAvailable", vmName) 128 | ret0, _ := ret[0].(bool) 129 | return ret0 130 | } 131 | 132 | // FuseAvailable indicates an expected call of FuseAvailable. 133 | func (mr *MockClientMockRecorder) FuseAvailable(vmName interface{}) *gomock.Call { 134 | mr.mock.ctrl.T.Helper() 135 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FuseAvailable", reflect.TypeOf((*MockClient)(nil).FuseAvailable), vmName) 136 | } 137 | 138 | // License mocks base method. 139 | func (m *MockClient) License() (client.LicenseResponse, error) { 140 | m.ctrl.T.Helper() 141 | ret := m.ctrl.Call(m, "License") 142 | ret0, _ := ret[0].(client.LicenseResponse) 143 | ret1, _ := ret[1].(error) 144 | return ret0, ret1 145 | } 146 | 147 | // License indicates an expected call of License. 148 | func (mr *MockClientMockRecorder) License() *gomock.Call { 149 | mr.mock.ctrl.T.Helper() 150 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "License", reflect.TypeOf((*MockClient)(nil).License)) 151 | } 152 | 153 | // Modify mocks base method. 154 | func (m *MockClient) Modify(vmName, command, property string, flags ...string) error { 155 | m.ctrl.T.Helper() 156 | varargs := []interface{}{vmName, command, property} 157 | for _, a := range flags { 158 | varargs = append(varargs, a) 159 | } 160 | ret := m.ctrl.Call(m, "Modify", varargs...) 161 | ret0, _ := ret[0].(error) 162 | return ret0 163 | } 164 | 165 | // Modify indicates an expected call of Modify. 166 | func (mr *MockClientMockRecorder) Modify(vmName, command, property interface{}, flags ...interface{}) *gomock.Call { 167 | mr.mock.ctrl.T.Helper() 168 | varargs := append([]interface{}{vmName, command, property}, flags...) 169 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Modify", reflect.TypeOf((*MockClient)(nil).Modify), varargs...) 170 | } 171 | 172 | // RegistryList mocks base method. 173 | func (m *MockClient) RegistryList(registryParams client.RegistryParams) ([]client.RegistryListResponse, error) { 174 | m.ctrl.T.Helper() 175 | ret := m.ctrl.Call(m, "RegistryList", registryParams) 176 | ret0, _ := ret[0].([]client.RegistryListResponse) 177 | ret1, _ := ret[1].(error) 178 | return ret0, ret1 179 | } 180 | 181 | // RegistryList indicates an expected call of RegistryList. 182 | func (mr *MockClientMockRecorder) RegistryList(registryParams interface{}) *gomock.Call { 183 | mr.mock.ctrl.T.Helper() 184 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegistryList", reflect.TypeOf((*MockClient)(nil).RegistryList), registryParams) 185 | } 186 | 187 | // RegistryListRepos mocks base method. 188 | func (m *MockClient) RegistryListRepos() ([]client.RegistryRemote, error) { 189 | m.ctrl.T.Helper() 190 | ret := m.ctrl.Call(m, "RegistryListRepos") 191 | ret0, _ := ret[0].([]client.RegistryRemote) 192 | ret1, _ := ret[1].(error) 193 | return ret0, ret1 194 | } 195 | 196 | // RegistryListRepos indicates an expected call of RegistryListRepos. 197 | func (mr *MockClientMockRecorder) RegistryListRepos() *gomock.Call { 198 | mr.mock.ctrl.T.Helper() 199 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegistryListRepos", reflect.TypeOf((*MockClient)(nil).RegistryListRepos)) 200 | } 201 | 202 | // RegistryPull mocks base method. 203 | func (m *MockClient) RegistryPull(registryParams client.RegistryParams, pullParams client.RegistryPullParams) error { 204 | m.ctrl.T.Helper() 205 | ret := m.ctrl.Call(m, "RegistryPull", registryParams, pullParams) 206 | ret0, _ := ret[0].(error) 207 | return ret0 208 | } 209 | 210 | // RegistryPull indicates an expected call of RegistryPull. 211 | func (mr *MockClientMockRecorder) RegistryPull(registryParams, pullParams interface{}) *gomock.Call { 212 | mr.mock.ctrl.T.Helper() 213 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegistryPull", reflect.TypeOf((*MockClient)(nil).RegistryPull), registryParams, pullParams) 214 | } 215 | 216 | // RegistryPush mocks base method. 217 | func (m *MockClient) RegistryPush(registryParams client.RegistryParams, pushParams client.RegistryPushParams) error { 218 | m.ctrl.T.Helper() 219 | ret := m.ctrl.Call(m, "RegistryPush", registryParams, pushParams) 220 | ret0, _ := ret[0].(error) 221 | return ret0 222 | } 223 | 224 | // RegistryPush indicates an expected call of RegistryPush. 225 | func (mr *MockClientMockRecorder) RegistryPush(registryParams, pushParams interface{}) *gomock.Call { 226 | mr.mock.ctrl.T.Helper() 227 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegistryPush", reflect.TypeOf((*MockClient)(nil).RegistryPush), registryParams, pushParams) 228 | } 229 | 230 | // RegistryRevert mocks base method. 231 | func (m *MockClient) RegistryRevert(url, id string) error { 232 | m.ctrl.T.Helper() 233 | ret := m.ctrl.Call(m, "RegistryRevert", url, id) 234 | ret0, _ := ret[0].(error) 235 | return ret0 236 | } 237 | 238 | // RegistryRevert indicates an expected call of RegistryRevert. 239 | func (mr *MockClientMockRecorder) RegistryRevert(url, id interface{}) *gomock.Call { 240 | mr.mock.ctrl.T.Helper() 241 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegistryRevert", reflect.TypeOf((*MockClient)(nil).RegistryRevert), url, id) 242 | } 243 | 244 | // Run mocks base method. 245 | func (m *MockClient) Run(params client.RunParams) (int, error) { 246 | m.ctrl.T.Helper() 247 | ret := m.ctrl.Call(m, "Run", params) 248 | ret0, _ := ret[0].(int) 249 | ret1, _ := ret[1].(error) 250 | return ret0, ret1 251 | } 252 | 253 | // Run indicates an expected call of Run. 254 | func (mr *MockClientMockRecorder) Run(params interface{}) *gomock.Call { 255 | mr.mock.ctrl.T.Helper() 256 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockClient)(nil).Run), params) 257 | } 258 | 259 | // Show mocks base method. 260 | func (m *MockClient) Show(vmName string) (client.ShowResponse, error) { 261 | m.ctrl.T.Helper() 262 | ret := m.ctrl.Call(m, "Show", vmName) 263 | ret0, _ := ret[0].(client.ShowResponse) 264 | ret1, _ := ret[1].(error) 265 | return ret0, ret1 266 | } 267 | 268 | // Show indicates an expected call of Show. 269 | func (mr *MockClientMockRecorder) Show(vmName interface{}) *gomock.Call { 270 | mr.mock.ctrl.T.Helper() 271 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Show", reflect.TypeOf((*MockClient)(nil).Show), vmName) 272 | } 273 | 274 | // Start mocks base method. 275 | func (m *MockClient) Start(params client.StartParams) error { 276 | m.ctrl.T.Helper() 277 | ret := m.ctrl.Call(m, "Start", params) 278 | ret0, _ := ret[0].(error) 279 | return ret0 280 | } 281 | 282 | // Start indicates an expected call of Start. 283 | func (mr *MockClientMockRecorder) Start(params interface{}) *gomock.Call { 284 | mr.mock.ctrl.T.Helper() 285 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockClient)(nil).Start), params) 286 | } 287 | 288 | // Stop mocks base method. 289 | func (m *MockClient) Stop(params client.StopParams) error { 290 | m.ctrl.T.Helper() 291 | ret := m.ctrl.Call(m, "Stop", params) 292 | ret0, _ := ret[0].(error) 293 | return ret0 294 | } 295 | 296 | // Stop indicates an expected call of Stop. 297 | func (mr *MockClientMockRecorder) Stop(params interface{}) *gomock.Call { 298 | mr.mock.ctrl.T.Helper() 299 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockClient)(nil).Stop), params) 300 | } 301 | 302 | // Suspend mocks base method. 303 | func (m *MockClient) Suspend(params client.SuspendParams) error { 304 | m.ctrl.T.Helper() 305 | ret := m.ctrl.Call(m, "Suspend", params) 306 | ret0, _ := ret[0].(error) 307 | return ret0 308 | } 309 | 310 | // Suspend indicates an expected call of Suspend. 311 | func (mr *MockClientMockRecorder) Suspend(params interface{}) *gomock.Call { 312 | mr.mock.ctrl.T.Helper() 313 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Suspend", reflect.TypeOf((*MockClient)(nil).Suspend), params) 314 | } 315 | 316 | // UpdateAddons mocks base method. 317 | func (m *MockClient) UpdateAddons(vmName string) error { 318 | m.ctrl.T.Helper() 319 | ret := m.ctrl.Call(m, "UpdateAddons", vmName) 320 | ret0, _ := ret[0].(error) 321 | return ret0 322 | } 323 | 324 | // UpdateAddons indicates an expected call of UpdateAddons. 325 | func (mr *MockClientMockRecorder) UpdateAddons(vmName interface{}) *gomock.Call { 326 | mr.mock.ctrl.T.Helper() 327 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAddons", reflect.TypeOf((*MockClient)(nil).UpdateAddons), vmName) 328 | } 329 | 330 | // Version mocks base method. 331 | func (m *MockClient) Version() (client.VersionResponse, error) { 332 | m.ctrl.T.Helper() 333 | ret := m.ctrl.Call(m, "Version") 334 | ret0, _ := ret[0].(client.VersionResponse) 335 | ret1, _ := ret[1].(error) 336 | return ret0, ret1 337 | } 338 | 339 | // Version indicates an expected call of Version. 340 | func (mr *MockClientMockRecorder) Version() *gomock.Call { 341 | mr.mock.ctrl.T.Helper() 342 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Version", reflect.TypeOf((*MockClient)(nil).Version)) 343 | } 344 | -------------------------------------------------------------------------------- /mocks/util_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: util/util.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | multistep "github.com/hashicorp/packer-plugin-sdk/multistep" 12 | packer "github.com/hashicorp/packer-plugin-sdk/packer" 13 | util "github.com/veertuinc/packer-plugin-veertu-anka/util" 14 | ) 15 | 16 | // MockUtil is a mock of Util interface. 17 | type MockUtil struct { 18 | ctrl *gomock.Controller 19 | recorder *MockUtilMockRecorder 20 | } 21 | 22 | // MockUtilMockRecorder is the mock recorder for MockUtil. 23 | type MockUtilMockRecorder struct { 24 | mock *MockUtil 25 | } 26 | 27 | // NewMockUtil creates a new mock instance. 28 | func NewMockUtil(ctrl *gomock.Controller) *MockUtil { 29 | mock := &MockUtil{ctrl: ctrl} 30 | mock.recorder = &MockUtilMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockUtil) EXPECT() *MockUtilMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // ConfigTmpDir mocks base method. 40 | func (m *MockUtil) ConfigTmpDir() (string, error) { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "ConfigTmpDir") 43 | ret0, _ := ret[0].(string) 44 | ret1, _ := ret[1].(error) 45 | return ret0, ret1 46 | } 47 | 48 | // ConfigTmpDir indicates an expected call of ConfigTmpDir. 49 | func (mr *MockUtilMockRecorder) ConfigTmpDir() *gomock.Call { 50 | mr.mock.ctrl.T.Helper() 51 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigTmpDir", reflect.TypeOf((*MockUtil)(nil).ConfigTmpDir)) 52 | } 53 | 54 | // ConvertDiskSizeToBytes mocks base method. 55 | func (m *MockUtil) ConvertDiskSizeToBytes(diskSize string) (uint64, error) { 56 | m.ctrl.T.Helper() 57 | ret := m.ctrl.Call(m, "ConvertDiskSizeToBytes", diskSize) 58 | ret0, _ := ret[0].(uint64) 59 | ret1, _ := ret[1].(error) 60 | return ret0, ret1 61 | } 62 | 63 | // ConvertDiskSizeToBytes indicates an expected call of ConvertDiskSizeToBytes. 64 | func (mr *MockUtilMockRecorder) ConvertDiskSizeToBytes(diskSize interface{}) *gomock.Call { 65 | mr.mock.ctrl.T.Helper() 66 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConvertDiskSizeToBytes", reflect.TypeOf((*MockUtil)(nil).ConvertDiskSizeToBytes), diskSize) 67 | } 68 | 69 | // ObtainMacOSVersionFromInstallerApp mocks base method. 70 | func (m *MockUtil) ObtainMacOSVersionFromInstallerApp(path string) (util.InstallerAppPlist, error) { 71 | m.ctrl.T.Helper() 72 | ret := m.ctrl.Call(m, "ObtainMacOSVersionFromInstallerApp", path) 73 | ret0, _ := ret[0].(util.InstallerAppPlist) 74 | ret1, _ := ret[1].(error) 75 | return ret0, ret1 76 | } 77 | 78 | // ObtainMacOSVersionFromInstallerApp indicates an expected call of ObtainMacOSVersionFromInstallerApp. 79 | func (mr *MockUtilMockRecorder) ObtainMacOSVersionFromInstallerApp(path interface{}) *gomock.Call { 80 | mr.mock.ctrl.T.Helper() 81 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObtainMacOSVersionFromInstallerApp", reflect.TypeOf((*MockUtil)(nil).ObtainMacOSVersionFromInstallerApp), path) 82 | } 83 | 84 | // ObtainMacOSVersionFromInstallerIPSW mocks base method. 85 | func (m *MockUtil) ObtainMacOSVersionFromInstallerIPSW(path string) (util.InstallerIPSWPlist, error) { 86 | m.ctrl.T.Helper() 87 | ret := m.ctrl.Call(m, "ObtainMacOSVersionFromInstallerIPSW", path) 88 | ret0, _ := ret[0].(util.InstallerIPSWPlist) 89 | ret1, _ := ret[1].(error) 90 | return ret0, ret1 91 | } 92 | 93 | // ObtainMacOSVersionFromInstallerIPSW indicates an expected call of ObtainMacOSVersionFromInstallerIPSW. 94 | func (mr *MockUtilMockRecorder) ObtainMacOSVersionFromInstallerIPSW(path interface{}) *gomock.Call { 95 | mr.mock.ctrl.T.Helper() 96 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObtainMacOSVersionFromInstallerIPSW", reflect.TypeOf((*MockUtil)(nil).ObtainMacOSVersionFromInstallerIPSW), path) 97 | } 98 | 99 | // RandSeq mocks base method. 100 | func (m *MockUtil) RandSeq(n int) string { 101 | m.ctrl.T.Helper() 102 | ret := m.ctrl.Call(m, "RandSeq", n) 103 | ret0, _ := ret[0].(string) 104 | return ret0 105 | } 106 | 107 | // RandSeq indicates an expected call of RandSeq. 108 | func (mr *MockUtilMockRecorder) RandSeq(n interface{}) *gomock.Call { 109 | mr.mock.ctrl.T.Helper() 110 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RandSeq", reflect.TypeOf((*MockUtil)(nil).RandSeq), n) 111 | } 112 | 113 | // StepError mocks base method. 114 | func (m *MockUtil) StepError(ui packer.Ui, state multistep.StateBag, err error) multistep.StepAction { 115 | m.ctrl.T.Helper() 116 | ret := m.ctrl.Call(m, "StepError", ui, state, err) 117 | ret0, _ := ret[0].(multistep.StepAction) 118 | return ret0 119 | } 120 | 121 | // StepError indicates an expected call of StepError. 122 | func (mr *MockUtilMockRecorder) StepError(ui, state, err interface{}) *gomock.Call { 123 | mr.mock.ctrl.T.Helper() 124 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StepError", reflect.TypeOf((*MockUtil)(nil).StepError), ui, state, err) 125 | } 126 | -------------------------------------------------------------------------------- /post-processor/ankaregistry/post-processor.go: -------------------------------------------------------------------------------- 1 | //go:generate packer-sdc mapstructure-to-hcl2 -type Config 2 | 3 | package ankaregistry 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | "fmt" 9 | "net/url" 10 | "runtime" 11 | 12 | "github.com/hashicorp/hcl/v2/hcldec" 13 | "github.com/hashicorp/packer-plugin-sdk/common" 14 | "github.com/hashicorp/packer-plugin-sdk/packer" 15 | "github.com/hashicorp/packer-plugin-sdk/template/config" 16 | "github.com/hashicorp/packer-plugin-sdk/template/interpolate" 17 | "github.com/veertuinc/packer-plugin-veertu-anka/builder/anka" 18 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 19 | ) 20 | 21 | // BuilderIdRegistry unique id for this post processor 22 | const BuilderIdRegistry = "packer.post-processor.veertu-anka-registry" 23 | 24 | // Config initializes the post processor using mapstructure which decodes 25 | // generic map values from either the json or hcl2 config files provided 26 | type Config struct { 27 | common.PackerConfig `mapstructure:",squash"` 28 | 29 | Remote string `mapstructure:"remote"` 30 | NodeCertPath string `mapstructure:"cert"` 31 | NodeKeyPath string `mapstructure:"key"` 32 | CaRootPath string `mapstructure:"cacert"` 33 | IsInsecure bool `mapstructure:"insecure"` 34 | 35 | Tag string `mapstructure:"tag"` 36 | Description string `mapstructure:"description"` 37 | RemoteVM string `mapstructure:"remote_vm"` 38 | Local bool `mapstructure:"local"` 39 | Force bool `mapstructure:"force"` 40 | 41 | HostArch string `mapstructure:"host_arch,omitempty"` 42 | 43 | ctx interpolate.Context 44 | } 45 | 46 | // PostProcessor is used to run the post processor 47 | type PostProcessor struct { 48 | config Config 49 | client client.Client 50 | } 51 | 52 | // ConfigSpec returns the decoded mapstructure config values into their respective format 53 | func (p *PostProcessor) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() } 54 | 55 | // Configure sets up the post processor with the decoded config values 56 | func (p *PostProcessor) Configure(raws ...interface{}) error { 57 | 58 | var errs *packer.MultiError 59 | 60 | err := config.Decode(&p.config, &config.DecodeOpts{ 61 | PluginType: BuilderIdRegistry, 62 | Interpolate: true, 63 | InterpolateContext: &p.config.ctx, 64 | InterpolateFilter: &interpolate.RenderFilter{ 65 | Exclude: []string{}, 66 | }, 67 | }, raws...) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | if p.config.Tag == "" { 73 | errs = packer.MultiErrorAppend(errs, errors.New("you must specify a valid tag for your Veertu Anka VM (e.g. 'latest')")) 74 | } 75 | 76 | if p.config.Local && p.config.RemoteVM != "" { 77 | errs = packer.MultiErrorAppend(errs, errors.New("the 'local' and 'remote_vm' settings are mutually exclusive")) 78 | } 79 | 80 | p.config.HostArch = runtime.GOARCH 81 | 82 | p.client = &client.AnkaClient{} 83 | 84 | if errs != nil && len(errs.Errors) > 0 { 85 | return errs 86 | } 87 | 88 | return nil 89 | } 90 | 91 | // PostProcess runs the post processor logic which uploads the artifact to an anka registry 92 | func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, bool, error) { 93 | var reposList []client.RegistryRemote 94 | var err error 95 | if artifact.BuilderId() != anka.BuilderId { 96 | err := fmt.Errorf( 97 | "unknown artifact type: %s\ncan only import from anka artifacts", 98 | artifact.BuilderId()) 99 | return nil, false, false, err 100 | } 101 | 102 | reposList, err = p.client.RegistryListRepos() 103 | if err != nil { 104 | return nil, false, false, err 105 | } 106 | 107 | if p.config.Remote == "" { // no remote set by user? use the default on the host 108 | for _, remote := range reposList { 109 | if remote.Default { 110 | p.config.Remote = remote.Name 111 | } 112 | } 113 | } else { 114 | _, err := url.ParseRequestURI(p.config.Remote) 115 | if err != nil { // not a url, so we should treat it as a string/name for the local registry 116 | foundRemoteName := false 117 | for _, repoRemote := range reposList { 118 | if repoRemote.Name == p.config.Remote { 119 | foundRemoteName = true 120 | } 121 | } 122 | if !foundRemoteName { 123 | return nil, false, false, fmt.Errorf("could not find configuration for registry remote name '%s'", p.config.Remote) 124 | } 125 | } 126 | } 127 | 128 | registryParams := client.RegistryParams{ 129 | Remote: p.config.Remote, 130 | NodeCertPath: p.config.NodeCertPath, 131 | NodeKeyPath: p.config.NodeKeyPath, 132 | CaRootPath: p.config.CaRootPath, 133 | IsInsecure: p.config.IsInsecure, 134 | HostArch: p.config.HostArch, 135 | } 136 | 137 | remoteVMName := artifact.String() 138 | if p.config.RemoteVM != "" { 139 | remoteVMName = p.config.RemoteVM 140 | } 141 | 142 | remoteTag := "latest" 143 | if p.config.Tag != "" { 144 | remoteTag = p.config.Tag 145 | } 146 | 147 | pushParams := client.RegistryPushParams{ 148 | Tag: remoteTag, 149 | Description: p.config.Description, 150 | RemoteVM: p.config.RemoteVM, 151 | Local: p.config.Local, 152 | Force: p.config.Force, 153 | VMID: artifact.String(), 154 | } 155 | 156 | var id string 157 | var latestTag string 158 | var found bool 159 | var foundMessage string 160 | 161 | if p.config.Local { 162 | ui.Say(fmt.Sprintf("Tagging local template %s with tag %s", remoteVMName, remoteTag)) 163 | } else { 164 | ui.Say(fmt.Sprintf("Pushing template to Anka Registry as %s with tag %s", remoteVMName, remoteTag)) 165 | 166 | // Check if it already exists first 167 | templates, err := p.client.RegistryList(registryParams) 168 | if err != nil { 169 | return nil, false, false, err 170 | } 171 | 172 | for i := 0; i < len(templates); i++ { 173 | if templates[i].Name == remoteVMName { 174 | id = templates[i].ID 175 | latestTag = templates[i].Latest 176 | if !pushParams.Force { // avoid revert and error if we're forcing the push with the CLI 177 | found = true 178 | } 179 | foundMessage = fmt.Sprintf("Found existing template %s on registry that matches name '%s'", id, remoteVMName) 180 | ui.Say(foundMessage) 181 | break 182 | } 183 | } 184 | 185 | if p.config.PackerForce { // differs from processor's force: true 186 | if id != "" && latestTag == remoteTag { 187 | err = p.client.RegistryRevert(registryParams.Remote, id) 188 | if err != nil { 189 | return nil, false, false, err 190 | } 191 | ui.Say(fmt.Sprintf("Reverted latest tag for template '%s' on registry", id)) 192 | } 193 | found = false 194 | } 195 | 196 | } 197 | 198 | if found { 199 | err = fmt.Errorf(foundMessage) 200 | } else { 201 | err = p.client.RegistryPush(registryParams, pushParams) 202 | if err == nil { 203 | ui.Say("Registry push successful") 204 | } 205 | } 206 | 207 | return artifact, true, false, err 208 | } 209 | -------------------------------------------------------------------------------- /post-processor/ankaregistry/post-processor.hcl2spec.go: -------------------------------------------------------------------------------- 1 | // Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT. 2 | 3 | package ankaregistry 4 | 5 | import ( 6 | "github.com/hashicorp/hcl/v2/hcldec" 7 | "github.com/zclconf/go-cty/cty" 8 | ) 9 | 10 | // FlatConfig is an auto-generated flat version of Config. 11 | // Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. 12 | type FlatConfig struct { 13 | PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"` 14 | PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"` 15 | PackerCoreVersion *string `mapstructure:"packer_core_version" cty:"packer_core_version" hcl:"packer_core_version"` 16 | PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"` 17 | PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"` 18 | PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"` 19 | PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"` 20 | PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"` 21 | Remote *string `mapstructure:"remote" cty:"remote" hcl:"remote"` 22 | NodeCertPath *string `mapstructure:"cert" cty:"cert" hcl:"cert"` 23 | NodeKeyPath *string `mapstructure:"key" cty:"key" hcl:"key"` 24 | CaRootPath *string `mapstructure:"cacert" cty:"cacert" hcl:"cacert"` 25 | IsInsecure *bool `mapstructure:"insecure" cty:"insecure" hcl:"insecure"` 26 | Tag *string `mapstructure:"tag" cty:"tag" hcl:"tag"` 27 | Description *string `mapstructure:"description" cty:"description" hcl:"description"` 28 | RemoteVM *string `mapstructure:"remote_vm" cty:"remote_vm" hcl:"remote_vm"` 29 | Local *bool `mapstructure:"local" cty:"local" hcl:"local"` 30 | Force *bool `mapstructure:"force" cty:"force" hcl:"force"` 31 | HostArch *string `mapstructure:"host_arch,omitempty" cty:"host_arch" hcl:"host_arch"` 32 | } 33 | 34 | // FlatMapstructure returns a new FlatConfig. 35 | // FlatConfig is an auto-generated flat version of Config. 36 | // Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. 37 | func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { 38 | return new(FlatConfig) 39 | } 40 | 41 | // HCL2Spec returns the hcl spec of a Config. 42 | // This spec is used by HCL to read the fields of Config. 43 | // The decoded values from this spec will then be applied to a FlatConfig. 44 | func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { 45 | s := map[string]hcldec.Spec{ 46 | "packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false}, 47 | "packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false}, 48 | "packer_core_version": &hcldec.AttrSpec{Name: "packer_core_version", Type: cty.String, Required: false}, 49 | "packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false}, 50 | "packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false}, 51 | "packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false}, 52 | "packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false}, 53 | "packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false}, 54 | "remote": &hcldec.AttrSpec{Name: "remote", Type: cty.String, Required: false}, 55 | "cert": &hcldec.AttrSpec{Name: "cert", Type: cty.String, Required: false}, 56 | "key": &hcldec.AttrSpec{Name: "key", Type: cty.String, Required: false}, 57 | "cacert": &hcldec.AttrSpec{Name: "cacert", Type: cty.String, Required: false}, 58 | "insecure": &hcldec.AttrSpec{Name: "insecure", Type: cty.Bool, Required: false}, 59 | "tag": &hcldec.AttrSpec{Name: "tag", Type: cty.String, Required: false}, 60 | "description": &hcldec.AttrSpec{Name: "description", Type: cty.String, Required: false}, 61 | "remote_vm": &hcldec.AttrSpec{Name: "remote_vm", Type: cty.String, Required: false}, 62 | "local": &hcldec.AttrSpec{Name: "local", Type: cty.Bool, Required: false}, 63 | "force": &hcldec.AttrSpec{Name: "force", Type: cty.Bool, Required: false}, 64 | "host_arch": &hcldec.AttrSpec{Name: "host_arch", Type: cty.String, Required: false}, 65 | } 66 | return s 67 | } 68 | -------------------------------------------------------------------------------- /post-processor/ankaregistry/post-processor_test.go: -------------------------------------------------------------------------------- 1 | package ankaregistry 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/golang/mock/gomock" 11 | "github.com/hashicorp/packer-plugin-sdk/common" 12 | "github.com/hashicorp/packer-plugin-sdk/packer" 13 | "github.com/veertuinc/packer-plugin-veertu-anka/builder/anka" 14 | "github.com/veertuinc/packer-plugin-veertu-anka/client" 15 | mocks "github.com/veertuinc/packer-plugin-veertu-anka/mocks" 16 | "gotest.tools/v3/assert" 17 | ) 18 | 19 | var templateList []client.RegistryListResponse 20 | var reposList []client.RegistryRemote 21 | 22 | func TestAnkaRegistryPostProcessor(t *testing.T) { 23 | 24 | err := json.Unmarshal(json.RawMessage(`[{"default": true, "url": "http://localhost:8080", "name": "go-mock"}]`), &reposList) 25 | if err != nil { 26 | t.Fail() 27 | } 28 | 29 | mockCtrl := gomock.NewController(t) 30 | defer mockCtrl.Finish() 31 | ankaClient := mocks.NewMockClient(mockCtrl) 32 | 33 | ui := packer.TestUi(t) 34 | 35 | artifact := &anka.Artifact{} 36 | 37 | reposList = []client.RegistryRemote{{Default: true, Name: "go-mock", Url: "http://localhost:8080"}} 38 | 39 | t.Run("push to registry with defaults", func(t *testing.T) { 40 | config := Config{ 41 | RemoteVM: "foo", 42 | Tag: "registry-push", 43 | Description: "mock for testing anka registry push", 44 | HostArch: runtime.GOARCH, 45 | } 46 | 47 | pp := PostProcessor{ 48 | config: config, 49 | client: ankaClient, 50 | } 51 | 52 | registryParams := client.RegistryParams{ 53 | Remote: "go-mock", 54 | HostArch: config.HostArch, 55 | } 56 | 57 | pushParams := client.RegistryPushParams{ 58 | Tag: config.Tag, 59 | Description: config.Description, 60 | RemoteVM: config.RemoteVM, 61 | Local: false, 62 | Force: false, 63 | } 64 | 65 | ankaClient.EXPECT().RegistryListRepos().Return(reposList, nil).Times(1) 66 | 67 | ankaClient.EXPECT().RegistryList(registryParams).Return([]client.RegistryListResponse{}, nil).Times(1) 68 | ankaClient.EXPECT().RegistryPush(registryParams, pushParams).Return(nil).Times(1) 69 | 70 | mockui := packer.MockUi{} 71 | mockui.Say(fmt.Sprintf("Pushing template to Anka Registry as %s with tag %s", config.RemoteVM, config.Tag)) 72 | mockui.Say("Registry push successful") 73 | 74 | assert.Equal(t, mockui.SayMessages[0].Message, "Pushing template to Anka Registry as foo with tag registry-push") 75 | assert.Equal(t, mockui.SayMessages[1].Message, "Registry push successful") 76 | 77 | _, _, _, err := pp.PostProcess(context.Background(), ui, artifact) 78 | if err != nil { 79 | t.Fail() 80 | } 81 | }) 82 | 83 | t.Run("push to registry with registry name", func(t *testing.T) { 84 | config := Config{ 85 | Remote: "go-mock", 86 | RemoteVM: "foo", 87 | Tag: "registry-push", 88 | Description: "mock for testing anka registry push", 89 | HostArch: runtime.GOARCH, 90 | } 91 | 92 | pp := PostProcessor{ 93 | config: config, 94 | client: ankaClient, 95 | } 96 | 97 | registryParams := client.RegistryParams{ 98 | Remote: "go-mock", 99 | HostArch: config.HostArch, 100 | } 101 | 102 | pushParams := client.RegistryPushParams{ 103 | Tag: config.Tag, 104 | Description: config.Description, 105 | RemoteVM: config.RemoteVM, 106 | Local: false, 107 | Force: false, 108 | } 109 | 110 | ankaClient.EXPECT().RegistryListRepos().Return(reposList, nil).Times(1) 111 | 112 | ankaClient.EXPECT().RegistryList(registryParams).Return([]client.RegistryListResponse{}, nil).Times(1) 113 | ankaClient.EXPECT().RegistryPush(registryParams, pushParams).Return(nil).Times(1) 114 | 115 | mockui := packer.MockUi{} 116 | mockui.Say(fmt.Sprintf("Pushing template to Anka Registry as %s with tag %s", config.RemoteVM, config.Tag)) 117 | 118 | assert.Equal(t, mockui.SayMessages[0].Message, "Pushing template to Anka Registry as foo with tag registry-push") 119 | 120 | _, _, _, err := pp.PostProcess(context.Background(), ui, artifact) 121 | if err != nil { 122 | t.Fail() 123 | } 124 | }) 125 | 126 | t.Run("push to registry with registry URL", func(t *testing.T) { 127 | config := Config{ 128 | Remote: "http://anka.example.test:8080", 129 | RemoteVM: "foo", 130 | Tag: "registry-push", 131 | Description: "mock for testing anka registry push", 132 | HostArch: runtime.GOARCH, 133 | } 134 | 135 | pp := PostProcessor{ 136 | config: config, 137 | client: ankaClient, 138 | } 139 | 140 | registryParams := client.RegistryParams{ 141 | Remote: "http://anka.example.test:8080", 142 | HostArch: config.HostArch, 143 | } 144 | 145 | pushParams := client.RegistryPushParams{ 146 | Tag: config.Tag, 147 | Description: config.Description, 148 | RemoteVM: config.RemoteVM, 149 | Local: false, 150 | Force: false, 151 | } 152 | 153 | ankaClient.EXPECT().RegistryListRepos().Return(reposList, nil).Times(1) 154 | 155 | ankaClient.EXPECT().RegistryList(registryParams).Return([]client.RegistryListResponse{}, nil).Times(1) 156 | ankaClient.EXPECT().RegistryPush(registryParams, pushParams).Return(nil).Times(1) 157 | 158 | mockui := packer.MockUi{} 159 | mockui.Say(fmt.Sprintf("Pushing template to Anka Registry as %s with tag %s", config.RemoteVM, config.Tag)) 160 | 161 | assert.Equal(t, mockui.SayMessages[0].Message, "Pushing template to Anka Registry as foo with tag registry-push") 162 | 163 | _, _, _, err := pp.PostProcess(context.Background(), ui, artifact) 164 | if err != nil { 165 | t.Fail() 166 | } 167 | }) 168 | 169 | t.Run("push to registry with no existing templates", func(t *testing.T) { 170 | packerConfig := common.PackerConfig{ 171 | PackerForce: true, 172 | } 173 | 174 | config := Config{ 175 | PackerConfig: packerConfig, 176 | RemoteVM: "foo", 177 | Tag: "registry-push", 178 | Description: "mock for testing anka registry push", 179 | HostArch: runtime.GOARCH, 180 | } 181 | 182 | pp := PostProcessor{ 183 | config: config, 184 | client: ankaClient, 185 | } 186 | 187 | registryParams := client.RegistryParams{ 188 | Remote: "go-mock", 189 | HostArch: config.HostArch, 190 | } 191 | 192 | pushParams := client.RegistryPushParams{ 193 | Tag: config.Tag, 194 | Description: config.Description, 195 | RemoteVM: config.RemoteVM, 196 | Local: false, 197 | Force: false, 198 | } 199 | 200 | ankaClient.EXPECT().RegistryListRepos().Return(reposList, nil).Times(1) 201 | 202 | ankaClient.EXPECT().RegistryList(registryParams).Return([]client.RegistryListResponse{}, nil).Times(1) 203 | ankaClient.EXPECT().RegistryPush(registryParams, pushParams).Return(nil).Times(1) 204 | 205 | mockui := packer.MockUi{} 206 | mockui.Say(fmt.Sprintf("Pushing template to Anka Registry as %s with tag %s", config.RemoteVM, config.Tag)) 207 | 208 | assert.Equal(t, mockui.SayMessages[0].Message, "Pushing template to Anka Registry as foo with tag registry-push") 209 | 210 | _, _, _, err := pp.PostProcess(context.Background(), ui, artifact) 211 | if err != nil { 212 | t.Fail() 213 | } 214 | }) 215 | 216 | t.Run("push to registry with existing template and fail", func(t *testing.T) { 217 | err := json.Unmarshal(json.RawMessage(`[{ "id": "foo_id", "name": "foo", "latest": "foo_tag" }]`), &templateList) 218 | if err != nil { 219 | t.Fail() 220 | } 221 | 222 | packerConfig := common.PackerConfig{ 223 | PackerForce: false, 224 | } 225 | 226 | config := Config{ 227 | PackerConfig: packerConfig, 228 | RemoteVM: "foo", 229 | Tag: "registry-push", 230 | Description: "mock for testing anka registry push", 231 | HostArch: runtime.GOARCH, 232 | } 233 | 234 | pp := PostProcessor{ 235 | config: config, 236 | client: ankaClient, 237 | } 238 | 239 | registryParams := client.RegistryParams{ 240 | Remote: "go-mock", 241 | HostArch: config.HostArch, 242 | } 243 | 244 | ankaClient.EXPECT().RegistryListRepos().Return(reposList, nil).Times(1) 245 | 246 | ankaClient.EXPECT().RegistryList(registryParams).Return(templateList, nil).Times(1) 247 | 248 | mockui := packer.MockUi{} 249 | mockui.Say(fmt.Sprintf("Pushing template to Anka Registry as %s with tag %s", config.RemoteVM, config.Tag)) 250 | mockui.Say(fmt.Sprintf("Found existing template %s on registry that matches name '%s'", templateList[0].ID, config.RemoteVM)) 251 | 252 | assert.Equal(t, mockui.SayMessages[0].Message, "Pushing template to Anka Registry as foo with tag registry-push") 253 | assert.Equal(t, mockui.SayMessages[1].Message, "Found existing template foo_id on registry that matches name 'foo'") 254 | 255 | _, _, _, err = pp.PostProcess(context.Background(), ui, artifact) 256 | if err == nil { 257 | t.Fail() 258 | } 259 | }) 260 | 261 | t.Run("push to registry with existing template and don't revert latest tag first [packer build -force]", func(t *testing.T) { 262 | err := json.Unmarshal(json.RawMessage(`[{ "id": "foo_id", "name": "foo", "latest": "foo_tag" }]`), &templateList) 263 | if err != nil { 264 | t.Fail() 265 | } 266 | 267 | packerConfig := common.PackerConfig{ 268 | PackerForce: true, 269 | } 270 | 271 | config := Config{ 272 | PackerConfig: packerConfig, 273 | RemoteVM: "foo", 274 | Tag: "registry-push", 275 | Description: "mock for testing anka registry push", 276 | HostArch: runtime.GOARCH, 277 | } 278 | 279 | pp := PostProcessor{ 280 | config: config, 281 | client: ankaClient, 282 | } 283 | 284 | registryParams := client.RegistryParams{ 285 | Remote: "go-mock", 286 | HostArch: config.HostArch, 287 | } 288 | 289 | pushParams := client.RegistryPushParams{ 290 | Tag: config.Tag, 291 | Description: config.Description, 292 | RemoteVM: config.RemoteVM, 293 | Local: false, 294 | Force: false, 295 | } 296 | 297 | ankaClient.EXPECT().RegistryListRepos().Return(reposList, nil).Times(1) 298 | 299 | ankaClient.EXPECT().RegistryList(registryParams).Return(templateList, nil).Times(1) 300 | ankaClient.EXPECT().RegistryRevert(registryParams.Remote, templateList[0].ID).Return(nil).Times(0) 301 | ankaClient.EXPECT().RegistryPush(registryParams, pushParams).Return(nil).Times(1) 302 | 303 | mockui := packer.MockUi{} 304 | mockui.Say(fmt.Sprintf("Pushing template to Anka Registry as %s with tag %s", config.RemoteVM, config.Tag)) 305 | mockui.Say(fmt.Sprintf("Found existing template %s on registry that matches name '%s'", templateList[0].ID, config.RemoteVM)) 306 | 307 | assert.Equal(t, mockui.SayMessages[0].Message, "Pushing template to Anka Registry as foo with tag registry-push") 308 | assert.Equal(t, mockui.SayMessages[1].Message, "Found existing template foo_id on registry that matches name 'foo'") 309 | 310 | _, _, _, err = pp.PostProcess(context.Background(), ui, artifact) 311 | if err != nil { 312 | t.Fail() 313 | } 314 | }) 315 | 316 | t.Run("push to registry with existing templates with latest tag match and revert tag first [packer build -force]", func(t *testing.T) { 317 | err := json.Unmarshal(json.RawMessage(`[{ "id": "foo_id", "name": "foo", "latest": "registry-push" }]`), &templateList) 318 | if err != nil { 319 | t.Fail() 320 | } 321 | 322 | packerConfig := common.PackerConfig{ 323 | PackerForce: true, 324 | } 325 | 326 | config := Config{ 327 | PackerConfig: packerConfig, 328 | RemoteVM: "foo", 329 | Tag: "registry-push", 330 | Description: "mock for testing anka registry push", 331 | HostArch: runtime.GOARCH, 332 | } 333 | 334 | pp := PostProcessor{ 335 | config: config, 336 | client: ankaClient, 337 | } 338 | 339 | registryParams := client.RegistryParams{ 340 | Remote: "go-mock", 341 | HostArch: config.HostArch, 342 | } 343 | 344 | pushParams := client.RegistryPushParams{ 345 | Tag: config.Tag, 346 | Description: config.Description, 347 | RemoteVM: config.RemoteVM, 348 | Local: false, 349 | Force: false, 350 | } 351 | 352 | ankaClient.EXPECT().RegistryListRepos().Return(reposList, nil).Times(1) 353 | 354 | ankaClient.EXPECT().RegistryList(registryParams).Return(templateList, nil).Times(1) 355 | ankaClient.EXPECT().RegistryRevert(registryParams.Remote, templateList[0].ID).Return(nil).Times(1) 356 | ankaClient.EXPECT().RegistryPush(registryParams, pushParams).Return(nil).Times(1) 357 | 358 | mockui := packer.MockUi{} 359 | mockui.Say(fmt.Sprintf("Pushing template to Anka Registry as %s with tag %s", config.RemoteVM, config.Tag)) 360 | mockui.Say(fmt.Sprintf("Found existing template %s on registry that matches name '%s'", templateList[0].ID, config.RemoteVM)) 361 | mockui.Say(fmt.Sprintf("Reverted latest tag for template '%s' on registry", templateList[0].ID)) 362 | 363 | assert.Equal(t, mockui.SayMessages[0].Message, "Pushing template to Anka Registry as foo with tag registry-push") 364 | assert.Equal(t, mockui.SayMessages[1].Message, "Found existing template foo_id on registry that matches name 'foo'") 365 | assert.Equal(t, mockui.SayMessages[2].Message, "Reverted latest tag for template 'foo_id' on registry") 366 | 367 | _, _, _, err = pp.PostProcess(context.Background(), ui, artifact) 368 | if err != nil { 369 | t.Fail() 370 | } 371 | }) 372 | 373 | t.Run("force push to registry with existing template", func(t *testing.T) { 374 | err := json.Unmarshal(json.RawMessage(`[{ "id": "foo_id", "name": "foo", "latest": "registry-push" }]`), &templateList) 375 | if err != nil { 376 | t.Fail() 377 | } 378 | 379 | config := Config{ 380 | RemoteVM: "foo", 381 | Tag: "registry-push", 382 | Description: "mock for testing anka registry push", 383 | HostArch: runtime.GOARCH, 384 | Force: true, 385 | } 386 | 387 | pp := PostProcessor{ 388 | config: config, 389 | client: ankaClient, 390 | } 391 | 392 | registryParams := client.RegistryParams{ 393 | Remote: "go-mock", 394 | HostArch: config.HostArch, 395 | } 396 | 397 | pushParams := client.RegistryPushParams{ 398 | Tag: config.Tag, 399 | Description: config.Description, 400 | RemoteVM: config.RemoteVM, 401 | Local: false, 402 | Force: true, 403 | } 404 | 405 | ankaClient.EXPECT().RegistryListRepos().Return(reposList, nil).Times(1) 406 | 407 | ankaClient.EXPECT().RegistryList(registryParams).Return(templateList, nil).Times(1) 408 | ankaClient.EXPECT().RegistryRevert(registryParams.Remote, templateList[0].ID).Return(nil).Times(0) 409 | ankaClient.EXPECT().RegistryPush(registryParams, pushParams).Return(nil).Times(1) 410 | 411 | mockui := packer.MockUi{} 412 | mockui.Say(fmt.Sprintf("Pushing template to Anka Registry as %s with tag %s", config.RemoteVM, config.Tag)) 413 | mockui.Say(fmt.Sprintf("Found existing template %s on registry that matches name '%s'", templateList[0].ID, config.RemoteVM)) 414 | 415 | assert.Equal(t, mockui.SayMessages[0].Message, "Pushing template to Anka Registry as foo with tag registry-push") 416 | assert.Equal(t, mockui.SayMessages[1].Message, "Found existing template foo_id on registry that matches name 'foo'") 417 | 418 | _, _, _, err = pp.PostProcess(context.Background(), ui, artifact) 419 | if err != nil { 420 | t.Fail() 421 | } 422 | }) 423 | 424 | } 425 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "math/rand" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | "time" 15 | "bytes" 16 | 17 | "github.com/groob/plist" 18 | "github.com/hashicorp/packer-plugin-sdk/multistep" 19 | "github.com/hashicorp/packer-plugin-sdk/packer" 20 | "github.com/hashicorp/packer-plugin-sdk/pathing" 21 | ) 22 | 23 | var ( 24 | random *rand.Rand 25 | letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 26 | ) 27 | 28 | // InstallerAppPlist is a list of variables that comes from the installer app 29 | type InstallerAppPlist struct { 30 | OSVersion string `plist:"DTPlatformVersion"` 31 | BundlerVersion string `plist:"CFBundleShortVersionString"` 32 | } 33 | 34 | // InstallerIPSWPlist is a list of variables that comes from the ipsw 35 | type InstallerIPSWPlist struct { 36 | ProductVersion string `plist:"ProductVersion"` 37 | ProductBuildVersion string `plist:"ProductBuildVersion"` 38 | } 39 | 40 | // Util defines everything this utility can do 41 | type Util interface { 42 | ConfigTmpDir() (string, error) 43 | ConvertDiskSizeToBytes(diskSize string) (uint64, error) 44 | ObtainMacOSVersionFromInstallerApp(path string) (InstallerAppPlist, error) 45 | ObtainMacOSVersionFromInstallerIPSW(path string) (InstallerIPSWPlist, error) 46 | RandSeq(n int) string 47 | StepError(ui packer.Ui, state multistep.StateBag, err error) multistep.StepAction 48 | // ExecuteHostCommand(name string, arg ...string) string 49 | } 50 | 51 | // AnkaUtil implements Util 52 | type AnkaUtil struct { 53 | } 54 | 55 | // StepError will return a halt action when any step fails 56 | func (u *AnkaUtil) StepError(ui packer.Ui, state multistep.StateBag, err error) multistep.StepAction { 57 | state.Put("error", err) 58 | 59 | ui.Error(err.Error()) 60 | 61 | return multistep.ActionHalt 62 | } 63 | 64 | // ConvertDiskSizeToBytes will convert the string into actual bytes the vm utilizes 65 | func (u *AnkaUtil) ConvertDiskSizeToBytes(diskSize string) (uint64, error) { 66 | match, err := regexp.MatchString("^[0-9]+[g|G|m|M]$", diskSize) 67 | if err != nil { 68 | return uint64(0), err 69 | } 70 | if !match { 71 | return 0, fmt.Errorf("Input %s is not a valid disk size input", diskSize) 72 | } 73 | 74 | numericValue, err := strconv.Atoi(diskSize[:len(diskSize)-1]) 75 | if err != nil { 76 | return uint64(0), err 77 | } 78 | suffix := diskSize[len(diskSize)-1:] 79 | 80 | switch strings.ToUpper(suffix) { 81 | case "G": 82 | return uint64(numericValue * 1024 * 1024 * 1024), nil 83 | case "M": 84 | return uint64(numericValue * 1024 * 1024), nil 85 | default: 86 | return uint64(0), fmt.Errorf("Invalid disk size suffix: %s", suffix) 87 | } 88 | } 89 | 90 | // ObtainMacOSVersionFromInstaller abstracts the os version from the installer ipsw provided 91 | func (u *AnkaUtil) ObtainMacOSVersionFromInstallerIPSW(path string) (InstallerIPSWPlist, error) { 92 | installerPlist := InstallerIPSWPlist{} 93 | plistContent := bytes.NewReader([]byte(executeHostCommand("unzip", "-p", path, "SystemVersion.plist"))) 94 | err := plist.NewXMLDecoder(plistContent).Decode(&installerPlist) 95 | if err != nil { 96 | return installerPlist, err 97 | } 98 | return installerPlist, nil 99 | } 100 | 101 | // ObtainMacOSVersionFromInstaller abstracts the os version from the installer app provided 102 | func (u *AnkaUtil) ObtainMacOSVersionFromInstallerApp(path string) (InstallerAppPlist, error) { 103 | installerPlist := InstallerAppPlist{} 104 | _, err := os.Stat(path) 105 | if os.IsNotExist(err) { 106 | return installerPlist, fmt.Errorf("installer app does not exist at %q: %w", path, err) 107 | } 108 | if err != nil { 109 | return installerPlist, fmt.Errorf("failed to stat installer at %q: %w", path, err) 110 | } 111 | plistPath := filepath.Join(path, "Contents", "Info.plist") 112 | _, err = os.Stat(plistPath) 113 | if os.IsNotExist(err) { 114 | return installerPlist, fmt.Errorf("installer app info plist did not exist at %q: %w", plistPath, err) 115 | } 116 | if err != nil { 117 | return installerPlist, fmt.Errorf("failed to stat installer app info plist at %q: %w", plistPath, err) 118 | } 119 | plistContent, _ := os.Open(plistPath) 120 | err = plist.NewXMLDecoder(plistContent).Decode(&installerPlist) 121 | if err != nil { 122 | return installerPlist, err 123 | } 124 | return installerPlist, nil 125 | } 126 | 127 | // ConfigTmpDir creates the temp dir used by packer during runtime 128 | func (u *AnkaUtil) ConfigTmpDir() (string, error) { 129 | configdir, err := pathing.ConfigDir() 130 | if err != nil { 131 | return "", err 132 | } 133 | 134 | tmpdir := os.Getenv("PACKER_TMP_DIR") 135 | if tmpdir != "" { 136 | fp, err := filepath.Abs(tmpdir) 137 | log.Printf("found PACKER_TMP_DIR env variable; setting tmpdir to %s", fp) 138 | if err != nil { 139 | return "", err 140 | } 141 | 142 | configdir = fp 143 | } 144 | 145 | _, err = os.Stat(configdir) 146 | if err != nil { 147 | if os.IsNotExist(err) { 148 | log.Printf("Config dir %s does not exist; creating...", configdir) 149 | 150 | err = os.MkdirAll(configdir, 0755) 151 | if err != nil { 152 | return "", err 153 | } 154 | } else { 155 | return "", err 156 | } 157 | } 158 | 159 | td, err := ioutil.TempDir(configdir, "tmp") 160 | if err != nil { 161 | return "", fmt.Errorf("Error creating temp dir: %s", err) 162 | } 163 | 164 | log.Printf("Set Packer temp dir to %s", td) 165 | return td, nil 166 | } 167 | 168 | func (u *AnkaUtil) RandSeq(n int) string { 169 | random = rand.New(rand.NewSource(time.Now().UnixNano())) 170 | 171 | b := make([]rune, n) 172 | for i := range b { 173 | b[i] = letters[random.Intn(len(letters))] 174 | } 175 | 176 | return string(b) 177 | } 178 | 179 | func executeHostCommand(name string, arg ...string) string { 180 | output, _ := exec.Command(name, arg...).CombinedOutput() 181 | 182 | return string(output) 183 | } 184 | --------------------------------------------------------------------------------