├── .github └── workflows │ └── continuous-integration.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── Makefile ├── README.md ├── acc-test-environment ├── README.md ├── database.tf ├── main.tf ├── network.tf ├── scripts │ └── Install-OctopusServer.ps1 ├── storage.tf └── vm.tf ├── datasource_environment.go ├── datasource_machine.go ├── datasource_project.go ├── datasource_variable.go ├── main.go ├── provider.go ├── provisioner.go ├── resource_environment.go ├── resource_helper.go ├── resource_variable.go └── utilities.go /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | go: ["1.12", "1.13"] 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | steps: 12 | - name: Set up Go 13 | uses: actions/setup-go@v1 14 | with: 15 | go-version: ${{ matrix.go }} 16 | id: go 17 | 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v1 20 | 21 | - name: Get dependencies 22 | run: | 23 | go get -v -t -d ./... 24 | 25 | - name: Build 26 | run: go build -v . 27 | 28 | - name: Run Unit Tests 29 | run: go test -v . 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | _bin 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | # Mac 28 | .DS_Store 29 | 30 | # Terraform local state and variables 31 | .terraform/ 32 | .terraform.tfstate* 33 | terraform.tfstate* 34 | terraform.tfvars 35 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/octopus"] 2 | path = vendor/octopus 3 | url = https://github.com/DimensionDataResearch/go-octo-api.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dimension Data Research 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 | default: build test 2 | 3 | fmt: 4 | go fmt github.com/DimensionDataResearch/terraform-octopus/... 5 | 6 | build: fmt 7 | go build -o _bin/terraform-provider-octopus 8 | cp _bin/terraform-provider-octopus _bin/terraform-provisioner-octopus 9 | 10 | test: fmt 11 | go test -v github.com/DimensionDataResearch/terraform-octopus/vendor/octopus 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-octopus 2 | A plugin for Terraform to control / integrate with [Octopus Deploy](https://octopus.com/). 3 | 4 | This is a work in progress. More providers and data-sources are planned, as well as a [Provisioner](https://www.terraform.io/docs/provisioners/index.html) to install the Octopus tentacle. 5 | 6 | Tested against Octopus Deploy v3.3.17. 7 | 8 | The following resource types are currently supported: 9 | 10 | * `octopus_environment`: Creates and manages an Octopus Deploy environment 11 | * `octopus_variable`: Creates and manages an Octopus Deploy variable (currently only project-level variables are supported) 12 | 13 | Note that variables are matched on both name and combined scopes (Environments, Roles, Machines, Actions). If a variable already exists with the specified name and scopes, the provider will start managing the existing variable. 14 | 15 | The following data-source types are currently supported: 16 | * `octopus_environment`: Tracks an existing Octopus Deploy environment 17 | * `octopus_machine`: Tracks an existing Octopus Deploy machine 18 | * `octopus_project`: Tracks an existing Octopus Deploy project 19 | * `octopus_variable`: Tracks an existing Octopus Deploy variable (currently only project-level variables are supported) 20 | 21 | Data-sources are similar to variables, except they are read-only. The provider will read and track their state but never modify it. 22 | 23 | To get started: 24 | 25 | * On windows, create / update `$HOME\terraform.rc` 26 | * On Linux / OSX, create / update `~/.terraformrc` 27 | 28 | And add the following contents: 29 | 30 | ```hcl 31 | providers { 32 | octopus = "path-to-the-folder/containing/terraform-provider-octopus" 33 | } 34 | ``` 35 | 36 | Create a folder containing a single `.tf` file: 37 | 38 | ```hcl 39 | # 40 | # This configuration will create an Octopus environment called "MyEnvironment" and configure a project-level variable named "MyVariable" to be scoped to it. 41 | # 42 | 43 | provider "octopus" { 44 | server_url = "https://my-octopus-server/" 45 | api_key = "my-octopus-api-key" 46 | } 47 | 48 | # Projects are a data source - the provider can read from them but not create or manage them. 49 | data "octopus_project" "my_project" { 50 | slug = "terraformtest" # The last segment of the URL in the browser when viewing the project home page. 51 | } 52 | 53 | data "octopus_machine" "my_machine" { 54 | slug = "Machines-351" # The last segment of the URL in the browser when viewing the machine details home page. 55 | } 56 | 57 | resource "octopus_environment" "my_environment" { 58 | name = "MyEnvironment" 59 | } 60 | 61 | resource "octopus_variable" "my_variable" { 62 | # This is the Id (or slug) of the project in which the variable is defined. 63 | project = "${data.octopus_project.my_project.id}" 64 | 65 | name = "MyVariable" 66 | value = "Hello World" 67 | 68 | # The scopes (environment, role, machine, action) to which the variable applies. 69 | environments = ["${octopus_environment.my_environment.id}"] 70 | } 71 | ``` 72 | 73 | 1. Run `terraform plan -out tf.plan`. 74 | 2. Verify that everything looks ok. 75 | 3. Run `terraform apply tf.plan` 76 | 4. Have a look around and 77 | 5. Run `terraform show` to inspect the current state. 78 | 6. when it's time to clean up... 79 | 7. Run `terraform plan -destroy -out tf.plan` 80 | 8. Verify that everything looks ok. 81 | 9. Run `terraform apply tf.plan` 82 | -------------------------------------------------------------------------------- /acc-test-environment/README.md: -------------------------------------------------------------------------------- 1 | # Acceptance test environment 2 | 3 | This configuration creates (in Azure) the Octopus Deploy server used in Terraform acceptance tests. 4 | 5 | You can create credentials for use in tests using the [Azure cross-platform CLI](https://www.terraform.io/docs/providers/azurerm/index.html#creating-credentials-using-the-azure-cli). 6 | 7 | You'll need to supply a couple of values in `terraform.tfvars`: 8 | 9 | * `azure_subscription_id` - The Id of your Azure subscription. 10 | * `azure_client_id` - The client Id you created in the Azure CLI. 11 | * `azure_client_secret` - The client secret you created in the Azure CLI. 12 | * `azure_tenant_id` - The name of the Azure AD tenant that the subscription belongs to. 13 | 14 | There are also a couple of option values in `main.tf` that you can override by supplying their values in `terraform.tfvars`: 15 | 16 | * `region_name` - The name of the target region where you will be deploying the environment. 17 | **Note** - the VM and the storage account _must_ be the same location. 18 | * `resource_group_name` - The name of the resource group where the environment will be deployed (must already exist; the configuration will not create it). 19 | * `storage_account_name` - The name of the Azure Storage account where VM disks (etc) will be stored. 20 | * `uniqueness_key` - A unique value added to resource names so that your deployment doesn't clash with other deployments. 21 | -------------------------------------------------------------------------------- /acc-test-environment/database.tf: -------------------------------------------------------------------------------- 1 | ## Database for Octopus Deploy 2 | 3 | # SQL Server for the acceptance test environment 4 | resource "azurerm_sql_server" "primary" { 5 | name = "tfoctoacctest${var.uniqueness_key}" 6 | resource_group_name = "${var.resource_group_name}" 7 | location = "${var.region_name}" 8 | version = "12.0" 9 | 10 | administrator_login = "${var.admin_username}" 11 | administrator_login_password = "${var.admin_password}" 12 | } 13 | 14 | # Database used by the environment's Octopus server 15 | resource "azurerm_sql_database" "octo" { 16 | name = "tfoctoacctest${var.uniqueness_key}" 17 | resource_group_name = "${var.resource_group_name}" 18 | location = "${var.region_name}" 19 | 20 | edition = "Basic" 21 | server_name = "${azurerm_sql_server.primary.name}" 22 | } 23 | 24 | # Permit T-SQL access from the Octopus server to the SQL server. 25 | resource "azurerm_sql_firewall_rule" "octo_server" { 26 | name = "octo_server_${var.uniqueness_key}" 27 | resource_group_name = "${var.resource_group_name}" 28 | 29 | server_name = "${azurerm_sql_server.primary.name}" 30 | start_ip_address = "${azurerm_network_interface.octo.private_ip_address}" 31 | end_ip_address = "${azurerm_network_interface.octo.private_ip_address}" 32 | } 33 | -------------------------------------------------------------------------------- /acc-test-environment/main.tf: -------------------------------------------------------------------------------- 1 | ## Provider configuration 2 | 3 | # The Id of the target Azure subscription. 4 | variable "azure_subscription_id" { } 5 | 6 | # The client Id used to authenticate to Azure. 7 | variable "azure_client_id" { } 8 | 9 | # The client secret used to authenticate to Azure. 10 | variable "azure_client_secret" { } 11 | 12 | # The Id of target Azure AD tenant. 13 | variable "azure_tenant_id" { } 14 | 15 | provider "azurerm" { 16 | subscription_id = "${var.azure_subscription_id}" 17 | client_id = "${var.azure_client_id}" 18 | client_secret = "${var.azure_client_secret}" 19 | tenant_id = "${var.azure_tenant_id}" 20 | } 21 | 22 | ## Common configuration 23 | 24 | # The name of the target Azure region (i.e. datacenter). 25 | variable "region_name" { default = "West US" } 26 | 27 | # The name of the resource group that holds the Octopus server used by acceptance tests. 28 | variable "resource_group_name" { default = "terraform-provider-octopus-acctest" } 29 | 30 | # The name of the storage account where VM disks (etc) are located. 31 | variable "storage_account_name" { default = "tfprovideroctopusacctest" } 32 | 33 | # Used to prevent naming clashes between multiple concurrent deployments. 34 | variable "uniqueness_key" { default = "acctest" } 35 | 36 | # The instance type for the Octopus Server VM. 37 | variable "octo_vm_instance_type" { default = "Standard_A3" } 38 | 39 | # The administrator username for the Octopus and SQL servers. 40 | variable "admin_username" { default = "octo-admin" } 41 | 42 | # The administrator password for the Octopus and SQL servers. 43 | variable "admin_password" { } 44 | -------------------------------------------------------------------------------- /acc-test-environment/network.tf: -------------------------------------------------------------------------------- 1 | # The primary network for the acceptance test environment. 2 | resource "azurerm_virtual_network" "primary" { 3 | name = "tf-octo-acc-test-${var.uniqueness_key}-network" 4 | address_space = ["10.7.0.0/16"] 5 | location = "${var.region_name}" 6 | resource_group_name = "${var.resource_group_name}" 7 | } 8 | 9 | # The primary subnet for the acceptance test environment. 10 | resource "azurerm_subnet" "primary" { 11 | name = "tf-octo-acc-test-${var.uniqueness_key}-subnet" 12 | resource_group_name = "${var.resource_group_name}" 13 | virtual_network_name = "${azurerm_virtual_network.primary.name}" 14 | address_prefix = "10.7.1.0/24" 15 | } 16 | 17 | # The primary network adapter for the Octopus Server VM. 18 | resource "azurerm_network_interface" "octo" { 19 | name = "octo-${var.uniqueness_key}-ni" 20 | location = "${var.region_name}" 21 | resource_group_name = "${var.resource_group_name}" 22 | 23 | ip_configuration { 24 | name = "octo-${var.uniqueness_key}-ni-config" 25 | subnet_id = "${azurerm_subnet.primary.id}" 26 | 27 | # Hook up public IP to private IP. 28 | public_ip_address_id = "${azurerm_public_ip.octo.id}" 29 | private_ip_address_allocation = "dynamic" 30 | } 31 | } 32 | 33 | # The default network security group for the acceptance test environment. 34 | resource "azurerm_network_security_group" "default" { 35 | name = "octo-${var.uniqueness_key}-default-nsg" 36 | location = "${var.region_name}" 37 | resource_group_name = "${var.resource_group_name}" 38 | 39 | # Remote Desktop 40 | security_rule { 41 | name = "rdp" 42 | priority = 100 43 | direction = "Inbound" 44 | access = "Allow" 45 | protocol = "Tcp" 46 | source_port_range = "*" 47 | destination_port_range = "3389" 48 | source_address_prefix = "*" 49 | destination_address_prefix = "*" 50 | } 51 | 52 | # WinRM 53 | security_rule { 54 | name = "winrm" 55 | priority = 101 56 | direction = "Inbound" 57 | access = "Allow" 58 | protocol = "Tcp" 59 | source_port_range = "*" 60 | destination_port_range = "5986" # HTTPS 61 | source_address_prefix = "*" 62 | destination_address_prefix = "*" 63 | } 64 | } 65 | 66 | # Public IP address for access to the Octopus Server VM. 67 | resource "azurerm_public_ip" "octo" { 68 | name = "tf-octo-acc-test-${var.uniqueness_key}-pip" 69 | location = "${var.region_name}" 70 | resource_group_name = "${var.resource_group_name}" 71 | 72 | domain_name_label = "tf-octo-acc-test-${var.uniqueness_key}" 73 | public_ip_address_allocation = "static" 74 | } 75 | -------------------------------------------------------------------------------- /acc-test-environment/scripts/Install-OctopusServer.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [Parameter(Mandatory = $true)] 3 | [string] $SqlServerHost, 4 | 5 | [Parameter(Mandatory = $true)] 6 | [string] $Database, 7 | 8 | [Parameter(Mandatory = $true)] 9 | [string] $User, 10 | 11 | [Parameter(Mandatory = $true)] 12 | [string] $Password 13 | ) 14 | 15 | Import-Module PowerShellGet 16 | Install-Module OctopusDSC -Force 17 | Import-Module OctopusDSC 18 | 19 | Configuration Octopus { 20 | Import-DscResource -Module OctopusDSC 21 | 22 | Node "localhost" { 23 | cOctopusServer OctopusServer { 24 | Ensure = "present" 25 | State = "started" 26 | 27 | Name = "OctopusServer" 28 | 29 | WebListenPrefix = "http://localhost:9081" 30 | SqlDbConnectionString = "Server=$SqlServerHost;Database=$Database;UID=$User;PWD=$Password" 31 | 32 | OctopusAdminUsername = $User 33 | OctopusAdminPassword = $Password 34 | 35 | # AllowUpgradeCheck = $false 36 | # AllowCollectionOfAnonymousUsageStatistics = $false 37 | # ForceSSL = $false 38 | # ListenPort = 10943 39 | } 40 | } 41 | } 42 | Octopus 43 | 44 | Start-DscConfiguration .\Octopus -Verbose -Wait 45 | Test-DscConfiguration 46 | -------------------------------------------------------------------------------- /acc-test-environment/storage.tf: -------------------------------------------------------------------------------- 1 | # Storage configuration 2 | 3 | resource "azurerm_storage_container" "primary" { 4 | name = "tf-octo-acc-test-${var.uniqueness_key}" 5 | resource_group_name = "${var.resource_group_name}" 6 | storage_account_name = "${var.storage_account_name}" 7 | container_access_type = "private" 8 | } 9 | -------------------------------------------------------------------------------- /acc-test-environment/vm.tf: -------------------------------------------------------------------------------- 1 | # The Octopus Deploy virtual machine. 2 | resource "azurerm_virtual_machine" "octo" { 3 | name = "octo-${var.uniqueness_key}" 4 | location = "${var.region_name}" 5 | resource_group_name = "${var.resource_group_name}" 6 | network_interface_ids = [ "${azurerm_network_interface.octo.id}" ] 7 | 8 | vm_size = "${var.octo_vm_instance_type}" 9 | 10 | storage_image_reference { 11 | publisher = "MicrosoftWindowsServer" 12 | offer = "WindowsServer" 13 | sku = "2012-R2-Datacenter" 14 | version = "latest" 15 | } 16 | 17 | storage_os_disk { 18 | name = "octo-${var.uniqueness_key}-osdisk1" 19 | vhd_uri = "https://${var.storage_account_name}.blob.core.windows.net/${azurerm_storage_container.primary.name}/octo-${var.uniqueness_key}-osdisk1.vhd" 20 | caching = "ReadWrite" 21 | create_option = "FromImage" 22 | } 23 | 24 | os_profile { 25 | computer_name = "octo-${var.uniqueness_key}" 26 | admin_username = "${var.admin_username}" 27 | admin_password = "${var.admin_password}" 28 | } 29 | 30 | os_profile_windows_config { 31 | provision_vm_agent = true 32 | enable_automatic_upgrades = true 33 | 34 | winrm { 35 | protocol = "http" 36 | } 37 | } 38 | 39 | tags { 40 | public_ip = "${azurerm_public_ip.octo.ip_address}" 41 | private_ip = "${azurerm_network_interface.octo.private_ip_address}" 42 | } 43 | } 44 | 45 | # Configure WinRM to enable SSL (since it's otherwise unusable, given Azure's default config). 46 | resource "azurerm_virtual_machine_extension" "configure_winrm" { 47 | name = "ConfigureWinRM" 48 | location = "${var.region_name}" 49 | resource_group_name = "${var.resource_group_name}" 50 | virtual_machine_name = "${azurerm_virtual_machine.octo.name}" 51 | publisher = "Microsoft.Compute" 52 | type = "CustomScriptExtension" 53 | type_handler_version = "1.8" 54 | 55 | settings = < 0 || allowEmpty { 48 | return &typedValue 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (helper resourcePropertyHelper) GetOptionalInt(key string, allowZero bool) *int { 56 | value := helper.data.Get(key) 57 | switch typedValue := value.(type) { 58 | case int: 59 | if typedValue != 0 || allowZero { 60 | return &typedValue 61 | } 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func (helper resourcePropertyHelper) GetOptionalBool(key string) *bool { 68 | value := helper.data.Get(key) 69 | switch typedValue := value.(type) { 70 | case bool: 71 | return &typedValue 72 | default: 73 | return nil 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /resource_variable.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hashicorp/terraform/helper/schema" 6 | "log" 7 | "octopus" 8 | ) 9 | 10 | const ( 11 | resourceKeyVariableProjectID = "project" 12 | resourceKeyVariableName = "name" 13 | resourceKeyVariableValue = "value" 14 | resourceKeyVariableEnvironments = "environments" 15 | resourceKeyVariableRoles = "roles" 16 | resourceKeyVariableMachines = "machines" 17 | resourceKeyVariableActions = "actions" 18 | ) 19 | 20 | func resourceVariable() *schema.Resource { 21 | return &schema.Resource{ 22 | Create: resourceVariableCreate, 23 | Read: resourceVariableRead, 24 | Update: resourceVariableUpdate, 25 | Delete: resourceVariableDelete, 26 | 27 | Schema: map[string]*schema.Schema{ 28 | resourceKeyVariableProjectID: &schema.Schema{ 29 | Type: schema.TypeString, 30 | Required: true, 31 | Description: "The variable name.", 32 | }, 33 | resourceKeyVariableName: &schema.Schema{ 34 | Type: schema.TypeString, 35 | Required: true, 36 | }, 37 | resourceKeyVariableValue: &schema.Schema{ 38 | Type: schema.TypeString, 39 | Optional: true, 40 | Computed: true, 41 | Default: nil, 42 | }, 43 | resourceKeyVariableEnvironments: &schema.Schema{ 44 | Type: schema.TypeList, 45 | Elem: &schema.Schema{ 46 | Type: schema.TypeString, 47 | }, 48 | Optional: true, 49 | Computed: true, 50 | }, 51 | resourceKeyVariableRoles: &schema.Schema{ 52 | Type: schema.TypeList, 53 | Elem: &schema.Schema{ 54 | Type: schema.TypeString, 55 | }, 56 | Optional: true, 57 | Computed: true, 58 | }, 59 | resourceKeyVariableMachines: &schema.Schema{ 60 | Type: schema.TypeList, 61 | Elem: &schema.Schema{ 62 | Type: schema.TypeString, 63 | }, 64 | Optional: true, 65 | Computed: true, 66 | }, 67 | resourceKeyVariableActions: &schema.Schema{ 68 | Type: schema.TypeList, 69 | Elem: &schema.Schema{ 70 | Type: schema.TypeString, 71 | }, 72 | Optional: true, 73 | Computed: true, 74 | }, 75 | }, 76 | } 77 | } 78 | 79 | // Create a variable resource. 80 | func resourceVariableCreate(data *schema.ResourceData, provider interface{}) error { 81 | propertyHelper := propertyHelper(data) 82 | 83 | projectID := data.Get(resourceKeyVariableProjectID).(string) 84 | name := data.Get(resourceKeyVariableName).(string) 85 | 86 | targetScope := octopus.VariableScopes{ 87 | Environments: propertyHelper.GetStringList(resourceKeyVariableEnvironments), 88 | Roles: propertyHelper.GetStringList(resourceKeyVariableRoles), 89 | Machines: propertyHelper.GetStringList(resourceKeyVariableMachines), 90 | Actions: propertyHelper.GetStringList(resourceKeyVariableActions), 91 | } 92 | 93 | log.Printf("Create variable '%s' for project '%s' (must match scopes: %#v)...", name, projectID, targetScope) 94 | 95 | providerClient := provider.(*octopus.Client) 96 | 97 | variableSet, err := providerClient.GetProjectVariableSet(projectID) 98 | if err != nil { 99 | return fmt.Errorf("Error retrieving variable set for project '%s': %s", projectID, err.Error()) 100 | } 101 | if variableSet == nil { 102 | return fmt.Errorf("Cannot find variable set for project '%s'", projectID) 103 | } 104 | 105 | matchingVariables := variableSet.GetVariablesByNameAndScopes(name, targetScope) 106 | 107 | var variable octopus.Variable 108 | if len(matchingVariables) == 0 { 109 | log.Printf("Create variable '%s' for project '%s' with scope %#v...", name, projectID, targetScope) 110 | 111 | variableSet.Variables = append(variableSet.Variables, octopus.Variable{ 112 | Name: name, 113 | Scope: targetScope, 114 | }) 115 | 116 | variableSet, err = providerClient.UpdateVariableSet(variableSet) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | matchingVariables = variableSet.GetVariablesByNameAndScopes(name, targetScope) 122 | if len(matchingVariables) != 1 { 123 | return fmt.Errorf("Found %d matching variables named '%s' for scope %#v (after attempting to create this variable for that scope).", len(matchingVariables), name, targetScope) 124 | } 125 | } else if len(matchingVariables) == 1 { 126 | log.Printf("Variable '%s' already exists for project '%s' with scope %#v.", name, projectID, targetScope) 127 | } else { 128 | return fmt.Errorf("Multiple variables exactly match scope %#v for variable '%s'.", name, targetScope) 129 | } 130 | 131 | variable = matchingVariables[0] 132 | data.SetId(variable.ID) 133 | data.Set(resourceKeyVariableValue, variable.Value) 134 | 135 | propertyHelper.SetStringList(resourceKeyVariableEnvironments, variable.Scope.Environments) 136 | propertyHelper.SetStringList(resourceKeyVariableRoles, variable.Scope.Roles) 137 | propertyHelper.SetStringList(resourceKeyVariableMachines, variable.Scope.Machines) 138 | propertyHelper.SetStringList(resourceKeyVariableActions, variable.Scope.Actions) 139 | 140 | return nil 141 | } 142 | 143 | // Read a variable resource. 144 | func resourceVariableRead(data *schema.ResourceData, provider interface{}) error { 145 | id := data.Id() 146 | projectID := data.Get(resourceKeyVariableProjectID).(string) 147 | 148 | log.Printf("Read variable '%s' (for project '%s').", id, projectID) 149 | 150 | providerClient := provider.(*octopus.Client) 151 | 152 | variableSet, err := providerClient.GetProjectVariableSet(projectID) 153 | if err != nil { 154 | return err 155 | } 156 | if variableSet == nil { 157 | return fmt.Errorf("Cannot find variable set for project '%s'.", projectID) 158 | } 159 | 160 | variable := variableSet.GetVariableByID(id) 161 | if variable == nil { 162 | // Variable has been deleted. 163 | data.SetId("") 164 | 165 | return nil 166 | } 167 | 168 | data.Set(resourceKeyVariableValue, variable.Value) 169 | 170 | log.Printf("Variable scope is now %#v", variable.Scope) 171 | propertyHelper := propertyHelper(data) 172 | propertyHelper.SetStringList(resourceKeyVariableEnvironments, variable.Scope.Environments) 173 | propertyHelper.SetStringList(resourceKeyVariableRoles, variable.Scope.Roles) 174 | propertyHelper.SetStringList(resourceKeyVariableMachines, variable.Scope.Machines) 175 | propertyHelper.SetStringList(resourceKeyVariableActions, variable.Scope.Actions) 176 | 177 | return nil 178 | } 179 | 180 | // Update a variable resource. 181 | func resourceVariableUpdate(data *schema.ResourceData, provider interface{}) error { 182 | id := data.Id() 183 | projectID := data.Get(resourceKeyVariableProjectID).(string) 184 | 185 | log.Printf("Update variable '%s' (for project '%s').", id, projectID) 186 | 187 | providerClient := provider.(*octopus.Client) 188 | 189 | variableSet, err := providerClient.GetProjectVariableSet(projectID) 190 | if err != nil { 191 | return err 192 | } 193 | if variableSet == nil { 194 | return fmt.Errorf("Cannot find variable set for project '%s'.", projectID) 195 | } 196 | 197 | variable := variableSet.GetVariableByID(id) 198 | if variable == nil { 199 | // Variable has been deleted. 200 | data.SetId("") 201 | 202 | return nil 203 | } 204 | 205 | variableSet.UpdateVariable(id, func(variable *octopus.Variable) { 206 | if data.HasChange(resourceKeyVariableValue) { 207 | value, ok := data.GetOk(resourceKeyVariableValue) 208 | if ok { 209 | variable.Value = value.(string) 210 | } else { 211 | data.Set(resourceKeyVariableValue, variable.Value) 212 | } 213 | } 214 | 215 | // TODO: Propagate scope changes (if any). 216 | }) 217 | 218 | _, err = providerClient.UpdateVariableSet(variableSet) 219 | 220 | return err 221 | } 222 | 223 | // Delete a variable resource. 224 | func resourceVariableDelete(data *schema.ResourceData, provider interface{}) error { 225 | id := data.Id() 226 | projectID := data.Get(resourceKeyVariableProjectID).(string) 227 | 228 | log.Printf("Delete variable '%s' (for project '%s').", id, projectID) 229 | 230 | providerClient := provider.(*octopus.Client) 231 | providerClient.Reset() // TODO: Replace call to Reset with appropriate API call(s). 232 | 233 | return nil 234 | } 235 | 236 | // Determine whether a variable resource exists. 237 | func resourceVariableExists(data *schema.ResourceData, provider interface{}) (exists bool, err error) { 238 | id := data.Id() 239 | projectID := data.Get(resourceKeyVariableProjectID).(string) 240 | 241 | log.Printf("Check if variable '%s' exists.", id) 242 | 243 | client := provider.(*octopus.Client) 244 | 245 | var variableSet *octopus.VariableSet 246 | variableSet, err = client.GetProjectVariableSet(projectID) 247 | if err != nil { 248 | return 249 | } 250 | 251 | exists = variableSet.GetVariableByID(id) != nil 252 | 253 | return 254 | } 255 | -------------------------------------------------------------------------------- /utilities.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hashicorp/terraform/helper/schema" 5 | ) 6 | 7 | func newStringSet() *schema.Set { 8 | return &schema.Set{ 9 | F: func(item interface{}) int { 10 | str := item.(string) 11 | 12 | return schema.HashString(str) 13 | }, 14 | } 15 | } 16 | 17 | func stringToPtr(value string) *string { 18 | return &value 19 | } 20 | 21 | func isEmpty(value string) bool { 22 | return len(value) == 0 23 | } 24 | --------------------------------------------------------------------------------