diff --git a/Tasks/TerraformTask/TerraformTaskV4/src/base-terraform-command-handler.ts b/Tasks/TerraformTask/TerraformTaskV4/src/base-terraform-command-handler.ts index 447cf10..caac934 100644 --- a/Tasks/TerraformTask/TerraformTaskV4/src/base-terraform-command-handler.ts +++ b/Tasks/TerraformTask/TerraformTaskV4/src/base-terraform-command-handler.ts @@ -54,7 +54,7 @@ export abstract class BaseTerraformCommandHandler { cwd: tasks.getInput("workingDirectory") }); - let countProviders = ["aws", "azurerm", "google"].filter(provider => commandOutput.stdout.includes(provider)).length; + let countProviders = ["aws", "azurerm", "google", "oracle"].filter(provider => commandOutput.stdout.includes(provider)).length; tasks.debug(countProviders.toString()); if (countProviders > 1) { @@ -69,6 +69,7 @@ export abstract class BaseTerraformCommandHandler { case "azurerm": return "AzureRM"; case "aws" : return "AWS"; case "gcp" : return "GCP"; + case "oci" : return "OCI"; } } diff --git a/Tasks/TerraformTask/TerraformTaskV4/src/oci-terraform-command-handler.ts b/Tasks/TerraformTask/TerraformTaskV4/src/oci-terraform-command-handler.ts new file mode 100644 index 0000000..4938c64 --- /dev/null +++ b/Tasks/TerraformTask/TerraformTaskV4/src/oci-terraform-command-handler.ts @@ -0,0 +1,72 @@ +import tasks = require('azure-pipelines-task-lib/task'); +import {ToolRunner} from 'azure-pipelines-task-lib/toolrunner'; +import {TerraformAuthorizationCommandInitializer} from './terraform-commands'; +import {BaseTerraformCommandHandler} from './base-terraform-command-handler'; +import path = require('path'); +import * as uuidV4 from 'uuid/v4'; + +export class TerraformCommandHandlerOCI extends BaseTerraformCommandHandler { + constructor() { + super(); + this.providerName = "oci"; + } + + private getPrivateKeyFilePath(privateKey: string) { + // This is a bit of a hack but spaces need to be converted to line breaks to make it work + privateKey = privateKey.replace('-----BEGIN PRIVATE KEY-----', '_begin_'); + privateKey = privateKey.replace('-----END PRIVATE KEY-----', '_end_'); + while(privateKey.indexOf(' ') > -1) + { + privateKey = privateKey.replace(' ', '\n'); + } + privateKey = privateKey.replace('_begin_', '-----BEGIN PRIVATE KEY-----'); + privateKey = privateKey.replace('_end_', '-----END PRIVATE KEY-----'); + const privateKeyFilePath = path.resolve(`keyfile-${uuidV4()}.pem`); + tasks.writeFile(privateKeyFilePath, privateKey); + return privateKeyFilePath; + } + + private setupBackend(backendServiceName: string) { + // Unfortunately this seems not to work with OCI provider for the tf statefile + // https://developer.hashicorp.com/terraform/language/settings/backends/configuration#command-line-key-value-pairs + //this.backendConfig.set('address', tasks.getInput("PAR url", true)); + //this.backendConfig.set('path', tasks.getInput("PAR path", true)); + //this.backendConfig.set('scheme', 'https'); + //PAR = OCI Object Storage preauthenticated request (for the statefile bucket) + + // Instead, will create a backend.tf config file for it in-flight when generate option was selected 'yes' (the default setting) + if(tasks.getInput("backendOCIBucketConfigGenerate", true) == 'yes') + { + tasks.debug('Generating backend tf statefile config.'); + var config = ""; + config = config + "terraform {\n backend \"http\" {\n"; + config = config + " address = \"" + tasks.getInput("backendOCIBucketPar", true) + "\"\n"; + config = config + " update_method = \"PUT\"\n }\n }\n"; + + const workingDirectory = tasks.getInput("workingDirectory"); + const tfConfigyFilePath = path.resolve(`${workingDirectory}/config-${uuidV4()}.tf`); + tasks.writeFile(tfConfigyFilePath, config); + tasks.debug('Generating backend tf statefile config done.'); + } + } + + public async handleBackend(terraformToolRunner: ToolRunner) : Promise { + let backendServiceName = tasks.getInput("backendServiceOCI", true); + this.setupBackend(backendServiceName); + + for (let [key, value] of this.backendConfig.entries()) { + terraformToolRunner.arg(`-backend-config=${key}=${value}`); + } + } + + public async handleProvider(command: TerraformAuthorizationCommandInitializer) : Promise { + if (command.serviceProvidername) { + let privateKeyFilePath = this.getPrivateKeyFilePath(tasks.getEndpointDataParameter(command.serviceProvidername, "privateKey", false)); + process.env['TF_VAR_tenancy_ocid'] = tasks.getEndpointDataParameter(command.serviceProvidername, "tenancy", false); + process.env['TF_VAR_user_ocid'] = tasks.getEndpointDataParameter(command.serviceProvidername, "user", false); + process.env['TF_VAR_region'] = tasks.getEndpointDataParameter(command.serviceProvidername, "region", false); + process.env['TF_VAR_fingerprint'] = tasks.getEndpointDataParameter(command.serviceProvidername, "fingerprint", false); + process.env['TF_VAR_private_key_path'] = `${privateKeyFilePath}`; + } + } +} \ No newline at end of file diff --git a/Tasks/TerraformTask/TerraformTaskV4/task.json b/Tasks/TerraformTask/TerraformTaskV4/task.json index 8718f08..622d161 100644 --- a/Tasks/TerraformTask/TerraformTaskV4/task.json +++ b/Tasks/TerraformTask/TerraformTaskV4/task.json @@ -53,6 +53,12 @@ "displayName": "Google Cloud Platform(GCP) backend configuration", "isExpanded": true, "visibleRule": "provider = gcp && command = init" + }, + { + "name": "backendOCI", + "displayName": "Oracle Cloud Infrastructure(OCI) backend configuration", + "isExpanded": true, + "visibleRule": "provider = oci && command = init" } ], "inputs": [ @@ -66,7 +72,8 @@ "options": { "azurerm": "azurerm", "aws": "aws", - "gcp": "gcp" + "gcp": "gcp", + "oci": "oci" }, "properties": { "EditableOptions": "False" @@ -175,6 +182,14 @@ "visibleRule": "provider = gcp && command != init && command != validate", "helpMarkDown": "Select a Google Cloud Platform connection for the deployment.

Note: If your connection is not listed or if you want to use an existing connection, you can setup a Google Cloud Platform service connection using the 'Add' or 'Manage' button." }, + { + "name": "environmentServiceNameOCI", + "type": "connectedService:OracleCloudServiceEndpoint", + "label": "Oracle Cloud Platform connection", + "required": true, + "visibleRule": "provider = oci && command != init && command != validate", + "helpMarkDown": "Select a Oracle Cloud Platform connection for the deployment.

Note: If your connection is not listed or if you want to use an existing connection, you can setup a Oracle Cloud Platform service connection using the 'Add' or 'Manage' button." + }, { "name": "backendAzureRmUseEnvironmentVariablesForAuthentication", "type": "boolean", @@ -297,6 +312,35 @@ "required": false, "helpMarkDown": "The relative path to the state file inside the GCP bucket. For example, if you give the input as 'terraform', then the state file, named default.tfstate, will be stored inside an object called terraform.", "groupName": "backendGCP" + }, + { + "name": "backendServiceOCI", + "type": "connectedService:OracleCloudServiceEndpoint", + "label": "Oracle Cloud Platform connection", + "required": true, + "helpMarkDown": "Oracle Cloud Platform connection for the terraform backend configuration.

Note: If your connection is not listed or if you want to use an existing connection, you can setup a Oracle Cloud Platform service connection using the 'Add' or 'Manage' button.", + "groupName": "backendOCI" + }, + { + "name": "backendOCIBucketPar", + "type": "string", + "label": "Bucket PAR for Terraform remote state file", + "required": false, + "helpMarkDown": "The OCI storage bucket PAR configuration for the Terraform remote state file (optional)", + "groupName": "backendOCI" + }, + { + "name": "backendOCIBucketConfigGenerate", + "type": "pickList", + "label": "Generate the Terraform remote state file config (Use Yes when not included in TF files)", + "required": true, + "defaultValue": "yes", + "helpMarkDown": "Generates the Terraform remote state file config, select Yes when not included in TF files, othwerwise No.", + "groupName": "backendOCI", + "options": { + "yes": "yes", + "no": "no" + } } ], "dataSourceBindings": [ diff --git a/azure-devops-extension.json b/azure-devops-extension.json index fc6839d..94f1cbf 100644 --- a/azure-devops-extension.json +++ b/azure-devops-extension.json @@ -19,6 +19,7 @@ "Azure", "AWS", "GCP", + "OCI", "Release", "DevOps" ], @@ -268,6 +269,96 @@ } ] } + }, + { + "id": "oracle-cloud-service-endpoint", + "description": "Credentials for connecting to Oracle Cloud Platform", + "type": "ms.vss-endpoint.service-endpoint-type", + "targets": [ + "ms.vss-endpoint.endpoint-types" + ], + "properties": { + "name": "OracleCloudServiceEndpoint", + "displayName": "OCI for Terraform", + "helpMarkDown": "", + "url": { + "displayName": "Server Url", + "helpText": "OCI homepage", + "value": "https://www.oracle.com/cloud/sign-in.html", + "isVisible": "false" + }, + "authenticationSchemes": [ + { + "id": "endpoint-auth-scheme-none", + "description": "OCI endpoint authentication scheme with no authentication.", + "type": "ms.vss-endpoint.endpoint-auth-scheme-none", + "targets": [ + "ms.vss-endpoint.endpoint-auth-schemes" + ], + "properties": { + "name": "None", + "displayName": "OCI No authentication for PAR url" + } + } + ], + "inputDescriptors": [ + { + "id": "user", + "name": "User OCID", + "description": "OCI user OCID", + "inputMode": "textbox", + "isConfidential": false, + "validation": { + "isRequired": true, + "dataType": "string" + } + }, + { + "id": "tenancy", + "name": "Tenancy OCID", + "description": "OCI tenancy OCID", + "inputMode": "textbox", + "isConfidential": true, + "validation": { + "isRequired": true, + "dataType": "string" + } + }, + { + "id": "region", + "name": "Region", + "description": "OCI region", + "inputMode": "textbox", + "isConfidential": false, + "validation": { + "isRequired": true, + "dataType": "string" + } + }, + { + "id": "fingerprint", + "name": "Key fingerprint", + "description": "The OCI private key fingerprint", + "inputMode": "textbox", + "isConfidential": false, + "validation": { + "isRequired": true, + "dataType": "string" + } + }, + { + "id": "privatekey", + "name": "Private key", + "description": "The OCI secret private key", + "inputMode": "passwordbox", + "isConfidential": true, + "validation": { + "isRequired": true, + "dataType": "string" + } + } + ] + } } ] }