From bc5d84aa22b37e794f3649d77624328b352c65e4 Mon Sep 17 00:00:00 2001 From: Mika Rinne Date: Thu, 5 Sep 2024 09:20:06 +0300 Subject: [PATCH 1/7] terraform for OCI --- .../src/base-terraform-command-handler.ts | 3 +- .../src/oci-terraform-command-handler.ts | 72 +++++++++++++++ Tasks/TerraformTask/TerraformTaskV4/task.json | 46 +++++++++- azure-devops-extension.json | 91 +++++++++++++++++++ 4 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 Tasks/TerraformTask/TerraformTaskV4/src/oci-terraform-command-handler.ts 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 12fa885..d3cd0be 100644 --- a/Tasks/TerraformTask/TerraformTaskV4/task.json +++ b/Tasks/TerraformTask/TerraformTaskV4/task.json @@ -50,6 +50,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": [ @@ -63,7 +69,8 @@ "options": { "azurerm": "azurerm", "aws": "aws", - "gcp": "gcp" + "gcp": "gcp", + "oci": "oci" }, "properties": { "EditableOptions": "False" @@ -172,6 +179,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", @@ -294,6 +309,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" + } + } + ] + } } ] } From cae7d1dbf1a0f6b3f1136e68dc9a0f59c5f95e96 Mon Sep 17 00:00:00 2001 From: Mika Rinne Date: Mon, 7 Oct 2024 12:37:45 +0300 Subject: [PATCH 2/7] fix --- Tasks/TerraformTask/TerraformTaskV4/src/parent-handler.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tasks/TerraformTask/TerraformTaskV4/src/parent-handler.ts b/Tasks/TerraformTask/TerraformTaskV4/src/parent-handler.ts index 5f94bba..0fb6bbd 100644 --- a/Tasks/TerraformTask/TerraformTaskV4/src/parent-handler.ts +++ b/Tasks/TerraformTask/TerraformTaskV4/src/parent-handler.ts @@ -2,6 +2,7 @@ import { BaseTerraformCommandHandler } from './base-terraform-command-handler'; import { TerraformCommandHandlerAzureRM } from './azure-terraform-command-handler'; import { TerraformCommandHandlerAWS } from './aws-terraform-command-handler'; import { TerraformCommandHandlerGCP } from './gcp-terraform-command-handler'; +import { TerraformCommandHandlerOCI } from './oci-terraform-command-handler'; export interface IParentCommandHandler { execute(providerName: string, command: string): Promise; @@ -24,6 +25,10 @@ export class ParentCommandHandler implements IParentCommandHandler { case "gcp": provider = new TerraformCommandHandlerGCP(); break; + + case "oci": + provider = new TerraformCommandHandlerOCI(); + break; } // Run the corrresponding command according to command name From 7f0eb3a275993c02387bc18d9d4f880454db29f3 Mon Sep 17 00:00:00 2001 From: Mika Rinne Date: Mon, 7 Oct 2024 13:49:11 +0300 Subject: [PATCH 3/7] Connection config input type fix --- azure-devops-extension.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-devops-extension.json b/azure-devops-extension.json index 94f1cbf..65a9c62 100644 --- a/azure-devops-extension.json +++ b/azure-devops-extension.json @@ -318,7 +318,7 @@ "name": "Tenancy OCID", "description": "OCI tenancy OCID", "inputMode": "textbox", - "isConfidential": true, + "isConfidential": false, "validation": { "isRequired": true, "dataType": "string" From a1001db1427eafe9bce90d5d23fa6b114bc74706 Mon Sep 17 00:00:00 2001 From: Mika Rinne Date: Mon, 7 Oct 2024 14:54:52 +0300 Subject: [PATCH 4/7] Update task.json --- Tasks/TerraformTask/TerraformTaskV4/task.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tasks/TerraformTask/TerraformTaskV4/task.json b/Tasks/TerraformTask/TerraformTaskV4/task.json index 622d161..2b23190 100644 --- a/Tasks/TerraformTask/TerraformTaskV4/task.json +++ b/Tasks/TerraformTask/TerraformTaskV4/task.json @@ -322,15 +322,15 @@ "groupName": "backendOCI" }, { - "name": "backendOCIBucketPar", + "name": "backendOCIPar", "type": "string", - "label": "Bucket PAR for Terraform remote state file", + "label": "PAR for Terraform remote state file", "required": false, - "helpMarkDown": "The OCI storage bucket PAR configuration for the Terraform remote state file (optional)", + "helpMarkDown": "The OCI object storage PAR configuration for the Terraform remote state file (optional)", "groupName": "backendOCI" }, { - "name": "backendOCIBucketConfigGenerate", + "name": "backendOCIConfigGenerate", "type": "pickList", "label": "Generate the Terraform remote state file config (Use Yes when not included in TF files)", "required": true, From ef58cb5a307266065293b0426aaec5c0618fecda Mon Sep 17 00:00:00 2001 From: Mika Rinne Date: Mon, 7 Oct 2024 14:55:46 +0300 Subject: [PATCH 5/7] Update oci-terraform-command-handler.ts --- .../TerraformTaskV4/src/oci-terraform-command-handler.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tasks/TerraformTask/TerraformTaskV4/src/oci-terraform-command-handler.ts b/Tasks/TerraformTask/TerraformTaskV4/src/oci-terraform-command-handler.ts index 4938c64..347f9d7 100644 --- a/Tasks/TerraformTask/TerraformTaskV4/src/oci-terraform-command-handler.ts +++ b/Tasks/TerraformTask/TerraformTaskV4/src/oci-terraform-command-handler.ts @@ -35,12 +35,12 @@ export class TerraformCommandHandlerOCI extends BaseTerraformCommandHandler { //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') + if(tasks.getInput("backendOCIConfigGenerate", 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 + " address = \"" + tasks.getInput("backendOCIPar", true) + "\"\n"; config = config + " update_method = \"PUT\"\n }\n }\n"; const workingDirectory = tasks.getInput("workingDirectory"); @@ -69,4 +69,4 @@ export class TerraformCommandHandlerOCI extends BaseTerraformCommandHandler { process.env['TF_VAR_private_key_path'] = `${privateKeyFilePath}`; } } -} \ No newline at end of file +} From be93cfa94a7c2f8cf302ab3b3fa66458e9d5ebd0 Mon Sep 17 00:00:00 2001 From: Mika Rinne Date: Mon, 7 Oct 2024 16:50:47 +0300 Subject: [PATCH 6/7] Update task.json renamed to OCIServiceEndpoint --- Tasks/TerraformTask/TerraformTaskV4/task.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tasks/TerraformTask/TerraformTaskV4/task.json b/Tasks/TerraformTask/TerraformTaskV4/task.json index 2b23190..41cf0e0 100644 --- a/Tasks/TerraformTask/TerraformTaskV4/task.json +++ b/Tasks/TerraformTask/TerraformTaskV4/task.json @@ -184,7 +184,7 @@ }, { "name": "environmentServiceNameOCI", - "type": "connectedService:OracleCloudServiceEndpoint", + "type": "connectedService:OCIServiceEndpoint", "label": "Oracle Cloud Platform connection", "required": true, "visibleRule": "provider = oci && command != init && command != validate", @@ -315,7 +315,7 @@ }, { "name": "backendServiceOCI", - "type": "connectedService:OracleCloudServiceEndpoint", + "type": "connectedService:OCIServiceEndpoint", "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.", From 4917d1e2d46c442e9d662ec9a663357c9c6f9439 Mon Sep 17 00:00:00 2001 From: Mika Rinne Date: Mon, 7 Oct 2024 16:51:39 +0300 Subject: [PATCH 7/7] Update azure-devops-extension.json renamed to OCIServiceEndpoint --- azure-devops-extension.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-devops-extension.json b/azure-devops-extension.json index 65a9c62..cc2b353 100644 --- a/azure-devops-extension.json +++ b/azure-devops-extension.json @@ -278,7 +278,7 @@ "ms.vss-endpoint.endpoint-types" ], "properties": { - "name": "OracleCloudServiceEndpoint", + "name": "OCIServiceEndpoint", "displayName": "OCI for Terraform", "helpMarkDown": "", "url": {