├── .circleci └── config.yml ├── .editorconfig ├── .envrc ├── .gitattributes ├── .gitignore ├── .golangci.yml ├── CONTRIBUTING.md ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── Makefile ├── README.md ├── ci-scripts ├── build.sh ├── helpers │ ├── build_binaries.sh │ ├── clean.sh │ ├── create_artifacts.sh │ ├── create_github_release.sh │ └── get_release_version.sh └── pull_request.sh ├── docs ├── decisions.md ├── provider │ ├── data_sources │ │ ├── environment.md │ │ ├── lifecycle.md │ │ └── project.md │ └── resources │ │ ├── environment.md │ │ └── lifecycle.md └── to_move_to_provider.md ├── examples └── main.tf ├── go.mod ├── go.sum ├── main.go └── octopusdeploy ├── config.go ├── data_environment.go ├── data_library_variable_set.go ├── data_lifecycle.go ├── data_machine.go ├── data_machinepolicy.go ├── data_project.go ├── data_variable.go ├── deploy_package.go ├── deploy_windows_service_action.go ├── deploy_windows_service_action_test.go ├── deployment_action.go ├── deployment_process.go ├── deployment_process_test.go ├── deployment_step.go ├── manual_intervention_action.go ├── manual_intervention_action_test.go ├── package_reference.go ├── property.go ├── provider.go ├── provider_test.go ├── resource_environment.go ├── resource_environment_test.go ├── resource_library_variable_set.go ├── resource_library_variable_set_test.go ├── resource_lifecycle.go ├── resource_lifecycle_test.go ├── resource_machine.go ├── resource_machine_test.go ├── resource_project.go ├── resource_project_deployment_target_trigger.go ├── resource_project_deployment_target_trigger_test.go ├── resource_project_group.go ├── resource_project_group_test.go ├── resource_project_test.go ├── resource_variable.go ├── resource_variable_test.go └── util.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | go-container: 5 | docker: 6 | - image: circleci/golang:1.11.4 7 | 8 | working_directory: /go/src/github.com/MattHodge/terraform-provider-octopusdeploy 9 | 10 | environment: 11 | # GITHUB_TOKEN: Specified In UI 12 | # OCTOPUS_APIKEY: Specified In UI 13 | # OCTOPUS_URL: Specified In UI 14 | GO111MODULE: "on" # Enable Go Modules 15 | 16 | jobs: 17 | 18 | test: 19 | executor: go-container 20 | 21 | steps: 22 | - checkout 23 | - run: ci-scripts/pull_request.sh 24 | - save_cache: 25 | key: go-mod-v1-{{ checksum "go.sum" }} 26 | paths: 27 | - "/go/pkg/mod" 28 | 29 | build: 30 | executor: go-container 31 | 32 | steps: 33 | - checkout 34 | - restore_cache: 35 | keys: 36 | - go-mod-v1-{{ checksum "go.sum" }} 37 | - run: ci-scripts/build.sh 38 | - persist_to_workspace: 39 | root: artifacts 40 | paths: 41 | - "*.zip" 42 | 43 | publish-github-release: 44 | docker: 45 | - image: cibuilds/github:0.12 46 | steps: 47 | - checkout 48 | - attach_workspace: 49 | at: artifacts 50 | - store_artifacts: 51 | path: artifacts/ 52 | - run: 53 | name: "Publish Release on GitHub" 54 | command: | 55 | . ci-scripts/helpers/get_release_version.sh 56 | ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete v${RELEASE_VERSION} ./artifacts/ 57 | 58 | workflows: 59 | version: 2 60 | build: 61 | jobs: 62 | - test 63 | - build: 64 | requires: 65 | - test 66 | filters: 67 | branches: 68 | only: 69 | - master 70 | - publish-github-release: 71 | requires: 72 | - build 73 | filters: 74 | branches: 75 | only: 76 | - master 77 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; indicate this is the root of the project 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | 7 | end_of_line = LF 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [Makefile] 15 | indent_style = tab 16 | 17 | [makefile] 18 | indent_style = tab 19 | 20 | [*.go] 21 | indent_style = tab 22 | 23 | [*.{yml,yaml}] 24 | indent_style = space 25 | indent_size = 2 26 | trim_trailing_whitespace = true 27 | 28 | [*.{psm1,ps1,cs,cshtml}] 29 | charset = utf-8 30 | indent_style = tab 31 | indent_size = 4 32 | trim_trailing_whitespace = true 33 | 34 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | dotenv 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Force checkout line endings in Unix format - https://help.github.com/articles/dealing-with-line-endings/ 2 | * text eol=lf 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Editors 15 | .atom/ 16 | .idea/ 17 | .vscode/ 18 | 19 | # Go Debugging 20 | OFF/ 21 | 22 | # Terraform 23 | .terraform/ 24 | *.tfstate 25 | *.tfstate.backup 26 | crash.log 27 | tflog.txt 28 | 29 | # TF Binaries 30 | terraform-provider-octopusdeploy 31 | terraform-provider-octopusdeploy.exe 32 | main.tf 33 | build/ 34 | artifacts/ 35 | 36 | node_modules/ 37 | .env 38 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - golint 4 | - gosec 5 | - unconvert 6 | - gocritic 7 | disable: 8 | - errcheck 9 | # Prevent this error: Error return value of `d.Set` is not checked (errcheck) 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Go Dependencies 4 | 5 | Dependencies are managed using [Go 1.11 Modules](https://github.com/golang/go/wiki/Modules) 6 | 7 | ## Local Integration Tests 8 | 9 | To make development easier, run a local Octopus Deploy server on your machine. You can `vagrant up` [this image](https://github.com/MattHodge/VagrantBoxes/tree/master/OctopusDeployServer) to get a fully working Octopus Deploy Server. 10 | 11 | When it comes up, login on [http://localhost:8081](http://localhost:8081) with username `Administrator` and password `OctoVagrant!`. 12 | 13 | To get an API to use for local development, go to **Administrator | Profile | My API Keys** and click **New API Key**. 14 | 15 | Set the two following environment variables: 16 | 17 | ```bash 18 | export OCTOPUS_URL=http://localhost:8081/ 19 | export OCTOPUS_APIKEY=API-YOUR-API-KEY 20 | ``` 21 | 22 | You can now run integration tests. 23 | 24 | ## Running Pull Requests Locally 25 | 26 | You can locally test pull requests just as the build server would. 27 | 28 | - Install [golangci-lint](https://github.com/golangci/golangci-lint) 29 | - Run `./ci-scripts/pull_request.sh` 30 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/MattHodge/go-octopusdeploy" 31 | 32 | [[constraint]] 33 | name = "github.com/hashicorp/terraform" 34 | version = "0.11.7" 35 | 36 | [prune] 37 | go-tests = true 38 | unused-packages = true 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Matthew Hodgkins 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 | TEST?=$$(go list ./... |grep -v 'vendor') 2 | 3 | default: build test 4 | 5 | fmt: 6 | go fmt ./octopusdeploy/... 7 | 8 | simplify: 9 | gofmt -s -w ./octopusdeploy 10 | 11 | build: fmt simplify 12 | go build 13 | 14 | test: fmt 15 | go test -v -timeout 30s ./... 16 | 17 | testacc: 18 | TF_ACC=1 go test $(TEST) -v -timeout 120m 19 | 20 | testaccnocache: 21 | TF_ACC=1 go test $(TEST) -v -timeout 120m -count=1 22 | 23 | tf_build: fmt build 24 | terraform init 25 | terraform plan 26 | terraform apply -auto-approve 27 | 28 | tf_destroy: 29 | terraform destroy -auto-approve 30 | 31 | tf_apply: 32 | terraform apply -auto-approve 33 | 34 | tf_plan: 35 | terraform plan 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [DEPRECIATED] terraform-provider-octopusdeploy 2 | 3 | Good news! 4 | 5 | The team at [Octopus Deploy](https://octopus.com/) have taken notice of this project :tada:. 6 | 7 | We have both decided the best way forward for the project is for the team at Octopus Deploy to take over the reins and continue the development and support. 8 | 9 | This means that Octopus Deploy will be getting an *official* Terraform provider :heart_eyes:. 10 | 11 | It was never my intention to spend a lot of time looking after this project, so I am very excited that the team reached out and offered assistance. 12 | 13 | The project has been forked at https://github.com/OctopusDeploy/terraform-provider-octopusdeploy. 14 | 15 | Please head over there to contribute and file bug reports! 16 | 17 | Happy Deploying! 18 | 19 | Matthew Hodgkins 20 | * **Twitter** - [@MattHodge](https://twitter.com/matthodge) 21 | * **Blog** - [hodgkins.io](https://hodgkins.io) 22 | -------------------------------------------------------------------------------- /ci-scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | # Clean the directories 5 | ci-scripts/helpers/clean.sh 6 | 7 | # Load the release version environment variable 8 | . ci-scripts/helpers/get_release_version.sh 9 | 10 | # Build the binaries 11 | ci-scripts/helpers/build_binaries.sh 12 | 13 | # Create zip files 14 | ci-scripts/helpers/create_artifacts.sh 15 | -------------------------------------------------------------------------------- /ci-scripts/helpers/build_binaries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | if [ -z "${RELEASE_VERSION}" ]; then 5 | echo "The environment variable RELEASE_VERSION needs to be set. Exiting script." 6 | exit 1 7 | fi 8 | 9 | BUILD_PATH_TEMPLATE="build/terraform-provider-octopusdeploy-{{.OS}}_{{.Arch}}-${RELEASE_VERSION}/{{.Dir}}_v${RELEASE_VERSION}" 10 | 11 | go get github.com/mitchellh/gox 12 | gox -osarch="linux/amd64" -osarch="linux/386" -osarch="windows/amd64" -osarch="windows/386" -osarch="darwin/amd64" -osarch="darwin/386" -output="${BUILD_PATH_TEMPLATE}" 13 | -------------------------------------------------------------------------------- /ci-scripts/helpers/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | echo "Performing Clean" 5 | 6 | rm -rf build/ 7 | rm -rf artifacts/ 8 | -------------------------------------------------------------------------------- /ci-scripts/helpers/create_artifacts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | ARTIFACTS_PATH="${PWD}/artifacts" 5 | mkdir -p $ARTIFACTS_PATH 6 | 7 | for build in $(ls -d build/*) 8 | do 9 | pushd $build 10 | 11 | RELEASENAME=$(basename $build) 12 | ZIPNAME="${RELEASENAME}.zip" 13 | 14 | echo "Preparing zip file ${ZIPNAME}" 15 | 16 | # Adding files to zip 17 | zip $ZIPNAME * 18 | 19 | mv $ZIPNAME $ARTIFACTS_PATH 20 | popd 21 | done 22 | -------------------------------------------------------------------------------- /ci-scripts/helpers/create_github_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | if [ -z "${RELEASE_VERSION}" ]; then 5 | echo "The environment variable RELEASE_VERSION needs to be set. Exiting script." 6 | exit 1 7 | fi 8 | 9 | go get -u github.com/tcnksm/ghr 10 | 11 | ghr -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${RELEASE_VERSION} ./artifacts/ 12 | -------------------------------------------------------------------------------- /ci-scripts/helpers/get_release_version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Dot Source this script to set the RELEASE_VERSION environment variable 4 | 5 | set -eo pipefail 6 | 7 | if [ -z "${CIRCLE_BUILD_NUM}" ]; then 8 | echo "The environment variable CIRCLE_BUILD_NUM is not set. Setting as 999." 9 | CIRCLE_BUILD_NUM="999" 10 | fi 11 | 12 | RELEASE_VERSION="0.0.2-alpha.${CIRCLE_BUILD_NUM}" 13 | echo "Release version is ${RELEASE_VERSION}" 14 | 15 | export RELEASE_VERSION=$RELEASE_VERSION 16 | -------------------------------------------------------------------------------- /ci-scripts/pull_request.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | 4 | # Download modules first otherwise linting has errors 5 | go mod download 6 | 7 | # Lint 8 | 9 | ## When in CI, install linter and run from a special path 10 | if [ -n "${CI}" ]; then 11 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s v1.12.4 12 | 13 | ./bin/golangci-lint run 14 | else 15 | golangci-lint run 16 | fi 17 | 18 | # Test 19 | 20 | ## Enable TF Acceptance Tests 21 | export TF_ACC=1 22 | 23 | go test -v -timeout 240s ./octopusdeploy/... 24 | -------------------------------------------------------------------------------- /docs/decisions.md: -------------------------------------------------------------------------------- 1 | # Decisions 2 | 3 | ## Development 4 | 5 | * You cannot use `ConflictsWith` inside `schema.TypeList`. Instead I am splitting them up into their own `schema.TypeList`. 6 | 7 | ## Documentation 8 | 9 | * Documentation for usage of the Terraform provider will go into the `/docs/provider/` folder and be split into the `data_sources` or `resources` sub-folder. 10 | 11 | * Documentation will be done in Markdown and match the style of the official Terraform providers. This is to make it easy to transition to official provider documentation if this occurs. 12 | -------------------------------------------------------------------------------- /docs/provider/data_sources/environment.md: -------------------------------------------------------------------------------- 1 | # octopusdeploy_environment 2 | 3 | Use this data source to retrieve information about an Octopus Deploy [environment](https://octopus.com/docs/infrastructure/environments). 4 | 5 | ## Example Usage 6 | 7 | ```hcl 8 | data "octopusdeploy_environment" "testing" { 9 | name = "Testing" 10 | } 11 | ``` 12 | 13 | ## Argument Reference 14 | 15 | The following arguments are supported: 16 | 17 | * `name` - (Required) The name of the environment. 18 | 19 | ## Attributes Reference 20 | 21 | * `id` - ID of the environment. 22 | 23 | * `description` - A description of the environment. 24 | 25 | * `use_guided_failure` - Whether guided failure mode is enabled or not. 26 | -------------------------------------------------------------------------------- /docs/provider/data_sources/lifecycle.md: -------------------------------------------------------------------------------- 1 | # octopusdeploy_lifecycle 2 | 3 | Use this data source to retrieve information about an Octopus Deploy [lifecycle](https://octopus.com/docs/deployment-process/lifecycles). 4 | 5 | ## Example Usage 6 | 7 | ```hcl 8 | data "octopusdeploy_lifecycle" "testing" { 9 | name = "Testing" 10 | } 11 | ``` 12 | 13 | ## Argument Reference 14 | 15 | The following arguments are supported: 16 | 17 | * `name` - (Required) The name of the lifecycle. 18 | 19 | ## Attributes Reference 20 | 21 | * `id` - ID of the environment. 22 | 23 | * `description` - A description of the lifecycle. 24 | -------------------------------------------------------------------------------- /docs/provider/data_sources/project.md: -------------------------------------------------------------------------------- 1 | # octopusdeploy_project 2 | 3 | Use this data source to retrieve information about an Octopus Deploy project. 4 | 5 | ## Example Usage 6 | 7 | ```hcl 8 | data "octopusdeploy_project" "my_project" { 9 | name = "My Project" 10 | } 11 | ``` 12 | 13 | ## Argument Reference 14 | 15 | The following arguments are supported: 16 | 17 | * `name` - (Required) The name of the project. 18 | 19 | ## Attributes Reference 20 | 21 | * `description` - A description of the project. 22 | 23 | * `lifecycle_id` - The life cycle identifier for the project. 24 | 25 | * `project_group_id` - The project group identifer for the project. 26 | 27 | * `default_failure_mode` - The failure mode for the project. 28 | 29 | * `skip_machine_behavior` - The skip machine behavior for the project. 30 | -------------------------------------------------------------------------------- /docs/provider/resources/environment.md: -------------------------------------------------------------------------------- 1 | # octopusdeploy_environment 2 | 3 | Use this resource allows the creation of Octopus Deploy [environment](https://octopus.com/docs/infrastructure/environments). 4 | 5 | Environments help you organize your deployment targets. 6 | 7 | ## Example Usage 8 | 9 | ```hcl 10 | resource "octopusdeploy_environment" "staging" { 11 | name = "Staging" 12 | description = "Staging environment" 13 | use_guided_failure = false 14 | } 15 | ``` 16 | 17 | ## Argument Reference 18 | 19 | The following arguments are supported: 20 | 21 | * `name` - (Required) Name of the environment. 22 | 23 | * `description` - (Optional) Description of the environment. 24 | 25 | * `use_guided_failure` - (Optional) Use guided failures for this environment. Defaults to `false`. 26 | 27 | ## Attributes Reference 28 | 29 | The following attributes are exported: 30 | 31 | * `id` - ID of the environment. 32 | -------------------------------------------------------------------------------- /docs/provider/resources/lifecycle.md: -------------------------------------------------------------------------------- 1 | # octopusdeploy_lifecycle 2 | 3 | Use this resource allows the creation of Octopus Deploy [lifecycles](https://octopus.com/docs/deployment-process/lifecycles). 4 | 5 | Lifecycles can be used to automatically promote deployments between environments, and limit environments that can be deployed to until a release has been thoroughly tested. 6 | 7 | ## Example Usage 8 | 9 | ```hcl 10 | resource "octopusdeploy_environment" "Env1" { 11 | name = "LifecycleTestEnv1" 12 | } 13 | 14 | resource "octopusdeploy_environment" "Env2" { 15 | name = "LifecycleTestEnv2" 16 | } 17 | 18 | resource "octopusdeploy_environment" "Env3" { 19 | name = "LifecycleTestEnv3" 20 | } 21 | 22 | resource "octopusdeploy_lifecycle" "foo" { 23 | name = "Funky Lifecycle" 24 | description = "Funky Lifecycle description" 25 | 26 | release_retention_policy { 27 | unit = "Items" 28 | quantity_to_keep = 2 29 | } 30 | 31 | tentacle_retention_policy { 32 | unit = "Days" 33 | quantity_to_keep = 1 34 | } 35 | 36 | phase { 37 | name = "P1" 38 | minimum_environments_before_promotion = 2 39 | is_optional_phase = true 40 | automatic_deployment_targets = ["${octopusdeploy_environment.Env1.id}"] 41 | optional_deployment_targets = ["${octopusdeploy_environment.Env2.id}"] 42 | } 43 | 44 | phase { 45 | name = "P2" 46 | } 47 | } 48 | 49 | ``` 50 | 51 | ## Argument Reference 52 | 53 | The following arguments are supported: 54 | 55 | * `name` - (Required) Name of the lifecycle. 56 | 57 | * `description` - (Optional) Description of the lifecycle. 58 | 59 | * `release_retention_policy` - (Optional) A release retention policy block as documented below. 60 | 61 | * `phase` - (Optional) A phase block as documented below. 62 | 63 | Release Retention Policy (`release_retention_policy`) blocks support the following: 64 | 65 | * `unit` - (Optional) The unit of quantity to keep. Either `Days` or `Items`. Defaults to `Days`. 66 | 67 | * `quantity_to_keep` - (Optional) The number of units required before a release can enter the next phase. If 0, all environments are required. Defaults to `0`. 68 | 69 | Phase (`phase`) blocks support the following: 70 | 71 | * `name` - (Required) The name of the phase. 72 | 73 | * `minimum_environments_before_promotion` - (Optional) The number of days/releases to keep. If 0 all are kept. Defaults to `0`. 74 | 75 | * `is_optional_phase` - (Optional) If false a release must be deployed to this phase before it can be deployed to the next phase. Defaults to `false`. 76 | 77 | * `automatic_deployment_targets` - (Optional) Environment Ids in this phase that a release is automatically deployed to when it is eligible for this phase. 78 | 79 | * `optional_deployment_targets` - (Optional) Environment Ids in this phase that a release can be deployed to, but is not automatically deployed to. 80 | 81 | ## Attributes Reference 82 | 83 | The following attributes are exported: 84 | 85 | * `id` - ID of the environment. 86 | -------------------------------------------------------------------------------- /docs/to_move_to_provider.md: -------------------------------------------------------------------------------- 1 | ## Project Groups 2 | 3 | [Project groups](https://octopus.com/docs/deployment-process/projects#project-group) are a way of organizing your projects. 4 | 5 | ### Example Usage 6 | 7 | Basic usage: 8 | ```hcl 9 | resource "octopusdeploy_project_group" "finance" { 10 | description = "Financial Applications" 11 | name = "Finance" 12 | } 13 | ``` 14 | 15 | Data usage: 16 | 17 | ```hcl 18 | data "octopusdeploy_project" "finance" { 19 | name = "Finance" 20 | } 21 | ``` 22 | 23 | Basic usage with ID export used to create project: 24 | ```hcl 25 | resource "octopusdeploy_project_group" "finance" { 26 | description = "Financial Applications" 27 | name = "Finance" 28 | } 29 | 30 | resource "octopusdeploy_project" "billing_service" { 31 | description = "The Finance teams billing service" 32 | lifecycle_id = "Lifecycles-1" 33 | name = "Billing Service" 34 | project_group_id = "${octopusdeploy_project_group.finance.id}" 35 | } 36 | ``` 37 | 38 | ### Argument Reference 39 | * `description` - (Optional) Description of the project group 40 | * `name` - (Required) Name of the project group 41 | 42 | ### Attributes Reference 43 | * `id` - The ID of the project group 44 | 45 | 46 | ## Project 47 | 48 | [Projects](https://octopus.com/docs/deployment-process/projects) can consist of multiple deployment steps, or they might only have a single step. 49 | 50 | ### Example Usage 51 | Basic project with some settings but no deployment steps: 52 | ```hcl 53 | resource "octopusdeploy_project_group" "finance" { 54 | description = "Financial Applications" 55 | name = "Finance" 56 | } 57 | 58 | resource "octopusdeploy_project" "test_project" { 59 | description = "A really groundbreaking app" 60 | lifecycle_id = "Lifecycles-1" 61 | name = "Epic App" 62 | project_group_id = "${octopusdeploy_project_group.finance.id}" 63 | skip_machine_behavior = "SkipUnavailableMachines" 64 | } 65 | ``` 66 | 67 | Project with many settings configured and multiple types of deployment steps: 68 | ```hcl 69 | resource "octopusdeploy_project_group" "finance" { 70 | description = "Financial Applications" 71 | name = "Finance" 72 | } 73 | 74 | resource "octopusdeploy_project" "billing_service" { 75 | description = "Billing Frontend and Backend" 76 | lifecycle_id = "Lifecycles-1" 77 | name = "Billing Service" 78 | project_group_id = "${octopusdeploy_project_group.finance.id}" 79 | skip_machine_behavior = "SkipUnavailableMachines" 80 | 81 | deployment_step_windows_service { 82 | executable_path = "batch_processor\\batch_processor_service.exe" 83 | service_name = "Billing Batch Processor" 84 | step_name = "Deploy Billing Batch Processor Windows Service" 85 | step_condition = "failure" 86 | package = "Billing.BatchProcessor" 87 | json_file_variable_replacement = "appsettings.json" 88 | 89 | target_roles = [ 90 | "Billing-Batch-Processor", 91 | ] 92 | } 93 | 94 | deployment_step_inline_script { 95 | step_name = "Cleanup Temporary Files" 96 | script_type = "PowerShell" 97 | 98 | script_body = < 1 { 111 | return fmt.Errorf("found %v variables for project %s with name %s, should match exactly 1", len(varItems), varProject, varName) 112 | } 113 | 114 | d.SetId(varItems[0].ID) 115 | d.Set("name", varItems[0].Name) 116 | d.Set("type", varItems[0].Type) 117 | d.Set("value", varItems[0].Value) 118 | d.Set("description", varItems[0].Description) 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /octopusdeploy/deploy_package.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 5 | "github.com/hashicorp/terraform/helper/schema" 6 | ) 7 | 8 | func getDeployPackageAction() *schema.Schema { 9 | actionSchema, element := getCommonDeploymentActionSchema() 10 | addPrimaryPackageSchema(element, true) 11 | //addCustomInstallationDirectoryFeature(element) 12 | //addIisWebSiteAndApplicationPoolFeature(element) 13 | addWindowsServiceFeature(element) 14 | //addCustomDeploymentScriptsFeature(element) 15 | //addJsonConfigurationVariablesFeature(element) 16 | //addConfigurationVariablesFeature(element) 17 | //addConfigurationTransformsFeature(element) 18 | //addSubstituteVariablesInFilesFeature(element) 19 | //addIis6HomeDirectoryFeature(element) 20 | //addRedGateDatabaseDeploymentFeature(element) 21 | return actionSchema 22 | } 23 | 24 | func buildDeployPackageActionResource(tfAction map[string]interface{}) octopusdeploy.DeploymentAction { 25 | action := buildDeploymentActionResource(tfAction) 26 | action.ActionType = "Octopus.TentaclePackage" 27 | addWindowsServiceFeatureToActionResource(tfAction, action) 28 | return action 29 | } 30 | -------------------------------------------------------------------------------- /octopusdeploy/deploy_windows_service_action.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 5 | "github.com/hashicorp/terraform/helper/schema" 6 | ) 7 | 8 | func getDeployWindowsServiceActionSchema() *schema.Schema { 9 | actionSchema, element := getCommonDeploymentActionSchema() 10 | addPrimaryPackageSchema(element, true) 11 | addDeployWindowsServiceSchema(element) 12 | //addCustomInstallationDirectoryFeature(element) 13 | //addCustomDeploymentScriptsFeature(element) 14 | //addJsonConfigurationVariablesFeature(element) 15 | //addConfigurationVariablesFeature(element) 16 | //addConfigurationTransformsFeature(element) 17 | //addSubstituteVariablesInFilesFeature(element) 18 | return actionSchema 19 | } 20 | 21 | func addWindowsServiceFeature(parent *schema.Resource) { 22 | element := &schema.Resource{ 23 | Schema: map[string]*schema.Schema{}, 24 | } 25 | addDeployWindowsServiceSchema(element) 26 | parent.Schema["windows_service"] = &schema.Schema{ 27 | Description: "Deploy a windows service feature", 28 | Type: schema.TypeSet, 29 | Optional: true, 30 | MaxItems: 1, 31 | Elem: element, 32 | } 33 | } 34 | 35 | func addDeployWindowsServiceSchema(element *schema.Resource) { 36 | element.Schema["service_name"] = &schema.Schema{ 37 | Type: schema.TypeString, 38 | Description: "The name of the service", 39 | Required: true, 40 | } 41 | element.Schema["display_name"] = &schema.Schema{ 42 | Type: schema.TypeString, 43 | Description: "The display name of the service (optional)", 44 | Optional: true, 45 | } 46 | element.Schema["description"] = &schema.Schema{ 47 | Type: schema.TypeString, 48 | Description: "User-friendly description of the service (optional)", 49 | Optional: true, 50 | } 51 | element.Schema["executable_path"] = &schema.Schema{ 52 | Type: schema.TypeString, 53 | Description: "The path to the executable relative to the package installation directory", 54 | Required: true, 55 | } 56 | element.Schema["arguments"] = &schema.Schema{ 57 | Type: schema.TypeString, 58 | Description: "The command line arguments that will be passed to the service when it starts", 59 | Optional: true, 60 | } 61 | element.Schema["service_account"] = &schema.Schema{ 62 | Type: schema.TypeString, 63 | Description: "Which built-in account will the service run under. Can be LocalSystem, NT Authority\\NetworkService, NT Authority\\LocalService, _CUSTOM or an expression", 64 | Optional: true, 65 | Default: "LocalSystem", 66 | } 67 | element.Schema["custom_account_name"] = &schema.Schema{ 68 | Type: schema.TypeString, 69 | Description: "The Windows/domain account of the custom user that the service will run under", 70 | Optional: true, 71 | } 72 | element.Schema["custom_account_password"] = &schema.Schema{ 73 | Type: schema.TypeString, 74 | Description: "The password for the custom account", 75 | Optional: true, 76 | } 77 | element.Schema["start_mode"] = &schema.Schema{ 78 | Type: schema.TypeString, 79 | Description: "When will the service start. Can be auto, delayed-auto, manual, unchanged or an expression", 80 | Optional: true, 81 | Default: "auto", 82 | } 83 | element.Schema["dependencies"] = &schema.Schema{ 84 | Type: schema.TypeString, 85 | Description: "Any dependencies that the service has. Separate the names using forward slashes (/).", 86 | Optional: true, 87 | } 88 | } 89 | 90 | func buildDeployWindowsServiceActionResource(tfAction map[string]interface{}) octopusdeploy.DeploymentAction { 91 | resource := buildDeploymentActionResource(tfAction) 92 | resource.ActionType = "Octopus.WindowsService" 93 | addWindowsServiceToActionResource(tfAction, resource) 94 | return resource 95 | } 96 | 97 | func addWindowsServiceFeatureToActionResource(tfAction map[string]interface{}, action octopusdeploy.DeploymentAction) { 98 | if windowsServiceList, ok := tfAction["windows_service"]; ok { 99 | tfWindowsService := windowsServiceList.(*schema.Set).List() 100 | if len(tfWindowsService) > 0 { 101 | addWindowsServiceToActionResource(tfWindowsService[0].(map[string]interface{}), action) 102 | } 103 | } 104 | } 105 | 106 | func addWindowsServiceToActionResource(tfAction map[string]interface{}, action octopusdeploy.DeploymentAction) { 107 | action.Properties["Octopus.Action.WindowsService.CreateOrUpdateService"] = "True" 108 | action.Properties["Octopus.Action.WindowsService.ServiceName"] = tfAction["service_name"].(string) 109 | 110 | displayName := tfAction["display_name"] 111 | if displayName != nil { 112 | action.Properties["Octopus.Action.WindowsService.DisplayName"] = displayName.(string) 113 | } 114 | 115 | description := tfAction["description"] 116 | if description != nil { 117 | action.Properties["Octopus.Action.WindowsService.Description"] = description.(string) 118 | } 119 | 120 | action.Properties["Octopus.Action.WindowsService.ExecutablePath"] = tfAction["executable_path"].(string) 121 | 122 | args := tfAction["arguments"] 123 | if args != nil { 124 | action.Properties["Octopus.Action.WindowsService.Arguments"] = args.(string) 125 | } 126 | 127 | action.Properties["Octopus.Action.WindowsService.ServiceAccount"] = tfAction["service_account"].(string) 128 | 129 | accountName := tfAction["custom_account_name"] 130 | if accountName != nil { 131 | action.Properties["Octopus.Action.WindowsService.CustomAccountName"] = accountName.(string) 132 | } 133 | 134 | accountPassword := tfAction["custom_account_password"] 135 | if accountPassword != nil { 136 | action.Properties["Octopus.Action.WindowsService.CustomAccountPassword"] = accountPassword.(string) 137 | } 138 | 139 | action.Properties["Octopus.Action.WindowsService.StartMode"] = tfAction["start_mode"].(string) 140 | 141 | dependencies := tfAction["dependencies"] 142 | if dependencies != nil { 143 | action.Properties["Octopus.Action.WindowsService.Dependencies"] = dependencies.(string) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /octopusdeploy/deploy_windows_service_action_test.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 8 | "github.com/hashicorp/terraform/helper/resource" 9 | "github.com/hashicorp/terraform/terraform" 10 | ) 11 | 12 | func TestAccOctopusDeployDeployWindowsServiceAction(t *testing.T) { 13 | resource.Test(t, resource.TestCase{ 14 | PreCheck: func() { testAccPreCheck(t) }, 15 | Providers: testAccProviders, 16 | CheckDestroy: testAccCheckOctopusDeployDeploymentProcessDestroy, 17 | Steps: []resource.TestStep{ 18 | { 19 | Config: testAccDeployWindowsServiceAction(), 20 | Check: resource.ComposeTestCheckFunc( 21 | testAccCheckDeployWindowsServiceActionOrFeature("Octopus.WindowsService"), 22 | ), 23 | }, 24 | }, 25 | }) 26 | } 27 | 28 | func TestAccOctopusDeployWindowsServiceFeature(t *testing.T) { 29 | resource.Test(t, resource.TestCase{ 30 | PreCheck: func() { testAccPreCheck(t) }, 31 | Providers: testAccProviders, 32 | CheckDestroy: testAccCheckOctopusDeployDeploymentProcessDestroy, 33 | Steps: []resource.TestStep{ 34 | { 35 | Config: testAccWindowsServiceFeature(), 36 | Check: resource.ComposeTestCheckFunc( 37 | testAccCheckDeployWindowsServiceActionOrFeature("Octopus.TentaclePackage"), 38 | ), 39 | }, 40 | }, 41 | }) 42 | } 43 | 44 | func testAccDeployWindowsServiceAction() string { 45 | return testAccBuildTestActionTerraform(` 46 | deploy_windows_service_action { 47 | name = "Test" 48 | 49 | primary_package { 50 | package_id = "MyPackage" 51 | } 52 | 53 | service_name = "MyService" 54 | display_name = "My Service" 55 | description = "Do stuff" 56 | executable_path = "MyService.exe" 57 | arguments = "-arg" 58 | service_account = "_CUSTOM" 59 | custom_account_name = "User" 60 | custom_account_password = "Password" 61 | start_mode = "manual" 62 | dependencies = "OtherService" 63 | } 64 | `) 65 | } 66 | 67 | func testAccWindowsServiceFeature() string { 68 | return testAccBuildTestActionTerraform(` 69 | deploy_package_action { 70 | name = "Test" 71 | 72 | primary_package { 73 | package_id = "MyPackage" 74 | } 75 | 76 | windows_service { 77 | service_name = "MyService" 78 | display_name = "My Service" 79 | description = "Do stuff" 80 | executable_path = "MyService.exe" 81 | arguments = "-arg" 82 | service_account = "_CUSTOM" 83 | custom_account_name = "User" 84 | custom_account_password = "Password" 85 | start_mode = "manual" 86 | dependencies = "OtherService" 87 | } 88 | } 89 | `) 90 | } 91 | 92 | func testAccCheckDeployWindowsServiceActionOrFeature(expectedActionType string) resource.TestCheckFunc { 93 | return func(s *terraform.State) error { 94 | client := testAccProvider.Meta().(*octopusdeploy.Client) 95 | 96 | process, err := getDeploymentProcess(s, client) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | action := process.Steps[0].Actions[0] 102 | 103 | if action.ActionType != expectedActionType { 104 | return fmt.Errorf("Action type is incorrect: %s, expected: %s", action.ActionType, expectedActionType) 105 | } 106 | 107 | if len(action.Packages) == 0 { 108 | return fmt.Errorf("No package") 109 | } 110 | 111 | if action.Properties["Octopus.Action.WindowsService.CreateOrUpdateService"] != "True" { 112 | return fmt.Errorf("Windows Service feature is not enabled") 113 | } 114 | 115 | if action.Properties["Octopus.Action.WindowsService.ServiceName"] != "MyService" { 116 | return fmt.Errorf("Service Name is incorrect: %s", action.Properties["Octopus.Action.WindowsService.ServiceName"]) 117 | } 118 | 119 | if action.Properties["Octopus.Action.WindowsService.DisplayName"] != "My Service" { 120 | return fmt.Errorf("Display Name is incorrect: %s", action.Properties["Octopus.Action.WindowsService.DisplayName"]) 121 | } 122 | 123 | if action.Properties["Octopus.Action.WindowsService.Description"] != "Do stuff" { 124 | return fmt.Errorf("Description is incorrect: %s", action.Properties["Octopus.Action.WindowsService.Description"]) 125 | } 126 | 127 | if action.Properties["Octopus.Action.WindowsService.ExecutablePath"] != "MyService.exe" { 128 | return fmt.Errorf("Executable Path is incorrect: %s", action.Properties["Octopus.Action.WindowsService.ExecutablePath"]) 129 | } 130 | 131 | if action.Properties["Octopus.Action.WindowsService.Arguments"] != "-arg" { 132 | return fmt.Errorf("Arguments is incorrect: %s", action.Properties["Octopus.Action.WindowsService.Arguments"]) 133 | } 134 | 135 | if action.Properties["Octopus.Action.WindowsService.ServiceAccount"] != "_CUSTOM" { 136 | return fmt.Errorf("Service Account is incorrect: %s", action.Properties["Octopus.Action.WindowsService.ServiceAccount"]) 137 | } 138 | 139 | if action.Properties["Octopus.Action.WindowsService.CustomAccountName"] != "User" { 140 | return fmt.Errorf("Custom Account Name is incorrect: %s", action.Properties["Octopus.Action.WindowsService.CustomAccountName"]) 141 | } 142 | 143 | if action.Properties["Octopus.Action.WindowsService.CustomAccountPassword"] != "Password" { 144 | return fmt.Errorf("Custom Account Password is incorrect: %s", action.Properties["Octopus.Action.WindowsService.CustomAccountPassword"]) 145 | } 146 | 147 | if action.Properties["Octopus.Action.WindowsService.StartMode"] != "manual" { 148 | return fmt.Errorf("Start Mode is incorrect: %s", action.Properties["Octopus.Action.WindowsService.StartMode"]) 149 | } 150 | 151 | if action.Properties["Octopus.Action.WindowsService.Dependencies"] != "OtherService" { 152 | return fmt.Errorf("Dependencies is incorrect: %s", action.Properties["Octopus.Action.WindowsService.Dependencies"]) 153 | } 154 | 155 | return nil 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /octopusdeploy/deployment_action.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 5 | "github.com/hashicorp/terraform/helper/schema" 6 | "strconv" 7 | ) 8 | 9 | func getDeploymentActionSchema() *schema.Schema { 10 | actionSchema, element := getCommonDeploymentActionSchema() 11 | addExecutionLocationSchema(element) 12 | element.Schema["action_type"] = &schema.Schema{ 13 | Type: schema.TypeString, 14 | Description: "The type of action", 15 | Required: true, 16 | } 17 | addWorkerPoolSchema(element) 18 | addPackagesSchema(element, false) 19 | 20 | return actionSchema 21 | } 22 | 23 | func getCommonDeploymentActionSchema() (*schema.Schema, *schema.Resource) { 24 | element := &schema.Resource{ 25 | Schema: map[string]*schema.Schema{ 26 | "name": { 27 | Type: schema.TypeString, 28 | Description: "The name of the action", 29 | Required: true, 30 | }, 31 | "disabled": { 32 | Type: schema.TypeBool, 33 | Description: "Whether this step is disabled", 34 | Optional: true, 35 | Default: false, 36 | }, 37 | "required": { 38 | Type: schema.TypeBool, 39 | Description: "Whether this step is required and cannot be skipped", 40 | Optional: true, 41 | Default: false, 42 | }, 43 | "environments": { 44 | Description: "The environments that this step will run in", 45 | Type: schema.TypeList, 46 | Optional: true, 47 | Elem: &schema.Schema{ 48 | Type: schema.TypeString, 49 | }, 50 | }, 51 | "excluded_environments": { 52 | Description: "The environments that this step will be skipped in", 53 | Type: schema.TypeList, 54 | Optional: true, 55 | Elem: &schema.Schema{ 56 | Type: schema.TypeString, 57 | }, 58 | }, 59 | "channels": { 60 | Description: "The channels that this step applies to", 61 | Type: schema.TypeList, 62 | Optional: true, 63 | Elem: &schema.Schema{ 64 | Type: schema.TypeString, 65 | }, 66 | }, 67 | "tenant_tags": { 68 | Description: "The tags for the tenants that this step applies to", 69 | Type: schema.TypeList, 70 | Optional: true, 71 | Elem: &schema.Schema{ 72 | Type: schema.TypeString, 73 | }, 74 | }, 75 | "property": getPropertySchema(), 76 | }, 77 | } 78 | 79 | actionSchema := &schema.Schema{ 80 | Type: schema.TypeList, 81 | Optional: true, 82 | Elem: element, 83 | } 84 | 85 | return actionSchema, element 86 | } 87 | 88 | func addExecutionLocationSchema(element *schema.Resource) { 89 | element.Schema["run_on_server"] = &schema.Schema{ 90 | Type: schema.TypeBool, 91 | Description: "Whether this step runs on a worker or on the target", 92 | Optional: true, 93 | Default: false, 94 | } 95 | } 96 | 97 | func addWorkerPoolSchema(element *schema.Resource) { 98 | element.Schema["worker_pool_id"] = &schema.Schema{ 99 | Type: schema.TypeString, 100 | Description: "Which worker pool to run on", 101 | Optional: true, 102 | } 103 | } 104 | 105 | func buildDeploymentActionResource(tfAction map[string]interface{}) octopusdeploy.DeploymentAction { 106 | action := octopusdeploy.DeploymentAction{ 107 | Name: tfAction["name"].(string), 108 | IsDisabled: tfAction["disabled"].(bool), 109 | IsRequired: tfAction["required"].(bool), 110 | Environments: getSliceFromTerraformTypeList(tfAction["environments"]), 111 | ExcludedEnvironments: getSliceFromTerraformTypeList(tfAction["excluded_environments"]), 112 | Channels: getSliceFromTerraformTypeList(tfAction["channels"]), 113 | TenantTags: getSliceFromTerraformTypeList(tfAction["tenant_tags"]), 114 | Properties: map[string]string{}, 115 | } 116 | 117 | actionType := tfAction["action_type"] 118 | if actionType != nil { 119 | action.ActionType = actionType.(string) 120 | } 121 | 122 | // Even though not all actions have these properties, we'll keep them here. 123 | // They will just be ignored if the action doesn't have it 124 | runOnServer := tfAction["run_on_server"] 125 | if runOnServer != nil { 126 | action.Properties["Octopus.Action.RunOnServer"] = strconv.FormatBool(runOnServer.(bool)) 127 | } 128 | 129 | workerPoolID := tfAction["worker_pool_id"] 130 | if workerPoolID != nil { 131 | action.WorkerPoolId = workerPoolID.(string) 132 | } 133 | 134 | if primaryPackage, ok := tfAction["primary_package"]; ok { 135 | tfPrimaryPackage := primaryPackage.(*schema.Set).List() 136 | if len(tfPrimaryPackage) > 0 { 137 | primaryPackage := buildPackageReferenceResource(tfPrimaryPackage[0].(map[string]interface{})) 138 | action.Packages = append(action.Packages, primaryPackage) 139 | } 140 | } 141 | 142 | if tfPkgs, ok := tfAction["package"]; ok { 143 | for _, tfPkg := range tfPkgs.(*schema.Set).List() { 144 | pkg := buildPackageReferenceResource(tfPkg.(map[string]interface{})) 145 | action.Packages = append(action.Packages, pkg) 146 | } 147 | } 148 | 149 | if tfProps, ok := tfAction["property"]; ok { 150 | for _, tfProp := range tfProps.(*schema.Set).List() { 151 | tfPropi := tfProp.(map[string]interface{}) 152 | action.Properties[tfPropi["key"].(string)] = tfPropi["value"].(string) 153 | } 154 | } 155 | 156 | return action 157 | } 158 | -------------------------------------------------------------------------------- /octopusdeploy/deployment_process.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 6 | "github.com/hashicorp/terraform/helper/schema" 7 | "log" 8 | ) 9 | 10 | func resourceDeploymentProcess() *schema.Resource { 11 | return &schema.Resource{ 12 | Create: resourceDeploymentProcessCreate, 13 | Read: resourceDeploymentProcessRead, 14 | Update: resourceDeploymentProcessUpdate, 15 | Delete: resourceDeploymentProcessDelete, 16 | 17 | Schema: map[string]*schema.Schema{ 18 | "project_id": { 19 | Type: schema.TypeString, 20 | Required: true, 21 | }, 22 | "step": getDeploymentStepSchema(), 23 | }, 24 | } 25 | } 26 | 27 | func resourceDeploymentProcessCreate(d *schema.ResourceData, m interface{}) error { 28 | client := m.(*octopusdeploy.Client) 29 | 30 | newDeploymentProcess := buildDeploymentProcessResource(d) 31 | 32 | project, err := client.Project.Get(newDeploymentProcess.ProjectID) 33 | if err != nil { 34 | return fmt.Errorf("error getting project %s: %s", project.Name, err.Error()) 35 | } 36 | 37 | current, err := client.DeploymentProcess.Get(project.DeploymentProcessID) 38 | if err != nil { 39 | return fmt.Errorf("error getting deployment process for %s: %s", project.Name, err.Error()) 40 | } 41 | 42 | newDeploymentProcess.ID = current.ID 43 | newDeploymentProcess.Version = current.Version 44 | createdDeploymentProcess, err := client.DeploymentProcess.Update(newDeploymentProcess) 45 | 46 | if err != nil { 47 | return fmt.Errorf("error creating deployment process: %s", err.Error()) 48 | } 49 | 50 | d.SetId(createdDeploymentProcess.ID) 51 | 52 | return nil 53 | } 54 | 55 | func buildDeploymentProcessResource(d *schema.ResourceData) *octopusdeploy.DeploymentProcess { 56 | deploymentProcess := &octopusdeploy.DeploymentProcess{ 57 | ProjectID: d.Get("project_id").(string), 58 | } 59 | 60 | if attr, ok := d.GetOk("step"); ok { 61 | tfSteps := attr.([]interface{}) 62 | 63 | for _, tfStep := range tfSteps { 64 | step := buildDeploymentStepResource(tfStep.(map[string]interface{})) 65 | deploymentProcess.Steps = append(deploymentProcess.Steps, step) 66 | } 67 | } 68 | 69 | return deploymentProcess 70 | } 71 | 72 | func resourceDeploymentProcessRead(d *schema.ResourceData, m interface{}) error { 73 | client := m.(*octopusdeploy.Client) 74 | 75 | deploymentProcessID := d.Id() 76 | 77 | _, err := client.DeploymentProcess.Get(deploymentProcessID) 78 | 79 | if err == octopusdeploy.ErrItemNotFound { 80 | d.SetId("") 81 | return nil 82 | } 83 | 84 | if err != nil { 85 | return fmt.Errorf("error reading deployment process id %s: %s", deploymentProcessID, err.Error()) 86 | } 87 | 88 | log.Printf("[DEBUG] deploymentProcess: %v", m) 89 | 90 | return nil 91 | } 92 | 93 | func resourceDeploymentProcessUpdate(d *schema.ResourceData, m interface{}) error { 94 | deploymentProcess := buildDeploymentProcessResource(d) 95 | deploymentProcess.ID = d.Id() // set deploymentProcess struct ID so octopus knows which deploymentProcess to update 96 | 97 | client := m.(*octopusdeploy.Client) 98 | 99 | current, err := client.DeploymentProcess.Get(deploymentProcess.ID) 100 | if err != nil { 101 | return fmt.Errorf("error getting deployment process %s: %s", deploymentProcess.ID, err.Error()) 102 | } 103 | 104 | deploymentProcess.Version = current.Version 105 | deploymentProcess, err = client.DeploymentProcess.Update(deploymentProcess) 106 | 107 | if err != nil { 108 | return fmt.Errorf("error updating deployment process id %s: %s", d.Id(), err.Error()) 109 | } 110 | 111 | d.SetId(deploymentProcess.ID) 112 | 113 | return nil 114 | } 115 | 116 | func resourceDeploymentProcessDelete(d *schema.ResourceData, m interface{}) error { 117 | client := m.(*octopusdeploy.Client) 118 | current, err := client.DeploymentProcess.Get(d.Id()) 119 | 120 | if err != nil { 121 | return fmt.Errorf("error getting deployment process with id %s: %s", d.Id(), err.Error()) 122 | } 123 | 124 | deploymentProcess := &octopusdeploy.DeploymentProcess{ 125 | ID: d.Id(), 126 | Version: current.Version, 127 | } 128 | 129 | deploymentProcess, err = client.DeploymentProcess.Update(deploymentProcess) 130 | 131 | if err != nil { 132 | return fmt.Errorf("error deleting deployment process with id %s: %s", deploymentProcess.ID, err.Error()) 133 | } 134 | 135 | d.SetId("") 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /octopusdeploy/deployment_process_test.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 8 | "github.com/hashicorp/terraform/helper/resource" 9 | "github.com/hashicorp/terraform/terraform" 10 | ) 11 | 12 | func TestAccOctopusDeployDeploymentProcessBasic(t *testing.T) { 13 | resource.Test(t, resource.TestCase{ 14 | PreCheck: func() { testAccPreCheck(t) }, 15 | Providers: testAccProviders, 16 | CheckDestroy: testAccCheckOctopusDeployDeploymentProcessDestroy, 17 | Steps: []resource.TestStep{ 18 | { 19 | Config: testAccDeploymentProcessBasic(), 20 | Check: resource.ComposeTestCheckFunc( 21 | testAccCheckOctopusDeployDeploymentProcess(), 22 | ), 23 | }, 24 | }, 25 | }) 26 | } 27 | 28 | func testAccDeploymentProcessBasic() string { 29 | return ` 30 | resource "octopusdeploy_lifecycle" "test" { 31 | name = "Test Lifecycle" 32 | } 33 | 34 | resource "octopusdeploy_project_group" "test" { 35 | name = "Test Group" 36 | } 37 | 38 | resource "octopusdeploy_project" "test" { 39 | name = "Test Project" 40 | lifecycle_id = "${octopusdeploy_lifecycle.test.id}" 41 | project_group_id = "${octopusdeploy_project_group.test.id}" 42 | } 43 | 44 | resource "octopusdeploy_deployment_process" "test" { 45 | project_id = "${octopusdeploy_project.test.id}" 46 | 47 | step { 48 | name = "Test" 49 | target_roles = ["A", "B"] 50 | package_requirement = "AfterPackageAcquisition" 51 | condition = "Variable" 52 | condition_expression = "#{run}" 53 | start_trigger = "StartWithPrevious" 54 | window_size = "5" 55 | 56 | 57 | action { 58 | name = "Test" 59 | action_type = "Octopus.Script" 60 | disabled = false 61 | required = true 62 | worker_pool_id = "WorkerPools-1" 63 | environments = ["Environments-1"] 64 | //excluded_environments = ["Environments-2"] 65 | //channels = ["Channels-1"] 66 | //tenant_tags = ["tag/tag"] 67 | 68 | primary_package { 69 | package_id = "MyPackage" 70 | feed_id = "feeds-builtin" 71 | acquisition_location = "ExecutionTarget" 72 | } 73 | 74 | package { 75 | name = "ThePackage" 76 | package_id = "MyPackage" 77 | feed_id = "feeds-builtin" 78 | acquisition_location = "NotAcquired" 79 | extract_during_deployment = true 80 | 81 | property { 82 | key = "WhatIsThis" 83 | value = "Dunno" 84 | } 85 | 86 | } 87 | 88 | property { 89 | key = "Octopus.Action.Script.ScriptFileName" 90 | value = "Run.ps132" 91 | } 92 | 93 | property { 94 | key = "Octopus.Action.Script.ScriptSource" 95 | value = "Package" 96 | } 97 | 98 | } 99 | } 100 | 101 | step { 102 | name = "Step2" 103 | start_trigger = "StartWithPrevious" 104 | 105 | action { 106 | name = "Step2" 107 | action_type = "Octopus.Script" 108 | run_on_server = true 109 | 110 | property { 111 | key = "Octopus.Action.Script.ScriptBody" 112 | value = "Write-Host 'hi'" 113 | } 114 | } 115 | } 116 | } 117 | ` 118 | } 119 | 120 | func testAccBuildTestActionTerraform(action string) string { 121 | return fmt.Sprintf(` 122 | resource "octopusdeploy_lifecycle" "test" { 123 | name = "Test Lifecycle" 124 | } 125 | 126 | resource "octopusdeploy_project_group" "test" { 127 | name = "Test Group" 128 | } 129 | 130 | resource "octopusdeploy_project" "test" { 131 | name = "Test Project" 132 | lifecycle_id = "${octopusdeploy_lifecycle.test.id}" 133 | project_group_id = "${octopusdeploy_project_group.test.id}" 134 | } 135 | 136 | resource "octopusdeploy_deployment_process" "test" { 137 | project_id = "${octopusdeploy_project.test.id}" 138 | 139 | step { 140 | name = "Test" 141 | target_roles = ["WebServer"] 142 | 143 | %s 144 | } 145 | } 146 | `, action) 147 | } 148 | 149 | func testAccCheckOctopusDeployDeploymentProcessDestroy(s *terraform.State) error { 150 | client := testAccProvider.Meta().(*octopusdeploy.Client) 151 | 152 | if err := destroyProjectHelper(s, client); err != nil { 153 | return err 154 | } 155 | if err := destroyHelperProjectGroup(s, client); err != nil { 156 | return err 157 | } 158 | if err := destroyHelperLifecycle(s, client); err != nil { 159 | return err 160 | } 161 | return nil 162 | } 163 | 164 | func testAccCheckOctopusDeployDeploymentProcess() resource.TestCheckFunc { 165 | return func(s *terraform.State) error { 166 | client := testAccProvider.Meta().(*octopusdeploy.Client) 167 | 168 | process, err := getDeploymentProcess(s, client) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | expectedNumberOfSteps := 2 174 | numberOfSteps := len(process.Steps) 175 | if numberOfSteps != expectedNumberOfSteps { 176 | return fmt.Errorf("Deployment process has %d steps instead of the expected %d", numberOfSteps, expectedNumberOfSteps) 177 | } 178 | 179 | if process.Steps[0].Actions[0].Properties["Octopus.Action.RunOnServer"] != "true" { 180 | return fmt.Errorf("The RunOnServer property has not been set to true on the deployment process") 181 | } 182 | 183 | return nil 184 | } 185 | } 186 | 187 | func getDeploymentProcess(s *terraform.State, client *octopusdeploy.Client) (*octopusdeploy.DeploymentProcess, error) { 188 | for _, r := range s.RootModule().Resources { 189 | if r.Type == "octopusdeploy_deployment_process" { 190 | return client.DeploymentProcess.Get(r.Primary.ID) 191 | } 192 | } 193 | return nil, fmt.Errorf("No deployment process found in the terraform resources") 194 | } 195 | -------------------------------------------------------------------------------- /octopusdeploy/deployment_step.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 5 | "github.com/hashicorp/terraform/helper/schema" 6 | "strings" 7 | ) 8 | 9 | func getDeploymentStepSchema() *schema.Schema { 10 | return &schema.Schema{ 11 | Type: schema.TypeList, 12 | Optional: true, 13 | Elem: &schema.Resource{ 14 | Schema: map[string]*schema.Schema{ 15 | "name": { 16 | Type: schema.TypeString, 17 | Description: "The name of the step", 18 | Required: true, 19 | }, 20 | "target_roles": { 21 | Description: "The roles that this step run against, or runs on behalf of", 22 | Type: schema.TypeList, 23 | Optional: true, 24 | Elem: &schema.Schema{ 25 | Type: schema.TypeString, 26 | }, 27 | }, 28 | "package_requirement": { 29 | Type: schema.TypeString, 30 | Description: "Whether to run this step before or after package acquisition (if possible)", 31 | Optional: true, 32 | Default: (string)(octopusdeploy.DeploymentStepPackageRequirement_LetOctopusDecide), 33 | ValidateFunc: validateValueFunc([]string{ 34 | (string)(octopusdeploy.DeploymentStepPackageRequirement_LetOctopusDecide), 35 | (string)(octopusdeploy.DeploymentStepPackageRequirement_BeforePackageAcquisition), 36 | (string)(octopusdeploy.DeploymentStepPackageRequirement_AfterPackageAcquisition), 37 | }), 38 | }, 39 | "condition": { 40 | Type: schema.TypeString, 41 | Description: "When to run the step, one of 'Success', 'Failure', 'Always' or 'Variable'", 42 | Optional: true, 43 | Default: (string)(octopusdeploy.DeploymentStepCondition_Success), 44 | ValidateFunc: validateValueFunc([]string{ 45 | (string)(octopusdeploy.DeploymentStepCondition_Success), 46 | (string)(octopusdeploy.DeploymentStepCondition_Failure), 47 | (string)(octopusdeploy.DeploymentStepCondition_Always), 48 | (string)(octopusdeploy.DeploymentStepCondition_Variable), 49 | }), 50 | }, 51 | "condition_expression": { 52 | Type: schema.TypeString, 53 | Description: "The expression to evaluate to determine whether to run this step when 'condition' is 'Variable'", 54 | Optional: true, 55 | }, 56 | "start_trigger": { 57 | Type: schema.TypeString, 58 | Description: "Whether to run this step after the previous step ('StartAfterPrevious') or at the same time as the previous step ('StartWithPrevious')", 59 | Optional: true, 60 | Default: (string)(octopusdeploy.DeploymentStepStartTrigger_StartAfterPrevious), 61 | ValidateFunc: validateValueFunc([]string{ 62 | (string)(octopusdeploy.DeploymentStepStartTrigger_StartAfterPrevious), 63 | (string)(octopusdeploy.DeploymentStepStartTrigger_StartWithPrevious), 64 | }), 65 | }, 66 | "window_size": { 67 | Type: schema.TypeString, 68 | Description: "The maximum number of targets to deploy to simultaneously", 69 | Optional: true, 70 | }, 71 | "action": getDeploymentActionSchema(), 72 | "manual_intervention_action": getManualInterventionActionSchema(), 73 | "deploy_package_action": getDeployPackageAction(), 74 | "deploy_windows_service_action": getDeployWindowsServiceActionSchema(), 75 | }, 76 | }, 77 | } 78 | } 79 | 80 | func buildDeploymentStepResource(tfStep map[string]interface{}) octopusdeploy.DeploymentStep { 81 | step := octopusdeploy.DeploymentStep{ 82 | Name: tfStep["name"].(string), 83 | PackageRequirement: octopusdeploy.DeploymentStepPackageRequirement(tfStep["package_requirement"].(string)), 84 | Condition: octopusdeploy.DeploymentStepCondition(tfStep["condition"].(string)), 85 | StartTrigger: octopusdeploy.DeploymentStepStartTrigger(tfStep["start_trigger"].(string)), 86 | Properties: map[string]string{}, 87 | } 88 | 89 | targetRoles := tfStep["target_roles"] 90 | if targetRoles != nil { 91 | step.Properties["Octopus.Action.TargetRoles"] = strings.Join(getSliceFromTerraformTypeList(targetRoles), ",") 92 | } 93 | 94 | conditionExpression := tfStep["condition_expression"] 95 | if conditionExpression != nil { 96 | step.Properties["Octopus.Action.ConditionVariableExpression"] = conditionExpression.(string) 97 | } 98 | 99 | windowSize := tfStep["window_size"] 100 | if windowSize != nil { 101 | step.Properties["Octopus.Action.MaxParallelism"] = windowSize.(string) 102 | } 103 | 104 | if attr, ok := tfStep["action"]; ok { 105 | for _, tfAction := range attr.([]interface{}) { 106 | action := buildDeploymentActionResource(tfAction.(map[string]interface{})) 107 | step.Actions = append(step.Actions, action) 108 | } 109 | } 110 | 111 | if attr, ok := tfStep["manual_intervention_action"]; ok { 112 | for _, tfAction := range attr.([]interface{}) { 113 | action := buildManualInterventionActionResource(tfAction.(map[string]interface{})) 114 | step.Actions = append(step.Actions, action) 115 | } 116 | } 117 | 118 | if attr, ok := tfStep["deploy_package_action"]; ok { 119 | for _, tfAction := range attr.([]interface{}) { 120 | action := buildDeployPackageActionResource(tfAction.(map[string]interface{})) 121 | step.Actions = append(step.Actions, action) 122 | } 123 | } 124 | 125 | if attr, ok := tfStep["deploy_windows_service_action"]; ok { 126 | for _, tfAction := range attr.([]interface{}) { 127 | action := buildDeployWindowsServiceActionResource(tfAction.(map[string]interface{})) 128 | step.Actions = append(step.Actions, action) 129 | } 130 | } 131 | 132 | return step 133 | } 134 | -------------------------------------------------------------------------------- /octopusdeploy/manual_intervention_action.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 5 | "github.com/hashicorp/terraform/helper/schema" 6 | ) 7 | 8 | func getManualInterventionActionSchema() *schema.Schema { 9 | actionSchema, element := getCommonDeploymentActionSchema() 10 | 11 | element.Schema["instructions"] = &schema.Schema{ 12 | Type: schema.TypeString, 13 | Description: "The instructions for the user to follow", 14 | Required: true, 15 | } 16 | 17 | element.Schema["responsible_teams"] = &schema.Schema{ 18 | Type: schema.TypeString, 19 | Description: "The teams responsible to resolve this step. If no teams are specified, all users who have permission to deploy the project can resolve it.", 20 | Optional: true, 21 | } 22 | 23 | return actionSchema 24 | } 25 | 26 | func buildManualInterventionActionResource(tfAction map[string]interface{}) octopusdeploy.DeploymentAction { 27 | resource := buildDeploymentActionResource(tfAction) 28 | resource.ActionType = "Octopus.Manual" 29 | resource.Properties["Octopus.Action.Manual.Instructions"] = tfAction["instructions"].(string) 30 | 31 | responsibleTeams := tfAction["responsible_teams"] 32 | if responsibleTeams != nil { 33 | resource.Properties["Octopus.Action.Manual.ResponsibleTeamIds"] = responsibleTeams.(string) 34 | } 35 | 36 | return resource 37 | } 38 | -------------------------------------------------------------------------------- /octopusdeploy/manual_intervention_action_test.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 8 | "github.com/hashicorp/terraform/helper/resource" 9 | "github.com/hashicorp/terraform/terraform" 10 | ) 11 | 12 | func TestAccOctopusDeployManualInterventionAction(t *testing.T) { 13 | resource.Test(t, resource.TestCase{ 14 | PreCheck: func() { testAccPreCheck(t) }, 15 | Providers: testAccProviders, 16 | CheckDestroy: testAccCheckOctopusDeployDeploymentProcessDestroy, 17 | Steps: []resource.TestStep{ 18 | { 19 | Config: testAccManualInterventionAction(), 20 | Check: resource.ComposeTestCheckFunc( 21 | testAccCheckManualInterventionAction(), 22 | ), 23 | }, 24 | }, 25 | }) 26 | } 27 | 28 | func testAccManualInterventionAction() string { 29 | return testAccBuildTestActionTerraform(` 30 | manual_intervention_action { 31 | name = "Test" 32 | instructions = "Approve Me" 33 | responsible_teams = "A Team" 34 | } 35 | `) 36 | } 37 | 38 | func testAccCheckManualInterventionAction() resource.TestCheckFunc { 39 | return func(s *terraform.State) error { 40 | client := testAccProvider.Meta().(*octopusdeploy.Client) 41 | 42 | process, err := getDeploymentProcess(s, client) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | action := process.Steps[0].Actions[0] 48 | 49 | if action.ActionType != "Octopus.Manual" { 50 | return fmt.Errorf("Action type is incorrect: %s", action.ActionType) 51 | } 52 | 53 | if action.Properties["Octopus.Action.Manual.Instructions"] != "Approve Me" { 54 | return fmt.Errorf("Instructions is incorrect: %s", action.Properties["Octopus.Action.Manual.Instructions"]) 55 | } 56 | 57 | if action.Properties["Octopus.Action.Manual.ResponsibleTeamIds"] != "A Team" { 58 | return fmt.Errorf("ResponsibleTeamIds is incorrect: %s", action.Properties["Octopus.Action.Manual.ResponsibleTeamIds"]) 59 | } 60 | 61 | return nil 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /octopusdeploy/package_reference.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 5 | "github.com/hashicorp/terraform/helper/schema" 6 | ) 7 | 8 | func addPrimaryPackageSchema(element *schema.Resource, required bool) { 9 | element.Schema["primary_package"] = getPackageSchema(required) 10 | } 11 | 12 | func addPackagesSchema(element *schema.Resource, primaryIsRequired bool) { 13 | addPrimaryPackageSchema(element, primaryIsRequired) 14 | 15 | element.Schema["package"] = getPackageSchema(false) 16 | 17 | packageElementSchema := element.Schema["package"].Elem.(*schema.Resource).Schema 18 | packageElementSchema["name"] = &schema.Schema{ 19 | Type: schema.TypeString, 20 | Description: "The name of the package", 21 | Required: true, 22 | } 23 | packageElementSchema["extract_during_deployment"] = &schema.Schema{ 24 | Type: schema.TypeString, 25 | Description: "Whether to extract the package during deployment", 26 | Optional: true, 27 | Default: "true", 28 | } 29 | } 30 | 31 | func getPackageSchema(required bool) *schema.Schema { 32 | return &schema.Schema{ 33 | Description: "The primary package for the action", 34 | Type: schema.TypeSet, 35 | Required: required, 36 | Optional: !required, 37 | MaxItems: 1, 38 | Elem: &schema.Resource{ 39 | Schema: map[string]*schema.Schema{ 40 | "package_id": { 41 | Type: schema.TypeString, 42 | Description: "The ID of the package", 43 | Required: true, 44 | }, 45 | "feed_id": { 46 | Type: schema.TypeString, 47 | Description: "The feed to retrieve the package from", 48 | Default: "feeds-builtin", 49 | Optional: true, 50 | }, 51 | "acquisition_location": { 52 | Type: schema.TypeString, 53 | Description: "Whether to acquire this package on the server ('Server'), target ('ExecutionTarget') or not at all ('NotAcquired'). Can be an expression", 54 | Default: (string)(octopusdeploy.PackageAcquisitionLocation_Server), 55 | Optional: true, 56 | }, 57 | "property": getPropertySchema(), 58 | }, 59 | }, 60 | } 61 | } 62 | 63 | func buildPackageReferenceResource(tfPkg map[string]interface{}) octopusdeploy.PackageReference { 64 | pkg := octopusdeploy.PackageReference{ 65 | Name: getStringOrEmpty(tfPkg["name"]), 66 | PackageId: tfPkg["package_id"].(string), 67 | FeedId: tfPkg["feed_id"].(string), 68 | AcquisitionLocation: tfPkg["acquisition_location"].(string), 69 | Properties: buildPropertiesMap(tfPkg["property"]), 70 | } 71 | 72 | extract := tfPkg["extract_during_deployment"] 73 | if extract != nil { 74 | pkg.Properties["Extract"] = extract.(string) 75 | } 76 | 77 | return pkg 78 | } 79 | -------------------------------------------------------------------------------- /octopusdeploy/property.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import "github.com/hashicorp/terraform/helper/schema" 4 | 5 | func getPropertySchema() *schema.Schema { 6 | return &schema.Schema{ 7 | Type: schema.TypeSet, 8 | Optional: true, 9 | Elem: &schema.Resource{ 10 | Schema: map[string]*schema.Schema{ 11 | "key": { 12 | Type: schema.TypeString, 13 | Description: "The name of the action", 14 | Required: true, 15 | }, 16 | "value": { 17 | Type: schema.TypeString, 18 | Description: "The type of action", 19 | Required: true, 20 | }, 21 | }, 22 | }, 23 | } 24 | } 25 | 26 | func buildPropertiesMap(tfProperties interface{}) map[string]string { 27 | properties := map[string]string{} 28 | if tfProperties != nil { 29 | for _, tfProp := range tfProperties.(*schema.Set).List() { 30 | m := tfProp.(map[string]interface{}) 31 | properties[m["key"].(string)] = m["value"].(string) 32 | } 33 | } 34 | return properties 35 | } 36 | -------------------------------------------------------------------------------- /octopusdeploy/provider.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/hashicorp/terraform/helper/schema" 7 | "github.com/hashicorp/terraform/terraform" 8 | ) 9 | 10 | //Provider is the plugin entry point 11 | func Provider() terraform.ResourceProvider { 12 | return &schema.Provider{ 13 | DataSourcesMap: map[string]*schema.Resource{ 14 | "octopusdeploy_project": dataProject(), 15 | "octopusdeploy_environment": dataEnvironment(), 16 | "octopusdeploy_variable": dataVariable(), 17 | "octopusdeploy_machinepolicy": dataMachinePolicy(), 18 | "octopusdeploy_machine": dataMachine(), 19 | "octopusdeploy_library_variable_set": dataLibraryVariableSet(), 20 | "octopusdeploy_lifecycle": dataLifecycle(), 21 | }, 22 | ResourcesMap: map[string]*schema.Resource{ 23 | "octopusdeploy_project": resourceProject(), 24 | "octopusdeploy_project_group": resourceProjectGroup(), 25 | "octopusdeploy_project_deployment_target_trigger": resourceProjectDeploymentTargetTrigger(), 26 | "octopusdeploy_environment": resourceEnvironment(), 27 | "octopusdeploy_variable": resourceVariable(), 28 | "octopusdeploy_machine": resourceMachine(), 29 | "octopusdeploy_library_variable_set": resourceLibraryVariableSet(), 30 | "octopusdeploy_lifecycle": resourceLifecycle(), 31 | "octopusdeploy_deployment_process": resourceDeploymentProcess(), 32 | }, 33 | Schema: map[string]*schema.Schema{ 34 | "address": { 35 | Type: schema.TypeString, 36 | Required: true, 37 | DefaultFunc: schema.EnvDefaultFunc("OCTOPUS_URL", nil), 38 | Description: "The URL of the Octopus Deploy server", 39 | }, 40 | "apikey": { 41 | Type: schema.TypeString, 42 | Required: true, 43 | DefaultFunc: schema.EnvDefaultFunc("OCTOPUS_APIKEY", nil), 44 | Description: "The API to use with the Octopus Deploy server.", 45 | }, 46 | }, 47 | 48 | ConfigureFunc: providerConfigure, 49 | } 50 | } 51 | 52 | func providerConfigure(d *schema.ResourceData) (interface{}, error) { 53 | config := Config{ 54 | Address: d.Get("address").(string), 55 | APIKey: d.Get("apikey").(string), 56 | } 57 | 58 | log.Println("[INFO] Initializing Octopus Deploy client") 59 | client := config.Client() 60 | 61 | return client, nil 62 | } 63 | -------------------------------------------------------------------------------- /octopusdeploy/provider_test.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform/helper/schema" 8 | "github.com/hashicorp/terraform/terraform" 9 | ) 10 | 11 | var testAccProviders map[string]terraform.ResourceProvider 12 | var testAccProvider *schema.Provider 13 | 14 | func init() { 15 | testAccProvider = Provider().(*schema.Provider) 16 | testAccProviders = map[string]terraform.ResourceProvider{ 17 | "octopusdeploy": testAccProvider, 18 | } 19 | } 20 | 21 | func TestProvider(t *testing.T) { 22 | if err := Provider().(*schema.Provider).InternalValidate(); err != nil { 23 | t.Fatalf("err: %s", err) 24 | } 25 | } 26 | 27 | func TestProvider_impl(t *testing.T) { 28 | var _ terraform.ResourceProvider = Provider() 29 | } 30 | 31 | func testAccPreCheck(t *testing.T) { 32 | if v := os.Getenv("OCTOPUS_URL"); v == "" { 33 | t.Fatal("OCTOPUS_URL must be set for acceptance tests") 34 | } 35 | if v := os.Getenv("OCTOPUS_APIKEY"); v == "" { 36 | t.Fatal("OCTOPUS_APIKEY must be set for acceptance tests") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /octopusdeploy/resource_environment.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 7 | "github.com/hashicorp/terraform/helper/schema" 8 | ) 9 | 10 | func resourceEnvironment() *schema.Resource { 11 | return &schema.Resource{ 12 | Create: resourceEnvironmentCreate, 13 | Read: resourceEnvironmentRead, 14 | Update: resourceEnvironmentUpdate, 15 | Delete: resourceEnvironmentDelete, 16 | Importer: &schema.ResourceImporter{ 17 | State: schema.ImportStatePassthrough, 18 | }, 19 | 20 | Schema: map[string]*schema.Schema{ 21 | "name": { 22 | Type: schema.TypeString, 23 | Required: true, 24 | }, 25 | "description": { 26 | Type: schema.TypeString, 27 | Optional: true, 28 | }, 29 | "use_guided_failure": { 30 | Type: schema.TypeBool, 31 | Optional: true, 32 | Default: false, 33 | }, 34 | }, 35 | } 36 | } 37 | 38 | func resourceEnvironmentRead(d *schema.ResourceData, m interface{}) error { 39 | client := m.(*octopusdeploy.Client) 40 | 41 | environmentID := d.Id() 42 | env, err := client.Environment.Get(environmentID) 43 | 44 | if err == octopusdeploy.ErrItemNotFound { 45 | d.SetId("") 46 | return nil 47 | } 48 | 49 | if err != nil { 50 | return fmt.Errorf("error reading environment %s: %s", environmentID, err.Error()) 51 | } 52 | 53 | d.Set("name", env.Name) 54 | d.Set("description", env.Description) 55 | d.Set("use_guided_failure", env.UseGuidedFailure) 56 | 57 | return nil 58 | } 59 | 60 | func buildEnvironmentResource(d *schema.ResourceData) *octopusdeploy.Environment { 61 | envName := d.Get("name").(string) 62 | 63 | var envDesc string 64 | var envGuided bool 65 | 66 | envDescInterface, ok := d.GetOk("description") 67 | if ok { 68 | envDesc = envDescInterface.(string) 69 | } 70 | 71 | envGuidedInterface, ok := d.GetOk("use_guided_failure") 72 | if ok { 73 | envGuided = envGuidedInterface.(bool) 74 | } 75 | 76 | return octopusdeploy.NewEnvironment(envName, envDesc, envGuided) 77 | } 78 | 79 | func resourceEnvironmentCreate(d *schema.ResourceData, m interface{}) error { 80 | client := m.(*octopusdeploy.Client) 81 | 82 | newEnvironment := buildEnvironmentResource(d) 83 | env, err := client.Environment.Add(newEnvironment) 84 | 85 | if err != nil { 86 | return fmt.Errorf("error creating environment %s: %s", newEnvironment.Name, err.Error()) 87 | } 88 | 89 | d.SetId(env.ID) 90 | 91 | return nil 92 | } 93 | 94 | func resourceEnvironmentUpdate(d *schema.ResourceData, m interface{}) error { 95 | env := buildEnvironmentResource(d) 96 | env.ID = d.Id() // set project struct ID so octopus knows which project to update 97 | 98 | client := m.(*octopusdeploy.Client) 99 | 100 | updatedEnv, err := client.Environment.Update(env) 101 | 102 | if err != nil { 103 | return fmt.Errorf("error updating environment id %s: %s", d.Id(), err.Error()) 104 | } 105 | 106 | d.SetId(updatedEnv.ID) 107 | return nil 108 | } 109 | 110 | func resourceEnvironmentDelete(d *schema.ResourceData, m interface{}) error { 111 | client := m.(*octopusdeploy.Client) 112 | 113 | environmentID := d.Id() 114 | 115 | err := client.Environment.Delete(environmentID) 116 | 117 | if err != nil { 118 | return fmt.Errorf("error deleting environment id %s: %s", environmentID, err.Error()) 119 | } 120 | 121 | d.SetId("") 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /octopusdeploy/resource_environment_test.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 8 | "github.com/hashicorp/terraform/helper/resource" 9 | "github.com/hashicorp/terraform/terraform" 10 | ) 11 | 12 | func TestAccOctopusDeployEnvironmentBasic(t *testing.T) { 13 | const envPrefix = "octopusdeploy_environment.foo" 14 | const envName = "Testing one two three" 15 | const envDesc = "Terraform testing module environment" 16 | const envGuided = "false" 17 | resource.Test(t, resource.TestCase{ 18 | PreCheck: func() { testAccPreCheck(t) }, 19 | Providers: testAccProviders, 20 | CheckDestroy: testOctopusDeployEnvironmentDestroy, 21 | Steps: []resource.TestStep{ 22 | { 23 | Config: testEnvironmenttBasic(envName, envDesc, envGuided), 24 | Check: resource.ComposeTestCheckFunc( 25 | testOctopusDeployEnvironmentExists(envPrefix), 26 | resource.TestCheckResourceAttr( 27 | envPrefix, "name", envName), 28 | resource.TestCheckResourceAttr( 29 | envPrefix, "description", envDesc), 30 | resource.TestCheckResourceAttr( 31 | envPrefix, "use_guided_failure", envGuided), 32 | ), 33 | }, 34 | }, 35 | }) 36 | } 37 | 38 | func testEnvironmenttBasic(name, description, useguided string) string { 39 | return fmt.Sprintf(` 40 | resource "octopusdeploy_environment" "foo" { 41 | name = "%s" 42 | description = "%s" 43 | use_guided_failure = "%s" 44 | } 45 | `, 46 | name, description, useguided, 47 | ) 48 | } 49 | 50 | func testOctopusDeployEnvironmentExists(n string) resource.TestCheckFunc { 51 | return func(s *terraform.State) error { 52 | client := testAccProvider.Meta().(*octopusdeploy.Client) 53 | return existsEnvHelper(s, client) 54 | } 55 | } 56 | 57 | func existsEnvHelper(s *terraform.State, client *octopusdeploy.Client) error { 58 | for _, r := range s.RootModule().Resources { 59 | if _, err := client.Environment.Get(r.Primary.ID); err != nil { 60 | return fmt.Errorf("Received an error retrieving environment %s", err) 61 | } 62 | } 63 | return nil 64 | } 65 | 66 | func testOctopusDeployEnvironmentDestroy(s *terraform.State) error { 67 | client := testAccProvider.Meta().(*octopusdeploy.Client) 68 | return destroyEnvHelper(s, client) 69 | } 70 | 71 | func destroyEnvHelper(s *terraform.State, client *octopusdeploy.Client) error { 72 | for _, r := range s.RootModule().Resources { 73 | if _, err := client.Environment.Get(r.Primary.ID); err != nil { 74 | if err == octopusdeploy.ErrItemNotFound { 75 | continue 76 | } 77 | return fmt.Errorf("Received an error retrieving environment %s", err) 78 | } 79 | return fmt.Errorf("Environment still exists") 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /octopusdeploy/resource_library_variable_set.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 6 | "github.com/hashicorp/terraform/helper/schema" 7 | "log" 8 | ) 9 | 10 | func resourceLibraryVariableSet() *schema.Resource { 11 | return &schema.Resource{ 12 | Create: resourceLibraryVariableSetCreate, 13 | Read: resourceLibraryVariableSetRead, 14 | Update: resourceLibraryVariableSetUpdate, 15 | Delete: resourceLibraryVariableSetDelete, 16 | 17 | Schema: map[string]*schema.Schema{ 18 | "name": { 19 | Type: schema.TypeString, 20 | Required: true, 21 | }, 22 | "description": { 23 | Type: schema.TypeString, 24 | Optional: true, 25 | }, 26 | }, 27 | } 28 | } 29 | 30 | func resourceLibraryVariableSetCreate(d *schema.ResourceData, m interface{}) error { 31 | client := m.(*octopusdeploy.Client) 32 | 33 | newLibraryVariableSet := buildLibraryVariableSetResource(d) 34 | 35 | createdLibraryVariableSet, err := client.LibraryVariableSet.Add(newLibraryVariableSet) 36 | 37 | if err != nil { 38 | return fmt.Errorf("error creating project: %s", err.Error()) 39 | } 40 | 41 | d.SetId(createdLibraryVariableSet.ID) 42 | 43 | return nil 44 | } 45 | 46 | func buildLibraryVariableSetResource(d *schema.ResourceData) *octopusdeploy.LibraryVariableSet { 47 | name := d.Get("name").(string) 48 | 49 | libraryVariableSet := octopusdeploy.NewLibraryVariableSet(name) 50 | 51 | if attr, ok := d.GetOk("description"); ok { 52 | libraryVariableSet.Description = attr.(string) 53 | } 54 | 55 | return libraryVariableSet 56 | } 57 | 58 | func resourceLibraryVariableSetRead(d *schema.ResourceData, m interface{}) error { 59 | client := m.(*octopusdeploy.Client) 60 | 61 | libraryVariableSetID := d.Id() 62 | 63 | libraryVariableSet, err := client.LibraryVariableSet.Get(libraryVariableSetID) 64 | 65 | if err == octopusdeploy.ErrItemNotFound { 66 | d.SetId("") 67 | return nil 68 | } 69 | 70 | if err != nil { 71 | return fmt.Errorf("error reading libraryVariableSet id %s: %s", libraryVariableSetID, err.Error()) 72 | } 73 | 74 | log.Printf("[DEBUG] libraryVariableSet: %v", m) 75 | d.Set("name", libraryVariableSet.Name) 76 | d.Set("description", libraryVariableSet.Description) 77 | d.Set("variable_set_id", libraryVariableSet.VariableSetId) 78 | 79 | return nil 80 | } 81 | 82 | func resourceLibraryVariableSetUpdate(d *schema.ResourceData, m interface{}) error { 83 | libraryVariableSet := buildLibraryVariableSetResource(d) 84 | libraryVariableSet.ID = d.Id() // set libraryVariableSet struct ID so octopus knows which libraryVariableSet to update 85 | 86 | client := m.(*octopusdeploy.Client) 87 | 88 | libraryVariableSet, err := client.LibraryVariableSet.Update(libraryVariableSet) 89 | 90 | if err != nil { 91 | return fmt.Errorf("error updating libraryVariableSet id %s: %s", d.Id(), err.Error()) 92 | } 93 | 94 | d.SetId(libraryVariableSet.ID) 95 | 96 | return nil 97 | } 98 | 99 | func resourceLibraryVariableSetDelete(d *schema.ResourceData, m interface{}) error { 100 | client := m.(*octopusdeploy.Client) 101 | 102 | libraryVariableSetID := d.Id() 103 | 104 | err := client.LibraryVariableSet.Delete(libraryVariableSetID) 105 | 106 | if err != nil { 107 | return fmt.Errorf("error deleting libraryVariableSet id %s: %s", libraryVariableSetID, err.Error()) 108 | } 109 | 110 | d.SetId("") 111 | return nil 112 | } 113 | -------------------------------------------------------------------------------- /octopusdeploy/resource_library_variable_set_test.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 8 | "github.com/hashicorp/terraform/helper/resource" 9 | "github.com/hashicorp/terraform/terraform" 10 | ) 11 | 12 | func TestAccOctopusDeployLibraryVariableSetBasic(t *testing.T) { 13 | const terraformNamePrefix = "octopusdeploy_library_variable_set.foo" 14 | const libraryVariableSetName = "Funky Set" 15 | resource.Test(t, resource.TestCase{ 16 | PreCheck: func() { testAccPreCheck(t) }, 17 | Providers: testAccProviders, 18 | CheckDestroy: testAccCheckOctopusDeployLibraryVariableSetDestroy, 19 | Steps: []resource.TestStep{ 20 | { 21 | Config: testAccLibraryVariableSetBasic(libraryVariableSetName), 22 | Check: resource.ComposeTestCheckFunc( 23 | testAccCheckOctopusDeployLibraryVariableSetExists(terraformNamePrefix), 24 | resource.TestCheckResourceAttr( 25 | terraformNamePrefix, "name", libraryVariableSetName), 26 | ), 27 | }, 28 | }, 29 | }) 30 | } 31 | 32 | func TestAccOctopusDeployLibraryVariableSetWithUpdate(t *testing.T) { 33 | const terraformNamePrefix = "octopusdeploy_library_variable_set.foo" 34 | const libraryVariableSetName = "Funky Set" 35 | const description = "I am a new set description" 36 | resource.Test(t, resource.TestCase{ 37 | PreCheck: func() { testAccPreCheck(t) }, 38 | Providers: testAccProviders, 39 | CheckDestroy: testAccCheckOctopusDeployLibraryVariableSetDestroy, 40 | Steps: []resource.TestStep{ 41 | // create variable set with no description 42 | { 43 | Config: testAccLibraryVariableSetBasic(libraryVariableSetName), 44 | Check: resource.ComposeTestCheckFunc( 45 | testAccCheckOctopusDeployLibraryVariableSetExists(terraformNamePrefix), 46 | resource.TestCheckResourceAttr( 47 | terraformNamePrefix, "name", libraryVariableSetName), 48 | ), 49 | }, 50 | // create update it with a description 51 | { 52 | Config: testAccLibraryVariableSetWithDescription(libraryVariableSetName, description), 53 | Check: resource.ComposeTestCheckFunc( 54 | testAccCheckOctopusDeployLibraryVariableSetExists(terraformNamePrefix), 55 | resource.TestCheckResourceAttr( 56 | terraformNamePrefix, "name", libraryVariableSetName), 57 | resource.TestCheckResourceAttr( 58 | terraformNamePrefix, "description", description), 59 | ), 60 | }, 61 | // update again by remove its description 62 | { 63 | Config: testAccLibraryVariableSetBasic(libraryVariableSetName), 64 | Check: resource.ComposeTestCheckFunc( 65 | testAccCheckOctopusDeployLibraryVariableSetExists(terraformNamePrefix), 66 | resource.TestCheckResourceAttr( 67 | terraformNamePrefix, "name", libraryVariableSetName), 68 | resource.TestCheckResourceAttr( 69 | terraformNamePrefix, "description", ""), 70 | ), 71 | }, 72 | }, 73 | }) 74 | } 75 | 76 | func testAccLibraryVariableSetBasic(name string) string { 77 | return fmt.Sprintf(` 78 | resource "octopusdeploy_library_variable_set" "foo" { 79 | name = "%s" 80 | } 81 | `, 82 | name, 83 | ) 84 | } 85 | func testAccLibraryVariableSetWithDescription(name, description string) string { 86 | return fmt.Sprintf(` 87 | resource "octopusdeploy_library_variable_set" "foo" { 88 | name = "%s" 89 | description = "%s" 90 | } 91 | `, 92 | name, description, 93 | ) 94 | } 95 | 96 | func testAccCheckOctopusDeployLibraryVariableSetDestroy(s *terraform.State) error { 97 | client := testAccProvider.Meta().(*octopusdeploy.Client) 98 | 99 | if err := destroyHelperLibraryVariableSet(s, client); err != nil { 100 | return err 101 | } 102 | if err := destroyEnvHelper(s, client); err != nil { 103 | return err 104 | } 105 | return nil 106 | } 107 | 108 | func testAccCheckOctopusDeployLibraryVariableSetExists(n string) resource.TestCheckFunc { 109 | return func(s *terraform.State) error { 110 | client := testAccProvider.Meta().(*octopusdeploy.Client) 111 | if err := existsHelperLibraryVariableSet(s, client); err != nil { 112 | return err 113 | } 114 | return nil 115 | } 116 | } 117 | 118 | func destroyHelperLibraryVariableSet(s *terraform.State, client *octopusdeploy.Client) error { 119 | for _, r := range s.RootModule().Resources { 120 | if _, err := client.LibraryVariableSet.Get(r.Primary.ID); err != nil { 121 | if err == octopusdeploy.ErrItemNotFound { 122 | continue 123 | } 124 | return fmt.Errorf("Received an error retrieving library variable set %s", err) 125 | } 126 | return fmt.Errorf("library variable set still exists") 127 | } 128 | return nil 129 | } 130 | 131 | func existsHelperLibraryVariableSet(s *terraform.State, client *octopusdeploy.Client) error { 132 | for _, r := range s.RootModule().Resources { 133 | if r.Type == "octopusdeploy_libraryVariableSet" { 134 | if _, err := client.LibraryVariableSet.Get(r.Primary.ID); err != nil { 135 | return fmt.Errorf("received an error retrieving library variable set %s", err) 136 | } 137 | } 138 | } 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /octopusdeploy/resource_lifecycle.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 6 | "github.com/hashicorp/terraform/helper/schema" 7 | "log" 8 | ) 9 | 10 | func resourceLifecycle() *schema.Resource { 11 | return &schema.Resource{ 12 | Create: resourceLifecycleCreate, 13 | Read: resourceLifecycleRead, 14 | Update: resourceLifecycleUpdate, 15 | Delete: resourceLifecycleDelete, 16 | 17 | Schema: map[string]*schema.Schema{ 18 | "name": { 19 | Type: schema.TypeString, 20 | Required: true, 21 | }, 22 | "description": { 23 | Type: schema.TypeString, 24 | Optional: true, 25 | }, 26 | "release_retention_policy": getRetentionPeriodSchema(), 27 | "tentacle_retention_policy": getRetentionPeriodSchema(), 28 | "phase": getPhasesSchema(), 29 | }, 30 | } 31 | } 32 | 33 | func getRetentionPeriodSchema() *schema.Schema { 34 | return &schema.Schema{ 35 | Type: schema.TypeSet, 36 | MaxItems: 1, 37 | Optional: true, 38 | Elem: &schema.Resource{ 39 | Schema: map[string]*schema.Schema{ 40 | "unit": { 41 | Type: schema.TypeString, 42 | Description: "The unit of quantity_to_keep.", 43 | Optional: true, 44 | Default: (string)(octopusdeploy.RetentionUnit_Days), 45 | ValidateFunc: validateValueFunc([]string{ 46 | (string)(octopusdeploy.RetentionUnit_Days), 47 | (string)(octopusdeploy.RetentionUnit_Items), 48 | }), 49 | }, 50 | "quantity_to_keep": { 51 | Type: schema.TypeInt, 52 | Description: "The number of days/releases to keep. If 0 all are kept.", 53 | Default: 0, 54 | Optional: true, 55 | }, 56 | }, 57 | }, 58 | } 59 | } 60 | 61 | func getPhasesSchema() *schema.Schema { 62 | return &schema.Schema{ 63 | Type: schema.TypeList, 64 | Optional: true, 65 | Elem: &schema.Resource{ 66 | Schema: map[string]*schema.Schema{ 67 | "name": { 68 | Type: schema.TypeString, 69 | Required: true, 70 | }, 71 | "minimum_environments_before_promotion": { 72 | Description: "The number of units required before a release can enter the next phase. If 0, all environments are required.", 73 | Type: schema.TypeInt, 74 | Optional: true, 75 | Default: 0, 76 | }, 77 | "is_optional_phase": { 78 | Description: "If false a release must be deployed to this phase before it can be deployed to the next phase.", 79 | Type: schema.TypeBool, 80 | Optional: true, 81 | Default: false, 82 | }, 83 | "automatic_deployment_targets": { 84 | Description: "Environment Ids in this phase that a release is automatically deployed to when it is eligible for this phase", 85 | Type: schema.TypeList, 86 | Optional: true, 87 | Elem: &schema.Schema{ 88 | Type: schema.TypeString, 89 | }, 90 | }, 91 | "optional_deployment_targets": { 92 | Description: "Environment Ids in this phase that a release can be deployed to, but is not automatically deployed to", 93 | Type: schema.TypeList, 94 | Optional: true, 95 | Elem: &schema.Schema{ 96 | Type: schema.TypeString, 97 | }, 98 | }, 99 | //"release_retention_policy": getRetentionPeriodSchema(), 100 | //"tentacle_retention_policy": getRetentionPeriodSchema(), 101 | }, 102 | }, 103 | } 104 | } 105 | 106 | func resourceLifecycleCreate(d *schema.ResourceData, m interface{}) error { 107 | client := m.(*octopusdeploy.Client) 108 | 109 | newLifecycle := buildLifecycleResource(d) 110 | 111 | createdLifecycle, err := client.Lifecycle.Add(newLifecycle) 112 | 113 | if err != nil { 114 | return fmt.Errorf("error creating project: %s", err.Error()) 115 | } 116 | 117 | d.SetId(createdLifecycle.ID) 118 | 119 | return nil 120 | } 121 | 122 | func buildLifecycleResource(d *schema.ResourceData) *octopusdeploy.Lifecycle { 123 | name := d.Get("name").(string) 124 | 125 | lifecycle := octopusdeploy.NewLifecycle(name) 126 | 127 | if attr, ok := d.GetOk("description"); ok { 128 | lifecycle.Description = attr.(string) 129 | } 130 | 131 | releaseRetentionPolicy := getRetentionPeriod(d, "release_retention_policy") 132 | if releaseRetentionPolicy != nil { 133 | lifecycle.ReleaseRetentionPolicy = *releaseRetentionPolicy 134 | } 135 | 136 | tentacleRetentionPolicy := getRetentionPeriod(d, "tentacle_retention_policy") 137 | if tentacleRetentionPolicy != nil { 138 | lifecycle.TentacleRetentionPolicy = *tentacleRetentionPolicy 139 | } 140 | 141 | if attr, ok := d.GetOk("phase"); ok { 142 | tfPhases := attr.([]interface{}) 143 | 144 | for _, tfPhase := range tfPhases { 145 | phase := buildPhaseResource(tfPhase.(map[string]interface{})) 146 | lifecycle.Phases = append(lifecycle.Phases, phase) 147 | } 148 | } 149 | 150 | return lifecycle 151 | } 152 | 153 | func getRetentionPeriod(d *schema.ResourceData, key string) *octopusdeploy.RetentionPeriod { 154 | attr, ok := d.GetOk(key) 155 | if ok { 156 | tfRetentionSettings := attr.(*schema.Set) 157 | if len(tfRetentionSettings.List()) == 1 { 158 | tfRetentionItem := tfRetentionSettings.List()[0].(map[string]interface{}) 159 | retention := octopusdeploy.RetentionPeriod{ 160 | Unit: octopusdeploy.RetentionUnit(tfRetentionItem["unit"].(string)), 161 | QuantityToKeep: int32(tfRetentionItem["quantity_to_keep"].(int)), 162 | } 163 | return &retention 164 | } 165 | } 166 | 167 | return nil 168 | } 169 | 170 | func buildPhaseResource(tfPhase map[string]interface{}) octopusdeploy.Phase { 171 | phase := octopusdeploy.Phase{ 172 | Name: tfPhase["name"].(string), 173 | MinimumEnvironmentsBeforePromotion: int32(tfPhase["minimum_environments_before_promotion"].(int)), 174 | IsOptionalPhase: tfPhase["is_optional_phase"].(bool), 175 | AutomaticDeploymentTargets: getSliceFromTerraformTypeList(tfPhase["automatic_deployment_targets"]), 176 | OptionalDeploymentTargets: getSliceFromTerraformTypeList(tfPhase["optional_deployment_targets"]), 177 | } 178 | 179 | if phase.AutomaticDeploymentTargets == nil { 180 | phase.AutomaticDeploymentTargets = []string{} 181 | } 182 | if phase.OptionalDeploymentTargets == nil { 183 | phase.OptionalDeploymentTargets = []string{} 184 | } 185 | 186 | return phase 187 | } 188 | 189 | func resourceLifecycleRead(d *schema.ResourceData, m interface{}) error { 190 | client := m.(*octopusdeploy.Client) 191 | 192 | lifecycleID := d.Id() 193 | 194 | lifecycle, err := client.Lifecycle.Get(lifecycleID) 195 | 196 | if err == octopusdeploy.ErrItemNotFound { 197 | d.SetId("") 198 | return nil 199 | } 200 | 201 | if err != nil { 202 | return fmt.Errorf("error reading lifecycle id %s: %s", lifecycleID, err.Error()) 203 | } 204 | 205 | log.Printf("[DEBUG] lifecycle: %v", m) 206 | d.Set("name", lifecycle.Name) 207 | d.Set("description", lifecycle.Description) 208 | 209 | return nil 210 | } 211 | 212 | func resourceLifecycleUpdate(d *schema.ResourceData, m interface{}) error { 213 | lifecycle := buildLifecycleResource(d) 214 | lifecycle.ID = d.Id() // set lifecycle struct ID so octopus knows which lifecycle to update 215 | 216 | client := m.(*octopusdeploy.Client) 217 | 218 | lifecycle, err := client.Lifecycle.Update(lifecycle) 219 | 220 | if err != nil { 221 | return fmt.Errorf("error updating lifecycle id %s: %s", d.Id(), err.Error()) 222 | } 223 | 224 | d.SetId(lifecycle.ID) 225 | 226 | return nil 227 | } 228 | 229 | func resourceLifecycleDelete(d *schema.ResourceData, m interface{}) error { 230 | client := m.(*octopusdeploy.Client) 231 | 232 | lifecycleID := d.Id() 233 | 234 | err := client.Lifecycle.Delete(lifecycleID) 235 | 236 | if err != nil { 237 | return fmt.Errorf("error deleting lifecycle id %s: %s", lifecycleID, err.Error()) 238 | } 239 | 240 | d.SetId("") 241 | return nil 242 | } 243 | -------------------------------------------------------------------------------- /octopusdeploy/resource_lifecycle_test.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 8 | "github.com/hashicorp/terraform/helper/resource" 9 | "github.com/hashicorp/terraform/terraform" 10 | ) 11 | 12 | func TestAccOctopusDeployLifecycleBasic(t *testing.T) { 13 | const terraformNamePrefix = "octopusdeploy_lifecycle.foo" 14 | const lifecycleName = "Funky Cycle" 15 | resource.Test(t, resource.TestCase{ 16 | PreCheck: func() { testAccPreCheck(t) }, 17 | Providers: testAccProviders, 18 | CheckDestroy: testAccCheckOctopusDeployLifecycleDestroy, 19 | Steps: []resource.TestStep{ 20 | { 21 | Config: testAccLifecycleBasic(lifecycleName), 22 | Check: resource.ComposeTestCheckFunc( 23 | testAccCheckOctopusDeployLifecycleExists(terraformNamePrefix), 24 | resource.TestCheckResourceAttr( 25 | terraformNamePrefix, "name", lifecycleName), 26 | ), 27 | }, 28 | }, 29 | }) 30 | } 31 | 32 | func TestAccOctopusDeployLifecycleWithUpdate(t *testing.T) { 33 | const terraformNamePrefix = "octopusdeploy_lifecycle.foo" 34 | const lifecycleName = "Funky Cycle" 35 | const description = "I am a new lifecycle description" 36 | resource.Test(t, resource.TestCase{ 37 | PreCheck: func() { testAccPreCheck(t) }, 38 | Providers: testAccProviders, 39 | CheckDestroy: testAccCheckOctopusDeployLifecycleDestroy, 40 | Steps: []resource.TestStep{ 41 | // create projectgroup with no description 42 | { 43 | Config: testAccLifecycleBasic(lifecycleName), 44 | Check: resource.ComposeTestCheckFunc( 45 | testAccCheckOctopusDeployLifecycleExists(terraformNamePrefix), 46 | resource.TestCheckResourceAttr( 47 | terraformNamePrefix, "name", lifecycleName), 48 | ), 49 | }, 50 | // create update it with a description 51 | { 52 | Config: testAccLifecycleWithDescription(lifecycleName, description), 53 | Check: resource.ComposeTestCheckFunc( 54 | testAccCheckOctopusDeployLifecycleExists(terraformNamePrefix), 55 | resource.TestCheckResourceAttr( 56 | terraformNamePrefix, "name", lifecycleName), 57 | resource.TestCheckResourceAttr( 58 | terraformNamePrefix, "description", description), 59 | ), 60 | }, 61 | // update again by remove its description 62 | { 63 | Config: testAccLifecycleBasic(lifecycleName), 64 | Check: resource.ComposeTestCheckFunc( 65 | testAccCheckOctopusDeployLifecycleExists(terraformNamePrefix), 66 | resource.TestCheckResourceAttr( 67 | terraformNamePrefix, "name", lifecycleName), 68 | resource.TestCheckResourceAttr( 69 | terraformNamePrefix, "description", ""), 70 | ), 71 | }, 72 | }, 73 | }) 74 | } 75 | 76 | func TestAccOctopusDeployLifecycleComplex(t *testing.T) { 77 | const terraformNamePrefix = "octopusdeploy_lifecycle.foo" 78 | resource.Test(t, resource.TestCase{ 79 | PreCheck: func() { testAccPreCheck(t) }, 80 | Providers: testAccProviders, 81 | CheckDestroy: testAccCheckOctopusDeployLifecycleDestroy, 82 | Steps: []resource.TestStep{ 83 | { 84 | Config: testAccLifecycleComplex(), 85 | Check: resource.ComposeTestCheckFunc( 86 | testAccCheckOctopusDeployLifecycleExists(terraformNamePrefix), 87 | testAccCheckOctopusDeployLifecyclePhaseCount("Funky Lifecycle", 2), 88 | resource.TestCheckResourceAttr( 89 | terraformNamePrefix, "name", "Funky Lifecycle"), 90 | ), 91 | }, 92 | }, 93 | }) 94 | } 95 | 96 | func testAccLifecycleBasic(name string) string { 97 | return fmt.Sprintf(` 98 | resource "octopusdeploy_lifecycle" "foo" { 99 | name = "%s" 100 | } 101 | `, 102 | name, 103 | ) 104 | } 105 | func testAccLifecycleWithDescription(name, description string) string { 106 | return fmt.Sprintf(` 107 | resource "octopusdeploy_lifecycle" "foo" { 108 | name = "%s" 109 | description = "%s" 110 | } 111 | `, 112 | name, description, 113 | ) 114 | } 115 | 116 | func testAccLifecycleComplex() string { 117 | return ` 118 | resource "octopusdeploy_environment" "Env1" { 119 | name = "LifecycleTestEnv1" 120 | } 121 | 122 | resource "octopusdeploy_environment" "Env2" { 123 | name = "LifecycleTestEnv2" 124 | } 125 | 126 | resource "octopusdeploy_environment" "Env3" { 127 | name = "LifecycleTestEnv3" 128 | } 129 | 130 | resource "octopusdeploy_lifecycle" "foo" { 131 | name = "Funky Lifecycle" 132 | description = "Funky Lifecycle description" 133 | 134 | release_retention_policy { 135 | unit = "Items" 136 | quantity_to_keep = 2 137 | } 138 | 139 | tentacle_retention_policy { 140 | unit = "Days" 141 | quantity_to_keep = 1 142 | } 143 | 144 | phase { 145 | name = "P1" 146 | minimum_environments_before_promotion = 2 147 | is_optional_phase = true 148 | automatic_deployment_targets = ["${octopusdeploy_environment.Env1.id}"] 149 | optional_deployment_targets = ["${octopusdeploy_environment.Env2.id}"] 150 | } 151 | 152 | phase { 153 | name = "P2" 154 | } 155 | } 156 | ` 157 | } 158 | 159 | func testAccCheckOctopusDeployLifecycleDestroy(s *terraform.State) error { 160 | client := testAccProvider.Meta().(*octopusdeploy.Client) 161 | 162 | if err := destroyHelperLifecycle(s, client); err != nil { 163 | return err 164 | } 165 | if err := destroyEnvHelper(s, client); err != nil { 166 | return err 167 | } 168 | return nil 169 | } 170 | 171 | func testAccCheckOctopusDeployLifecycleExists(n string) resource.TestCheckFunc { 172 | return func(s *terraform.State) error { 173 | client := testAccProvider.Meta().(*octopusdeploy.Client) 174 | if err := existsHelperLifecycle(s, client); err != nil { 175 | return err 176 | } 177 | return nil 178 | } 179 | } 180 | 181 | func testAccCheckOctopusDeployLifecyclePhaseCount(name string, expected int) resource.TestCheckFunc { 182 | return func(s *terraform.State) error { 183 | client := testAccProvider.Meta().(*octopusdeploy.Client) 184 | lifecycle, err := client.Lifecycle.GetByName(name) 185 | 186 | if err != nil { 187 | return err 188 | } 189 | 190 | if len(lifecycle.Phases) != expected { 191 | return fmt.Errorf("Lifecycle has %d phases instead of the expected %d", len(lifecycle.Phases), expected) 192 | } 193 | 194 | return nil 195 | } 196 | } 197 | func destroyHelperLifecycle(s *terraform.State, client *octopusdeploy.Client) error { 198 | for _, r := range s.RootModule().Resources { 199 | if _, err := client.Lifecycle.Get(r.Primary.ID); err != nil { 200 | if err == octopusdeploy.ErrItemNotFound { 201 | continue 202 | } 203 | return fmt.Errorf("Received an error retrieving lifecycle %s", err) 204 | } 205 | return fmt.Errorf("lifecycle still exists") 206 | } 207 | return nil 208 | } 209 | 210 | func existsHelperLifecycle(s *terraform.State, client *octopusdeploy.Client) error { 211 | for _, r := range s.RootModule().Resources { 212 | if r.Type == "octopusdeploy_lifecycle" { 213 | if _, err := client.Lifecycle.Get(r.Primary.ID); err != nil { 214 | return fmt.Errorf("received an error retrieving lifecycle %s", err) 215 | } 216 | } 217 | } 218 | return nil 219 | } 220 | -------------------------------------------------------------------------------- /octopusdeploy/resource_machine.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 7 | "github.com/hashicorp/terraform/helper/schema" 8 | ) 9 | 10 | func resourceMachine() *schema.Resource { 11 | return &schema.Resource{ 12 | Create: resourceMachineCreate, 13 | Read: resourceMachineRead, 14 | Update: resourceMachineUpdate, 15 | Delete: resourceMachineDelete, 16 | Importer: &schema.ResourceImporter{ 17 | State: schema.ImportStatePassthrough, 18 | }, 19 | 20 | Schema: map[string]*schema.Schema{ 21 | "name": { 22 | Type: schema.TypeString, 23 | Required: true, 24 | }, 25 | "endpoint": { 26 | Type: schema.TypeSet, 27 | MaxItems: 1, 28 | MinItems: 1, 29 | Required: true, 30 | Elem: &schema.Resource{ 31 | Schema: map[string]*schema.Schema{ 32 | "communicationstyle": { 33 | Type: schema.TypeString, 34 | Required: true, 35 | ValidateFunc: validateValueFunc([]string{ 36 | "None", 37 | "TentaclePassive", 38 | "TentacleActive", 39 | "Ssh", 40 | "OfflineDrop", 41 | "AzureWebApp", 42 | "Ftp", 43 | "AzureCloudService", 44 | }), 45 | }, 46 | 47 | "proxyid": { 48 | Type: schema.TypeString, 49 | Optional: true, 50 | }, 51 | "thumbprint": { 52 | Type: schema.TypeString, 53 | Required: true, 54 | }, 55 | "uri": { 56 | Type: schema.TypeString, 57 | Required: true, 58 | }, 59 | }, 60 | }, 61 | }, 62 | "environments": { 63 | Type: schema.TypeList, 64 | Elem: &schema.Schema{ 65 | Type: schema.TypeString, 66 | }, 67 | Required: true, 68 | }, 69 | "haslatestcalamari": { 70 | Type: schema.TypeBool, 71 | Computed: true, 72 | }, 73 | "isdisabled": { 74 | Type: schema.TypeBool, 75 | Required: true, 76 | }, 77 | "isinprocess": { 78 | Type: schema.TypeBool, 79 | Computed: true, 80 | }, 81 | "machinepolicy": { 82 | Type: schema.TypeString, 83 | Required: true, 84 | }, 85 | "roles": { 86 | Type: schema.TypeList, 87 | Elem: &schema.Schema{ 88 | Type: schema.TypeString, 89 | MinItems: 1, 90 | }, 91 | Required: true, 92 | }, 93 | "status": { 94 | Type: schema.TypeString, 95 | Computed: true, 96 | }, 97 | "statussummary": { 98 | Type: schema.TypeString, 99 | Computed: true, 100 | }, 101 | "tenanteddeploymentparticipation": { 102 | Type: schema.TypeString, 103 | Required: true, 104 | ValidateFunc: validateValueFunc([]string{ 105 | "Untenanted", 106 | "TenantedOrUntenanted", 107 | "Tenanted", 108 | }), 109 | }, 110 | "tenantids": { 111 | Type: schema.TypeList, 112 | Elem: &schema.Schema{ 113 | Type: schema.TypeString, 114 | }, 115 | Optional: true, 116 | }, 117 | "tenanttags": { 118 | Type: schema.TypeList, 119 | Elem: &schema.Schema{ 120 | Type: schema.TypeString, 121 | }, 122 | Optional: true, 123 | }, 124 | }, 125 | } 126 | } 127 | 128 | func resourceMachineRead(d *schema.ResourceData, m interface{}) error { 129 | client := m.(*octopusdeploy.Client) 130 | 131 | machineID := d.Id() 132 | machine, err := client.Machine.Get(machineID) 133 | if err == octopusdeploy.ErrItemNotFound { 134 | d.SetId("") 135 | return nil 136 | } 137 | if err != nil { 138 | return fmt.Errorf("error reading machine %s: %s", machineID, err.Error()) 139 | } 140 | 141 | d.SetId(machine.ID) 142 | setMachineProperties(d, machine) 143 | 144 | return nil 145 | } 146 | 147 | func setMachineProperties(d *schema.ResourceData, m *octopusdeploy.Machine) { 148 | d.Set("environments", m.EnvironmentIDs) 149 | d.Set("haslatestcalamari", m.HasLatestCalamari) 150 | d.Set("isdisabled", m.IsDisabled) 151 | d.Set("isinprocess", m.IsInProcess) 152 | d.Set("machinepolicy", m.MachinePolicyID) 153 | d.Set("roles", m.Roles) 154 | d.Set("status", m.Status) 155 | d.Set("statussummary", m.StatusSummary) 156 | d.Set("tenanteddeploymentparticipation", m.TenantedDeploymentParticipation) 157 | d.Set("tenantids", m.TenantIDs) 158 | d.Set("tenanttags", m.TenantTags) 159 | } 160 | 161 | func buildMachineResource(d *schema.ResourceData) *octopusdeploy.Machine { 162 | mName := d.Get("name").(string) 163 | mMachinepolicy := d.Get("machinepolicy").(string) 164 | mEnvironments := getSliceFromTerraformTypeList(d.Get("environments")) 165 | mRoles := getSliceFromTerraformTypeList(d.Get("roles")) 166 | mDisabled := d.Get("isdisabled").(bool) 167 | mTenantedDeploymentParticipation := d.Get("tenanteddeploymentparticipation").(string) 168 | mTenantIDs := getSliceFromTerraformTypeList(d.Get("tenantids")) 169 | mTenantTags := getSliceFromTerraformTypeList(d.Get("tenanttags")) 170 | 171 | //If we end up with a nil return, Octopus doesn't accept the API call. This ensure that we send 172 | //blank values rather than nil values. 173 | if mTenantIDs == nil { 174 | mTenantIDs = []string{} 175 | } 176 | if mTenantTags == nil { 177 | mTenantTags = []string{} 178 | } 179 | 180 | tfSchemaSetInterface, ok := d.GetOk("endpoint") 181 | if !ok { 182 | return nil 183 | } 184 | tfSchemaSet := tfSchemaSetInterface.(*schema.Set) 185 | if len(tfSchemaSet.List()) == 0 { 186 | return nil 187 | } 188 | //Get the first element in the list, which is a map of the interfaces 189 | tfSchemaList := tfSchemaSet.List()[0].(map[string]interface{}) 190 | 191 | tfMachine := octopusdeploy.NewMachine( 192 | mName, 193 | mDisabled, 194 | mEnvironments, 195 | mRoles, 196 | mMachinepolicy, 197 | mTenantedDeploymentParticipation, 198 | mTenantIDs, 199 | mTenantTags, 200 | ) 201 | 202 | tfMachine.URI = tfSchemaList["uri"].(string) 203 | tfMachine.Thumbprint = tfSchemaList["thumbprint"].(string) 204 | 205 | var proxyid *string 206 | if tfSchemaList["proxyid"] != nil { 207 | proxyString := tfSchemaList["proxyid"].(string) 208 | proxyid = &proxyString 209 | } 210 | 211 | tfMachine.Endpoint = &octopusdeploy.MachineEndpoint{ 212 | URI: tfSchemaList["uri"].(string), 213 | Thumbprint: tfSchemaList["thumbprint"].(string), 214 | CommunicationStyle: tfSchemaList["communicationstyle"].(string), 215 | ProxyID: proxyid, 216 | } 217 | 218 | return tfMachine 219 | } 220 | 221 | func resourceMachineCreate(d *schema.ResourceData, m interface{}) error { 222 | client := m.(*octopusdeploy.Client) 223 | newMachine := buildMachineResource(d) 224 | newMachine.Status = "Unknown" //We don't want TF to attempt to update a machine just because its status has changed, so set it to Unknown on creation and let TF sort it out in the future. 225 | machine, err := client.Machine.Add(newMachine) 226 | if err != nil { 227 | return fmt.Errorf("error creating machine %s: %s", newMachine.Name, err.Error()) 228 | } 229 | d.SetId(machine.ID) 230 | setMachineProperties(d, machine) 231 | return nil 232 | } 233 | 234 | func resourceMachineDelete(d *schema.ResourceData, m interface{}) error { 235 | client := m.(*octopusdeploy.Client) 236 | machineID := d.Id() 237 | err := client.Machine.Delete(machineID) 238 | if err != nil { 239 | return fmt.Errorf("error deleting machine id %s: %s", machineID, err.Error()) 240 | } 241 | d.SetId("") 242 | return nil 243 | } 244 | 245 | func resourceMachineUpdate(d *schema.ResourceData, m interface{}) error { 246 | machine := buildMachineResource(d) 247 | machine.ID = d.Id() // set project struct ID so octopus knows which project to update 248 | client := m.(*octopusdeploy.Client) 249 | updatedMachine, err := client.Machine.Update(machine) 250 | if err != nil { 251 | return fmt.Errorf("error updating machine id %s: %s", d.Id(), err.Error()) 252 | } 253 | d.SetId(updatedMachine.ID) 254 | setMachineProperties(d, machine) 255 | return nil 256 | } 257 | -------------------------------------------------------------------------------- /octopusdeploy/resource_machine_test.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 8 | "github.com/hashicorp/terraform/helper/resource" 9 | "github.com/hashicorp/terraform/terraform" 10 | ) 11 | 12 | func TestAccOctopusDeployMachineBasic(t *testing.T) { 13 | const tfVarPrefix = "octopusdeploy_machine.foomac" 14 | const tfMachineName = "octo-terra-test" 15 | 16 | resource.Test(t, resource.TestCase{ 17 | PreCheck: func() { testAccPreCheck(t) }, 18 | Providers: testAccProviders, 19 | CheckDestroy: testOctopusDeployMachineDestroy, 20 | Steps: []resource.TestStep{ 21 | { 22 | Config: testMachineBasic(tfMachineName), 23 | Check: resource.ComposeTestCheckFunc( 24 | testOctopusDeployMachineExists(tfVarPrefix), 25 | resource.TestCheckResourceAttr( 26 | tfVarPrefix, "name", tfMachineName), 27 | ), 28 | }, 29 | }, 30 | }) 31 | } 32 | 33 | func testMachineBasic(machineName string) string { 34 | config := fmt.Sprintf(` 35 | data "octopusdeploy_machinepolicy" "default" { 36 | name = "Default Machine Policy" 37 | } 38 | 39 | resource "octopusdeploy_environment" "tf_test_env" { 40 | name = "OctopusTestMachineBasic" 41 | description = "Environment for testing Octopus Machines" 42 | use_guided_failure = "false" 43 | } 44 | 45 | resource "octopusdeploy_machine" "foomac" { 46 | name = "%s" 47 | environments = ["${octopusdeploy_environment.tf_test_env.id}"] 48 | isdisabled = true 49 | machinepolicy = "${data.octopusdeploy_machinepolicy.default.id}" 50 | roles = ["Prod"] 51 | tenanteddeploymentparticipation = "Untenanted" 52 | 53 | endpoint { 54 | communicationstyle = "None" 55 | thumbprint = "" 56 | uri = "" 57 | } 58 | } 59 | `, machineName, 60 | ) 61 | fmt.Println(config) 62 | return config 63 | } 64 | 65 | func testOctopusDeployMachineExists(n string) resource.TestCheckFunc { 66 | return func(s *terraform.State) error { 67 | client := testAccProvider.Meta().(*octopusdeploy.Client) 68 | return existsMachineHelper(s, client) 69 | } 70 | } 71 | 72 | func existsMachineHelper(s *terraform.State, client *octopusdeploy.Client) error { 73 | macID := s.RootModule().Resources["octopusdeploy_machine.foomac"].Primary.ID 74 | 75 | if _, err := client.Machine.Get(macID); err != nil { 76 | return fmt.Errorf("Received an error retrieving machine %s", err) 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func testOctopusDeployMachineDestroy(s *terraform.State) error { 83 | client := testAccProvider.Meta().(*octopusdeploy.Client) 84 | return destroyMachineHelper(s, client) 85 | } 86 | 87 | func destroyMachineHelper(s *terraform.State, client *octopusdeploy.Client) error { 88 | macID := s.RootModule().Resources["octopusdeploy_machine.foomac"].Primary.ID 89 | 90 | if err := client.Machine.Delete(macID); err != nil { 91 | if err == octopusdeploy.ErrItemNotFound { 92 | return nil 93 | } 94 | return fmt.Errorf("Received an error retrieving machine %s", err) 95 | } 96 | return fmt.Errorf("Machine still exists") 97 | } 98 | -------------------------------------------------------------------------------- /octopusdeploy/resource_project_deployment_target_trigger.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 8 | "github.com/hashicorp/terraform/helper/schema" 9 | ) 10 | 11 | func resourceProjectDeploymentTargetTrigger() *schema.Resource { 12 | return &schema.Resource{ 13 | Create: resourceProjectDeploymentTargetTriggerCreate, 14 | Read: resourceProjectDeploymentTargetTriggerRead, 15 | Update: resourceProjectDeploymentTargetTriggerUpdate, 16 | Delete: resourceProjectDeploymentTargetTriggerDelete, 17 | 18 | Schema: map[string]*schema.Schema{ 19 | "name": { 20 | Type: schema.TypeString, 21 | Required: true, 22 | Description: "The name of the trigger.", 23 | }, 24 | "project_id": { 25 | Type: schema.TypeString, 26 | Required: true, 27 | Description: "The project_id of the Project to attach the trigger to.", 28 | }, 29 | "should_redeploy": { 30 | Type: schema.TypeBool, 31 | Optional: true, 32 | Default: false, 33 | Description: "Enable to re-deploy to the deployment targets even if they are already up-to-date with the current deployment.", 34 | }, 35 | "event_groups": { 36 | Type: schema.TypeList, 37 | Elem: &schema.Schema{ 38 | Type: schema.TypeString, 39 | }, 40 | Optional: true, 41 | Description: "Apply event group filters to restrict which deployment targets will actually cause the trigger to fire, and consequently, which deployment targets will be automatically deployed to.", 42 | }, 43 | "event_categories": { 44 | Type: schema.TypeList, 45 | Elem: &schema.Schema{ 46 | Type: schema.TypeString, 47 | }, 48 | Optional: true, 49 | Description: "Apply event category filters to restrict which deployment targets will actually cause the trigger to fire, and consequently, which deployment targets will be automatically deployed to.", 50 | }, 51 | "roles": { 52 | Type: schema.TypeList, 53 | Elem: &schema.Schema{ 54 | Type: schema.TypeString, 55 | }, 56 | Optional: true, 57 | Description: "Apply event role filters to restrict which deployment targets will actually cause the trigger to fire, and consequently, which deployment targets will be automatically deployed to.", 58 | }, 59 | "environment_ids": { 60 | Type: schema.TypeList, 61 | Elem: &schema.Schema{ 62 | Type: schema.TypeString, 63 | }, 64 | Optional: true, 65 | Description: "Apply environment id filters to restrict which deployment targets will actually cause the trigger to fire, and consequently, which deployment targets will be automatically deployed to.", 66 | }, 67 | }, 68 | } 69 | } 70 | 71 | func buildProjectDeploymentTargetTriggerResource(d *schema.ResourceData) (*octopusdeploy.ProjectTrigger, error) { 72 | name := d.Get("name").(string) 73 | projectID := d.Get("project_id").(string) 74 | shouldRedeploy := d.Get("should_redeploy").(bool) 75 | 76 | deploymentTargetTrigger := octopusdeploy.NewProjectDeploymentTargetTrigger(name, projectID, shouldRedeploy, nil, nil, nil) 77 | 78 | if attr, ok := d.GetOk("event_groups"); ok { 79 | eventGroups := getSliceFromTerraformTypeList(attr) 80 | 81 | // need to validate here "ValidateFunc is not yet supported on lists or sets." 82 | validValues := []string{ 83 | "Machine", 84 | "MachineCritical", 85 | "MachineAvailableForDeployment", 86 | "MachineUnavailableForDeployment", 87 | "MachineHealthChanged", 88 | } 89 | 90 | if invalidValue, ok := validateAllSliceItemsInSlice(eventGroups, validValues); !ok { 91 | return nil, fmt.Errorf("Invalid value for event_groups. %s not in %v", invalidValue, validValues) 92 | } 93 | 94 | deploymentTargetTrigger.AddEventGroups(eventGroups) 95 | } 96 | 97 | if attr, ok := d.GetOk("event_categories"); ok { 98 | eventCategories := getSliceFromTerraformTypeList(attr) 99 | 100 | // need to validate here "ValidateFunc is not yet supported on lists or sets." 101 | validValues := []string{ 102 | "MachineCleanupFailed", 103 | "MachineAdded", 104 | "MachineDeploymentRelatedPropertyWasUpdated", 105 | "MachineDisabled", 106 | "MachineEnabled", 107 | "MachineHealthy", 108 | "MachineUnavailable", 109 | "MachineUnhealthy", 110 | "MachineHasWarnings", 111 | } 112 | 113 | if invalidValue, ok := validateAllSliceItemsInSlice(eventCategories, validValues); !ok { 114 | return nil, fmt.Errorf("Invalid value for event_categories. %s not in %v", invalidValue, validValues) 115 | } 116 | 117 | deploymentTargetTrigger.AddEventCategories(eventCategories) 118 | } 119 | 120 | if attr, ok := d.GetOk("roles"); ok { 121 | deploymentTargetTrigger.Filter.Roles = getSliceFromTerraformTypeList(attr) 122 | } 123 | 124 | if attr, ok := d.GetOk("environment_ids"); ok { 125 | deploymentTargetTrigger.Filter.EnvironmentIds = getSliceFromTerraformTypeList(attr) 126 | } 127 | 128 | return deploymentTargetTrigger, nil 129 | } 130 | 131 | func resourceProjectDeploymentTargetTriggerCreate(d *schema.ResourceData, m interface{}) error { 132 | client := m.(*octopusdeploy.Client) 133 | 134 | deploymentTargetTrigger, err := buildProjectDeploymentTargetTriggerResource(d) 135 | 136 | if err != nil { 137 | return err 138 | } 139 | 140 | createdProjectDeploymentTargetTrigger, err := client.ProjectTrigger.Add(deploymentTargetTrigger) 141 | 142 | if err != nil { 143 | return fmt.Errorf("error creating project deployment target trigger: %s", err.Error()) 144 | } 145 | 146 | d.SetId(createdProjectDeploymentTargetTrigger.ID) 147 | return nil 148 | } 149 | 150 | func resourceProjectDeploymentTargetTriggerRead(d *schema.ResourceData, m interface{}) error { 151 | client := m.(*octopusdeploy.Client) 152 | 153 | projectTriggerID := d.Id() 154 | 155 | projectTrigger, err := client.ProjectTrigger.Get(projectTriggerID) 156 | 157 | if err == octopusdeploy.ErrItemNotFound { 158 | d.SetId("") 159 | return nil 160 | } 161 | 162 | if err != nil { 163 | return fmt.Errorf("error reading project trigger id %s: %s", projectTrigger.ID, err.Error()) 164 | } 165 | 166 | log.Printf("[DEBUG] project trigger: %v", m) 167 | d.Set("name", projectTrigger.Name) 168 | d.Set("should_redeploy", projectTrigger.Action.ShouldRedeployWhenMachineHasBeenDeployedTo) 169 | d.Set("event_groups", projectTrigger.Filter.EventGroups) 170 | d.Set("event_categories", projectTrigger.Filter.EventCategories) 171 | d.Set("roles", projectTrigger.Filter.Roles) 172 | d.Set("environment_ids", projectTrigger.Filter.EnvironmentIds) 173 | return nil 174 | } 175 | 176 | func resourceProjectDeploymentTargetTriggerUpdate(d *schema.ResourceData, m interface{}) error { 177 | deploymentTargetTrigger, err := buildProjectDeploymentTargetTriggerResource(d) 178 | 179 | if err != nil { 180 | return err 181 | } 182 | 183 | deploymentTargetTrigger.ID = d.Id() // set deploymenttrigger struct ID so octopus knows which to update 184 | 185 | client := m.(*octopusdeploy.Client) 186 | 187 | updatedProjectTrigger, err := client.ProjectTrigger.Update(deploymentTargetTrigger) 188 | 189 | if err != nil { 190 | return fmt.Errorf("error updating project trigger id %s: %s", d.Id(), err.Error()) 191 | } 192 | 193 | d.SetId(updatedProjectTrigger.ID) 194 | return nil 195 | } 196 | 197 | func resourceProjectDeploymentTargetTriggerDelete(d *schema.ResourceData, m interface{}) error { 198 | client := m.(*octopusdeploy.Client) 199 | 200 | projectTriggerID := d.Id() 201 | 202 | err := client.ProjectTrigger.Delete(projectTriggerID) 203 | 204 | if err != nil { 205 | return fmt.Errorf("error deleting project trigger id %s: %s", projectTriggerID, err.Error()) 206 | } 207 | 208 | d.SetId("") 209 | return nil 210 | } 211 | -------------------------------------------------------------------------------- /octopusdeploy/resource_project_deployment_target_trigger_test.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 8 | "github.com/hashicorp/terraform/helper/acctest" 9 | "github.com/hashicorp/terraform/helper/resource" 10 | "github.com/hashicorp/terraform/terraform" 11 | ) 12 | 13 | func TestAccOctopusDeployDeploymentTargetTriggerAddDelete(t *testing.T) { 14 | const terraformNamePrefix = "octopusdeploy_project_deployment_target_trigger.foo" 15 | const deployTargetTriggerName = "Funky Monkey Trigger" 16 | projectName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) 17 | 18 | resource.Test(t, resource.TestCase{ 19 | PreCheck: func() { testAccPreCheck(t) }, 20 | Providers: testAccProviders, 21 | CheckDestroy: testAccCheckOctopusDeployProjectDestroy, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: testAccProjectDeploymentTargetTriggerResource(t, deployTargetTriggerName, projectName), 25 | Check: resource.ComposeTestCheckFunc( 26 | testAccProjectTriggerExists(terraformNamePrefix), 27 | resource.TestCheckResourceAttr( 28 | terraformNamePrefix, "name", deployTargetTriggerName), 29 | resource.TestCheckResourceAttr( 30 | terraformNamePrefix, "should_redeploy", "true"), 31 | resource.TestCheckResourceAttr( 32 | terraformNamePrefix, "event_groups.0", "Machine"), 33 | resource.TestCheckResourceAttr( 34 | terraformNamePrefix, "event_categories.0", "MachineCleanupFailed"), 35 | ), 36 | }, 37 | }, 38 | }) 39 | } 40 | 41 | func TestAccOctopusDeployDeploymentTargetTriggerUpdate(t *testing.T) { 42 | const terraformNamePrefix = "octopusdeploy_project_deployment_target_trigger.foo" 43 | const deployTargetTriggerName = "Funky Monkey Trigger" 44 | projectName := acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) 45 | 46 | resource.Test(t, resource.TestCase{ 47 | PreCheck: func() { testAccPreCheck(t) }, 48 | Providers: testAccProviders, 49 | CheckDestroy: testAccCheckOctopusDeployProjectDestroy, 50 | Steps: []resource.TestStep{ 51 | { 52 | Config: testAccProjectDeploymentTargetTriggerResource(t, deployTargetTriggerName, projectName), 53 | Check: resource.ComposeTestCheckFunc( 54 | testAccProjectTriggerExists(terraformNamePrefix), 55 | resource.TestCheckResourceAttr( 56 | terraformNamePrefix, "event_groups.0", "Machine"), 57 | resource.TestCheckResourceAttr( 58 | terraformNamePrefix, "event_categories.0", "MachineCleanupFailed"), 59 | resource.TestCheckResourceAttr( 60 | terraformNamePrefix, "should_redeploy", "true"), 61 | ), 62 | }, 63 | { 64 | Config: testAccProjectDeploymentTargetTriggerResourceUpdated(t, deployTargetTriggerName, projectName), 65 | Check: resource.ComposeTestCheckFunc( 66 | testAccProjectTriggerExists(terraformNamePrefix), 67 | resource.TestCheckResourceAttr( 68 | terraformNamePrefix, "event_groups.0", "Machine"), 69 | resource.TestCheckResourceAttr( 70 | terraformNamePrefix, "event_groups.1", "MachineCritical"), 71 | resource.TestCheckResourceAttr( 72 | terraformNamePrefix, "event_categories.0", "MachineHealthy"), 73 | resource.TestCheckResourceAttr( 74 | terraformNamePrefix, "should_redeploy", "false"), 75 | ), 76 | }, 77 | }, 78 | }) 79 | } 80 | 81 | func testAccProjectDeploymentTargetTriggerResource(t *testing.T, triggerName, projectName string) string { 82 | return fmt.Sprintf(` 83 | resource "octopusdeploy_project_group" "foo" { 84 | name = "Integration Test Project Group" 85 | } 86 | 87 | resource "octopusdeploy_project" "foo" { 88 | lifecycle_id = "Lifecycles-1" 89 | name = "%s" 90 | project_group_id = "${octopusdeploy_project_group.foo.id}" 91 | } 92 | 93 | resource "octopusdeploy_project_deployment_target_trigger" "foo" { 94 | name = "%s" 95 | project_id = "${octopusdeploy_project.foo.id}" 96 | event_groups = ["Machine"] 97 | event_categories = ["MachineCleanupFailed"] 98 | should_redeploy = true 99 | 100 | roles = [ 101 | "FooRoles" 102 | ] 103 | } 104 | `, 105 | projectName, triggerName, 106 | ) 107 | } 108 | 109 | func testAccProjectDeploymentTargetTriggerResourceUpdated(t *testing.T, triggerName, projectName string) string { 110 | return fmt.Sprintf(` 111 | resource "octopusdeploy_project_group" "foo" { 112 | name = "Integration Test Project Group" 113 | } 114 | 115 | resource "octopusdeploy_project" "foo" { 116 | lifecycle_id = "Lifecycles-1" 117 | name = "%s" 118 | project_group_id = "${octopusdeploy_project_group.foo.id}" 119 | } 120 | 121 | resource "octopusdeploy_project_deployment_target_trigger" "foo" { 122 | name = "%s" 123 | project_id = "${octopusdeploy_project.foo.id}" 124 | event_groups = ["Machine", "MachineCritical"] 125 | event_categories = ["MachineHealthy"] 126 | should_redeploy = false 127 | 128 | roles = [ 129 | "FooRoles" 130 | ] 131 | } 132 | `, 133 | projectName, triggerName, 134 | ) 135 | } 136 | 137 | // testAccProjectDeploymentTriggerExists checks if a ProjectTrigger Exists 138 | func testAccProjectTriggerExists(resourceName string) resource.TestCheckFunc { 139 | return func(s *terraform.State) error { 140 | rs, ok := s.RootModule().Resources[resourceName] 141 | if !ok { 142 | return fmt.Errorf("Not found: %s", resourceName) 143 | } 144 | 145 | client := testAccProvider.Meta().(*octopusdeploy.Client) 146 | 147 | if _, err := client.ProjectTrigger.Get(rs.Primary.ID); err != nil { 148 | return fmt.Errorf("Received an error retrieving project trigger %s", err) 149 | } 150 | 151 | return nil 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /octopusdeploy/resource_project_group.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 8 | "github.com/hashicorp/terraform/helper/schema" 9 | ) 10 | 11 | func resourceProjectGroup() *schema.Resource { 12 | return &schema.Resource{ 13 | Create: resourceProjectGroupCreate, 14 | Read: resourceProjectGroupRead, 15 | Update: resourceProjectGroupUpdate, 16 | Delete: resourceProjectGroupDelete, 17 | 18 | Schema: map[string]*schema.Schema{ 19 | "name": { 20 | Type: schema.TypeString, 21 | Required: true, 22 | }, 23 | "description": { 24 | Type: schema.TypeString, 25 | Optional: true, 26 | }, 27 | }, 28 | } 29 | } 30 | 31 | func buildProjectGroupResource(d *schema.ResourceData) *octopusdeploy.ProjectGroup { 32 | name := d.Get("name").(string) 33 | 34 | projectGroup := octopusdeploy.NewProjectGroup(name) 35 | 36 | if attr, ok := d.GetOk("description"); ok { 37 | projectGroup.Description = attr.(string) 38 | } 39 | 40 | return projectGroup 41 | } 42 | 43 | func resourceProjectGroupCreate(d *schema.ResourceData, m interface{}) error { 44 | client := m.(*octopusdeploy.Client) 45 | 46 | newProjectGroup := buildProjectGroupResource(d) 47 | 48 | createdProjectGroup, err := client.ProjectGroup.Add(newProjectGroup) 49 | 50 | if err != nil { 51 | return fmt.Errorf("error creating projectgroup: %s", err.Error()) 52 | } 53 | 54 | d.SetId(createdProjectGroup.ID) 55 | return nil 56 | } 57 | 58 | func resourceProjectGroupRead(d *schema.ResourceData, m interface{}) error { 59 | client := m.(*octopusdeploy.Client) 60 | 61 | projectGroupID := d.Id() 62 | 63 | projectGroup, err := client.ProjectGroup.Get(projectGroupID) 64 | 65 | if err == octopusdeploy.ErrItemNotFound { 66 | d.SetId("") 67 | return nil 68 | } 69 | 70 | if err != nil { 71 | return fmt.Errorf("error reading projectgroup id %s: %s", projectGroup.ID, err.Error()) 72 | } 73 | 74 | log.Printf("[DEBUG] projectgroup: %v", m) 75 | d.Set("name", projectGroup.Name) 76 | d.Set("description", projectGroup.Description) 77 | return nil 78 | } 79 | 80 | func resourceProjectGroupUpdate(d *schema.ResourceData, m interface{}) error { 81 | projectGroup := buildProjectGroupResource(d) 82 | projectGroup.ID = d.Id() // set projectgroup struct ID so octopus knows which to update 83 | 84 | client := m.(*octopusdeploy.Client) 85 | 86 | updatedProject, err := client.ProjectGroup.Update(projectGroup) 87 | 88 | if err != nil { 89 | return fmt.Errorf("error updating projectgroup id %s: %s", d.Id(), err.Error()) 90 | } 91 | 92 | d.SetId(updatedProject.ID) 93 | return nil 94 | } 95 | 96 | func resourceProjectGroupDelete(d *schema.ResourceData, m interface{}) error { 97 | client := m.(*octopusdeploy.Client) 98 | 99 | projectGroupID := d.Id() 100 | 101 | err := client.ProjectGroup.Delete(projectGroupID) 102 | 103 | if err != nil { 104 | return fmt.Errorf("error deleting projectgroup id %s: %s", projectGroupID, err.Error()) 105 | } 106 | 107 | d.SetId("") 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /octopusdeploy/resource_project_group_test.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 8 | "github.com/hashicorp/terraform/helper/resource" 9 | "github.com/hashicorp/terraform/terraform" 10 | ) 11 | 12 | func TestAccOctopusDeployProjectGroupBasic(t *testing.T) { 13 | const terraformNamePrefix = "octopusdeploy_project_group.foo" 14 | const projectGroupName = "Funky Group" 15 | resource.Test(t, resource.TestCase{ 16 | PreCheck: func() { testAccPreCheck(t) }, 17 | Providers: testAccProviders, 18 | CheckDestroy: testAccCheckOctopusDeployProjectGroupDestroy, 19 | Steps: []resource.TestStep{ 20 | { 21 | Config: testAccProjectGroupBasic(projectGroupName), 22 | Check: resource.ComposeTestCheckFunc( 23 | testAccCheckOctopusDeployProjectGroupExists(terraformNamePrefix), 24 | resource.TestCheckResourceAttr( 25 | terraformNamePrefix, "name", projectGroupName), 26 | ), 27 | }, 28 | }, 29 | }) 30 | } 31 | 32 | func TestAccOctopusDeployProjectGroupWithUpdate(t *testing.T) { 33 | const terraformNamePrefix = "octopusdeploy_project_group.foo" 34 | const projectGroupName = "Funky Group" 35 | const description = "I am a new group description" 36 | resource.Test(t, resource.TestCase{ 37 | PreCheck: func() { testAccPreCheck(t) }, 38 | Providers: testAccProviders, 39 | CheckDestroy: testAccCheckOctopusDeployProjectGroupDestroy, 40 | Steps: []resource.TestStep{ 41 | // create projectgroup with no description 42 | { 43 | Config: testAccProjectGroupBasic(projectGroupName), 44 | Check: resource.ComposeTestCheckFunc( 45 | testAccCheckOctopusDeployProjectGroupExists(terraformNamePrefix), 46 | resource.TestCheckResourceAttr( 47 | terraformNamePrefix, "name", projectGroupName), 48 | ), 49 | }, 50 | // create update it with a description 51 | { 52 | Config: testAccProjectGroupWithDescription(projectGroupName, description), 53 | Check: resource.ComposeTestCheckFunc( 54 | testAccCheckOctopusDeployProjectGroupExists(terraformNamePrefix), 55 | resource.TestCheckResourceAttr( 56 | terraformNamePrefix, "name", projectGroupName), 57 | resource.TestCheckResourceAttr( 58 | terraformNamePrefix, "description", description), 59 | ), 60 | }, 61 | // update again by remove its description 62 | { 63 | Config: testAccProjectGroupBasic(projectGroupName), 64 | Check: resource.ComposeTestCheckFunc( 65 | testAccCheckOctopusDeployProjectGroupExists(terraformNamePrefix), 66 | resource.TestCheckResourceAttr( 67 | terraformNamePrefix, "name", projectGroupName), 68 | resource.TestCheckResourceAttr( 69 | terraformNamePrefix, "description", ""), 70 | ), 71 | }, 72 | }, 73 | }) 74 | } 75 | 76 | func testAccProjectGroupBasic(name string) string { 77 | return fmt.Sprintf(` 78 | resource "octopusdeploy_project_group" "foo" { 79 | name = "%s" 80 | } 81 | `, 82 | name, 83 | ) 84 | } 85 | func testAccProjectGroupWithDescription(name, description string) string { 86 | return fmt.Sprintf(` 87 | resource "octopusdeploy_project_group" "foo" { 88 | name = "%s" 89 | description = "%s" 90 | } 91 | `, 92 | name, description, 93 | ) 94 | } 95 | 96 | func testAccCheckOctopusDeployProjectGroupDestroy(s *terraform.State) error { 97 | client := testAccProvider.Meta().(*octopusdeploy.Client) 98 | 99 | if err := destroyHelperProjectGroup(s, client); err != nil { 100 | return err 101 | } 102 | return nil 103 | } 104 | 105 | func testAccCheckOctopusDeployProjectGroupExists(n string) resource.TestCheckFunc { 106 | return func(s *terraform.State) error { 107 | client := testAccProvider.Meta().(*octopusdeploy.Client) 108 | if err := existsHelperProjectGroup(s, client); err != nil { 109 | return err 110 | } 111 | return nil 112 | } 113 | } 114 | 115 | func destroyHelperProjectGroup(s *terraform.State, client *octopusdeploy.Client) error { 116 | for _, r := range s.RootModule().Resources { 117 | if _, err := client.ProjectGroup.Get(r.Primary.ID); err != nil { 118 | if err == octopusdeploy.ErrItemNotFound { 119 | continue 120 | } 121 | return fmt.Errorf("Received an error retrieving projectgroup %s", err) 122 | } 123 | return fmt.Errorf("projectgroup still exists") 124 | } 125 | return nil 126 | } 127 | 128 | func existsHelperProjectGroup(s *terraform.State, client *octopusdeploy.Client) error { 129 | for _, r := range s.RootModule().Resources { 130 | if _, err := client.ProjectGroup.Get(r.Primary.ID); err != nil { 131 | return fmt.Errorf("received an error retrieving projectgroup %s", err) 132 | } 133 | } 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /octopusdeploy/resource_project_test.go: -------------------------------------------------------------------------------- 1 | package octopusdeploy 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/MattHodge/go-octopusdeploy/octopusdeploy" 10 | "github.com/hashicorp/terraform/helper/resource" 11 | "github.com/hashicorp/terraform/terraform" 12 | ) 13 | 14 | func TestAccOctopusDeployProjectBasic(t *testing.T) { 15 | const terraformNamePrefix = "octopusdeploy_project.foo" 16 | const projectName = "Funky Monkey" 17 | const lifeCycleID = "Lifecycles-1" 18 | resource.Test(t, resource.TestCase{ 19 | PreCheck: func() { testAccPreCheck(t) }, 20 | Providers: testAccProviders, 21 | CheckDestroy: testAccCheckOctopusDeployProjectDestroy, 22 | Steps: []resource.TestStep{ 23 | { 24 | Config: testAccProjectBasic(projectName, lifeCycleID), 25 | Check: resource.ComposeTestCheckFunc( 26 | testAccCheckOctopusDeployProjectExists(terraformNamePrefix), 27 | resource.TestCheckResourceAttr( 28 | terraformNamePrefix, "name", projectName), 29 | resource.TestCheckResourceAttr( 30 | terraformNamePrefix, "lifecycle_id", lifeCycleID), 31 | resource.TestCheckResourceAttrSet( 32 | terraformNamePrefix, "project_group_id"), 33 | ), 34 | }, 35 | }, 36 | }) 37 | } 38 | 39 | func TestAccOctopusDeployProjectWithDeploymentStepWindowsService(t *testing.T) { 40 | const terraformNamePrefix = "octopusdeploy_project.foo" 41 | const projectName = "Funky Monkey" 42 | const lifeCycleID = "Lifecycles-1" 43 | const serviceName = "Epic Service" 44 | const executablePath = `bin\\MyService.exe` 45 | const stepName = "Deploying Epic Service" 46 | const packageName = "MyPackage" 47 | targetRoles := []string{"Lab1", "Lab2"} 48 | projectIDRegex := regexp.MustCompile(`Projects\-`) 49 | deploymentProcessIDRegex := regexp.MustCompile(`deploymentprocess\-Projects\-.*`) 50 | 51 | resource.Test(t, resource.TestCase{ 52 | PreCheck: func() { testAccPreCheck(t) }, 53 | Providers: testAccProviders, 54 | CheckDestroy: testAccCheckOctopusDeployProjectDestroy, 55 | Steps: []resource.TestStep{ 56 | { 57 | Config: testAccWithDeploymentStepWindowsService(projectName, lifeCycleID, serviceName, executablePath, stepName, packageName, targetRoles), 58 | Check: resource.ComposeTestCheckFunc( 59 | testAccCheckOctopusDeployProjectExists(terraformNamePrefix), 60 | resource.TestMatchResourceAttr( 61 | terraformNamePrefix, "id", projectIDRegex), 62 | resource.TestCheckResourceAttr( 63 | terraformNamePrefix, "name", projectName), 64 | resource.TestCheckResourceAttr( 65 | terraformNamePrefix, "lifecycle_id", lifeCycleID), 66 | resource.TestCheckResourceAttrSet( 67 | terraformNamePrefix, "project_group_id"), 68 | resource.TestCheckResourceAttr( 69 | terraformNamePrefix, "deployment_step_windows_service.0.service_name", serviceName), 70 | resource.TestCheckResourceAttr( 71 | terraformNamePrefix, "deployment_step_windows_service.0.step_name", stepName), 72 | resource.TestCheckResourceAttr( 73 | terraformNamePrefix, "deployment_step_windows_service.0.target_roles.0", targetRoles[0]), 74 | resource.TestCheckResourceAttr( 75 | terraformNamePrefix, "deployment_step_windows_service.0.target_roles.1", targetRoles[1]), 76 | resource.TestCheckResourceAttr( 77 | terraformNamePrefix, "deployment_step_windows_service.0.executable_path", strings.Replace(executablePath, "\\\\", "\\", 1)), // need to scape the backslashes 78 | resource.TestMatchResourceAttr( 79 | terraformNamePrefix, "deployment_process_id", deploymentProcessIDRegex), 80 | ), 81 | }, 82 | }, 83 | }) 84 | } 85 | 86 | func TestAccOctopusDeployProjectWithUpdate(t *testing.T) { 87 | const terraformNamePrefix = "octopusdeploy_project.foo" 88 | const projectName = "Funky Monkey" 89 | const lifeCycleID = "Lifecycles-1" 90 | inlineScriptRegex := regexp.MustCompile(`.*Get\-Process.*`) 91 | resource.Test(t, resource.TestCase{ 92 | PreCheck: func() { testAccPreCheck(t) }, 93 | Providers: testAccProviders, 94 | CheckDestroy: testAccCheckOctopusDeployProjectDestroy, 95 | Steps: []resource.TestStep{ 96 | // create project with no description 97 | { 98 | Config: testAccProjectBasic(projectName, lifeCycleID), 99 | Check: resource.ComposeTestCheckFunc( 100 | testAccCheckOctopusDeployProjectExists(terraformNamePrefix), 101 | resource.TestCheckResourceAttr( 102 | terraformNamePrefix, "name", projectName), 103 | resource.TestCheckResourceAttr( 104 | terraformNamePrefix, "lifecycle_id", lifeCycleID), 105 | resource.TestCheckResourceAttrSet( 106 | terraformNamePrefix, "project_group_id"), 107 | ), 108 | }, 109 | // create update it with a description + build steps 110 | { 111 | Config: testAccWithMultipleDeploymentStepWindowsService, 112 | Check: resource.ComposeTestCheckFunc( 113 | testAccCheckOctopusDeployProjectExists(terraformNamePrefix), 114 | resource.TestCheckResourceAttr( 115 | terraformNamePrefix, "name", "Project Name"), 116 | resource.TestCheckResourceAttr( 117 | terraformNamePrefix, "lifecycle_id", "Lifecycles-1"), 118 | resource.TestCheckResourceAttrSet( 119 | terraformNamePrefix, "project_group_id"), 120 | resource.TestCheckResourceAttr( 121 | terraformNamePrefix, "description", "My Awesome Description"), 122 | resource.TestCheckResourceAttr( 123 | terraformNamePrefix, "deployment_step_windows_service.0.service_name", "My First Service"), 124 | resource.TestCheckResourceAttr( 125 | terraformNamePrefix, "deployment_step_windows_service.0.step_name", "Deploy My First Service"), 126 | resource.TestCheckResourceAttr( 127 | terraformNamePrefix, "deployment_step_windows_service.0.target_roles.0", "Role1"), 128 | resource.TestCheckResourceAttr( 129 | terraformNamePrefix, "deployment_step_windows_service.0.target_roles.1", "Role2"), 130 | resource.TestCheckResourceAttr( 131 | terraformNamePrefix, "deployment_step_windows_service.0.executable_path", "C:\\MyService\\my_service.exe"), 132 | resource.TestCheckResourceAttr( 133 | terraformNamePrefix, "deployment_step_windows_service.1.service_name", "My Second Service"), 134 | resource.TestCheckResourceAttr( 135 | terraformNamePrefix, "deployment_step_windows_service.1.step_name", "Deploy My Second Service"), 136 | resource.TestCheckResourceAttr( 137 | terraformNamePrefix, "deployment_step_windows_service.1.target_roles.0", "Role3"), 138 | resource.TestCheckNoResourceAttr( 139 | terraformNamePrefix, "deployment_step_windows_service.1.target_roles.1"), 140 | resource.TestCheckResourceAttr( 141 | terraformNamePrefix, "deployment_step_windows_service.1.executable_path", "C:\\MyService\\my_service2.exe"), 142 | resource.TestCheckResourceAttr( 143 | terraformNamePrefix, "deployment_step_windows_service.1.configuration_transforms", "false"), 144 | resource.TestCheckResourceAttr( 145 | terraformNamePrefix, "deployment_step_windows_service.1.configuration_variables", "false"), 146 | resource.TestCheckResourceAttr( 147 | terraformNamePrefix, "deployment_step_iis_website.0.step_name", "Deploy Website"), 148 | resource.TestCheckResourceAttr( 149 | terraformNamePrefix, "deployment_step_iis_website.0.target_roles.0", "MyRole1"), 150 | resource.TestCheckResourceAttr( 151 | terraformNamePrefix, "deployment_step_iis_website.0.website_name", "Awesome Website"), 152 | resource.TestCheckResourceAttr( 153 | terraformNamePrefix, "deployment_step_iis_website.0.application_pool_name", "MyAppPool"), 154 | resource.TestCheckResourceAttr( 155 | terraformNamePrefix, "deployment_step_iis_website.0.application_pool_framework", "v2.0"), 156 | resource.TestCheckResourceAttr( 157 | terraformNamePrefix, "deployment_step_iis_website.0.step_condition", "failure"), 158 | resource.TestCheckResourceAttr( 159 | terraformNamePrefix, "deployment_step_iis_website.0.basic_authentication", "true"), 160 | resource.TestCheckResourceAttr( 161 | terraformNamePrefix, "deployment_step_iis_website.0.anonymous_authentication", "true"), 162 | resource.TestCheckResourceAttr( 163 | terraformNamePrefix, "deployment_step_iis_website.0.json_file_variable_replacement", "appsettings.json,Config\\*.json"), 164 | resource.TestCheckResourceAttr( 165 | terraformNamePrefix, "deployment_step_inline_script.0.step_name", "Run Cleanup Script"), 166 | resource.TestCheckResourceAttr( 167 | terraformNamePrefix, "deployment_step_inline_script.0.target_roles.0", "MyRole1"), 168 | resource.TestCheckResourceAttr( 169 | terraformNamePrefix, "deployment_step_inline_script.0.target_roles.1", "MyRole2"), 170 | resource.TestMatchResourceAttr( 171 | terraformNamePrefix, "deployment_step_inline_script.0.script_body", inlineScriptRegex), 172 | resource.TestCheckResourceAttr( 173 | terraformNamePrefix, "deployment_step_inline_script.0.script_type", "PowerShell"), 174 | resource.TestCheckResourceAttr( 175 | terraformNamePrefix, "deployment_step_inline_script.0.step_condition", "success"), 176 | resource.TestCheckResourceAttr( 177 | terraformNamePrefix, "deployment_step_package_script.0.feed_id", "feeds-builtin"), 178 | resource.TestCheckResourceAttr( 179 | terraformNamePrefix, "deployment_step_package_script.0.package", "cleanup.yolo"), 180 | resource.TestCheckResourceAttr( 181 | terraformNamePrefix, "deployment_step_package_script.0.script_file_name", "bin\\cleanup.ps1"), 182 | resource.TestCheckResourceAttr( 183 | terraformNamePrefix, "deployment_step_package_script.0.script_parameters", "-Force"), 184 | resource.TestCheckResourceAttr( 185 | terraformNamePrefix, "deployment_step_package_script.0.step_name", "Run Verify From Package Script"), 186 | resource.TestCheckResourceAttr( 187 | terraformNamePrefix, "deployment_step_package_script.0.step_condition", "success"), 188 | resource.TestCheckResourceAttr( 189 | terraformNamePrefix, "deployment_step_package_script.0.target_roles.0", "MyRole1"), 190 | resource.TestCheckResourceAttr( 191 | terraformNamePrefix, "deployment_step_package_script.0.target_roles.1", "MyRole2"), 192 | ), 193 | }, 194 | // update again by remove its description 195 | { 196 | Config: testAccProjectBasic(projectName, lifeCycleID), 197 | Check: resource.ComposeTestCheckFunc( 198 | testAccCheckOctopusDeployProjectExists(terraformNamePrefix), 199 | resource.TestCheckResourceAttr( 200 | terraformNamePrefix, "name", projectName), 201 | resource.TestCheckResourceAttr( 202 | terraformNamePrefix, "lifecycle_id", lifeCycleID), 203 | resource.TestCheckResourceAttrSet( 204 | terraformNamePrefix, "project_group_id"), 205 | resource.TestCheckResourceAttr( 206 | terraformNamePrefix, "description", ""), 207 | resource.TestCheckNoResourceAttr( 208 | terraformNamePrefix, "deployment_step_windows_service.0.step_name"), 209 | resource.TestCheckNoResourceAttr( 210 | terraformNamePrefix, "deployment_step_windows_service.1.step_name"), 211 | resource.TestCheckNoResourceAttr( 212 | terraformNamePrefix, "deployment_step_iis_website.0.step_name"), 213 | ), 214 | }, 215 | }, 216 | }) 217 | } 218 | 219 | func testAccProjectBasic(name, lifeCycleID string) string { 220 | return fmt.Sprintf(` 221 | resource "octopusdeploy_project_group" "foo" { 222 | name = "Integration Test Project Group" 223 | } 224 | 225 | resource "octopusdeploy_project" "foo" { 226 | name = "%s" 227 | lifecycle_id = "%s" 228 | project_group_id = "${octopusdeploy_project_group.foo.id}" 229 | } 230 | `, 231 | name, lifeCycleID, 232 | ) 233 | } 234 | 235 | const testAccWithMultipleDeploymentStepWindowsService = ` 236 | resource "octopusdeploy_project_group" "foo" { 237 | name = "Integration Test Project Group" 238 | } 239 | 240 | resource "octopusdeploy_project" "foo" { 241 | name = "Project Name" 242 | lifecycle_id = "Lifecycles-1" 243 | project_group_id = "${octopusdeploy_project_group.foo.id}" 244 | description = "My Awesome Description" 245 | 246 | deployment_step_windows_service { 247 | executable_path = "C:\\MyService\\my_service.exe" 248 | package = "MyPackage" 249 | service_name = "My First Service" 250 | step_name = "Deploy My First Service" 251 | 252 | target_roles = [ 253 | "Role1", 254 | "Role2" 255 | ] 256 | } 257 | 258 | deployment_step_windows_service { 259 | configuration_transforms = false 260 | configuration_variables = false 261 | executable_path = "C:\\MyService\\my_service2.exe" 262 | package = "MyServicePackage" 263 | service_account = "NewServiceAccount" 264 | service_name = "My Second Service" 265 | service_start_mode = "demand" 266 | step_name = "Deploy My Second Service" 267 | step_start_trigger = "StartWithPrevious" 268 | 269 | target_roles = [ 270 | "Role3", 271 | ] 272 | } 273 | 274 | deployment_step_iis_website { 275 | anonymous_authentication = true 276 | application_pool_framework = "v2.0" 277 | application_pool_name = "MyAppPool" 278 | basic_authentication = true 279 | json_file_variable_replacement = "appsettings.json,Config\\*.json" 280 | package = "MyWebsitePackage" 281 | step_condition = "failure" 282 | step_name = "Deploy Website" 283 | website_name = "Awesome Website" 284 | 285 | target_roles = [ 286 | "MyRole1", 287 | ] 288 | } 289 | 290 | deployment_step_inline_script { 291 | step_name = "Run Cleanup Script" 292 | script_type = "PowerShell" 293 | 294 | script_body = <