Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Added tenant scope deployments for the ARM Deploy action (#125)
* Basic tenant scope logic added.

* Added positive and negative test cases.

* Added tenant scope to descriptions.
  • Loading branch information
bearmannl committed Jan 22, 2024
1 parent ba158a2 commit 95b309a
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 6 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/ci-workflow.yml
Expand Up @@ -80,6 +80,28 @@ jobs:
EXPECTED_TO: fail
run: ts-node test/main.tests.ts

- name: Tenant scope test
env:
INPUT_SCOPE: tenant
INPUT_SUBSCRIPTIONID: ${{ secrets.SUBSCRIPTION_ID }}
INPUT_REGION: centralus
INPUT_TEMPLATE: ./test/tenant/template.json
INPUT_PARAMETERS: ./test/tenant/parameters.json
INPUT_DEPLOYMENTNAME: github-test-ten
EXPECTED_TO: pass
run: ts-node test/main.tests.ts

- name: Tenant scope test - Negative
env:
INPUT_SCOPE: tenant
INPUT_SUBSCRIPTIONID: ${{ secrets.SUBSCRIPTION_ID }}
INPUT_REGION: centralus
INPUT_TEMPLATE: ./test/tenant/negative/template.json
INPUT_PARAMETERS: ./test/tenant/negative/parameters.json
INPUT_DEPLOYMENTNAME: github-test-ten
EXPECTED_TO: fail
run: ts-node test/main.tests.ts

- name: Validate mode test
env:
INPUT_SCOPE: resourcegroup
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/edge-build-ci-workflow.yml
Expand Up @@ -82,6 +82,28 @@ jobs:
EXPECTED_TO: fail
run: ts-node test/main.tests.ts

- name: Tenant scope test
env:
INPUT_SCOPE: tenant
INPUT_SUBSCRIPTIONID: ${{ secrets.SUBSCRIPTION_ID }}
INPUT_REGION: centralus
INPUT_TEMPLATE: ./test/tenant/template.json
INPUT_PARAMETERS: ./test/tenant/parameters.json
INPUT_DEPLOYMENTNAME: github-test-eb-ten
EXPECTED_TO: pass
run: ts-node test/main.tests.ts

- name: Tenant scope test - Negative
env:
INPUT_SCOPE: tenant
INPUT_SUBSCRIPTIONID: ${{ secrets.SUBSCRIPTION_ID }}
INPUT_REGION: centralus
INPUT_TEMPLATE: ./test/tenant/negative/template.json
INPUT_PARAMETERS: ./test/tenant/negative/parameters.json
INPUT_DEPLOYMENTNAME: github-test-eb-ten
EXPECTED_TO: fail
run: ts-node test/main.tests.ts

- name: Validate mode test
env:
INPUT_SCOPE: resourcegroup
Expand Down
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -13,11 +13,11 @@ By default, the action only parses the output and does not print them out. In or

## Inputs

* `scope`: Provide the scope of the deployment. Valid values are: `resourcegroup`(default) , `subscription`, `managementgroup`.
* `scope`: Provide the scope of the deployment. Valid values are: `resourcegroup`(default) , `tenant`, `subscription`, `managementgroup`.
* `resourceGroupName`: **Conditional** Provide the name of a resource group. Only required for Resource Group Scope
* `subscriptionId`: **Conditional** Provide a value to override the subscription ID set by [Azure Login](https://github.com/Azure/login).
* `managementGroupId`: **Conditional** Specify the Management Group ID, only required for Management Group Deployments.
* `region`: **Conditional** Provide the target region, only required for Management Group or Subscription deployments.
* `region`: **Conditional** Provide the target region, only required for Tenant, Management Group or Subscription deployments.
* `template`: **Required** Specify the path or URL to the Azure Resource Manager template.
* `parameters`: Specify the path or URL to the Azure Resource Manager deployment parameter values file (local / remote) and/or specify local overrides.
* `deploymentMode`: `Incremental`(default) (only add resources to resource group) or `Complete` (remove extra resources from resource group) or `Validate` (only validates the template).
Expand Down
4 changes: 2 additions & 2 deletions action.yml
Expand Up @@ -2,7 +2,7 @@ name: "Deploy Azure Resource Manager (ARM) Template"
description: "Use this GitHub Action task to deploy Azure Resource Manager (ARM) template"
inputs:
scope:
description: "Provide the scope of the deployment. Valid values are: 'resourcegroup', 'managementgroup', 'subscription'"
description: "Provide the scope of the deployment. Valid values are: 'resourcegroup', 'tenant', 'managementgroup', 'subscription'"
required: true
subscriptionId:
description: "Override the Subscription Id set by Azure Login."
Expand All @@ -11,7 +11,7 @@ inputs:
description: "Specify the Id for the Management Group, only required for Management Group Deployments."
required: false
region:
description: "Provide the target region, only required for management Group or Subscription deployments."
description: "Provide the target region, only required for tenant, management Group or Subscription deployments."
required: false
resourceGroupName:
description: "Provide the name of a resource group, only required for resource Group deployments."
Expand Down
91 changes: 91 additions & 0 deletions src/deploy/scope_tenant.ts
@@ -0,0 +1,91 @@
import { exec } from '@actions/exec';
import { ExecOptions } from '@actions/exec/lib/interfaces';
import { ParseOutputs, Outputs } from '../utils/utils';
import * as core from '@actions/core';

export async function DeployTenantScope(azPath: string, region: string, template: string, deploymentMode: string, deploymentName: string, parameters: string, failOnStdErr: Boolean, additionalArguments: String): Promise<Outputs> {
// Check if region is set
if (!region) {
throw Error("Region must be set.")
}

// check if mode is set as this will be ignored
if (deploymentMode && deploymentMode != "validate") {
core.warning("This deployment mode is not supported for tenant scoped deployments, this parameter will be ignored!")
}
// create the parameter list
const validateParameters = [
region ? `--location "${region}"` : undefined,
template ?
template.startsWith("http") ? `--template-uri ${template}` : `--template-file ${template}`
: undefined,
deploymentName ? `--name "${deploymentName}"` : undefined,
parameters ? `--parameters ${parameters}` : undefined
].filter(Boolean).join(' ');

let azDeployParameters = validateParameters;
if(additionalArguments){
azDeployParameters += ` ${additionalArguments}`;
}

// configure exec to write the json output to a buffer
let commandOutput = '';
let commandStdErr = false;
const deployOptions: ExecOptions = {
silent: true,
ignoreReturnCode: true,
failOnStdErr: false,
listeners: {
stderr: (data: BufferSource) => {
let error = data.toString();
if(error && error.trim().length !== 0)
{
commandStdErr = true;
core.error(error);
}
},
stdout: (data: BufferSource) => {
commandOutput += data.toString();
},
debug: (data: string) => {
core.debug(data);
}
}
}
const validateOptions: ExecOptions = {
silent: true,
ignoreReturnCode: true,
listeners: {
stderr: (data: BufferSource) => {
core.warning(data.toString());
},
}
}

// validate the deployment
core.info("Validating template...")
var code = await exec(`"${azPath}" deployment tenant validate ${validateParameters} -o json`, [], validateOptions);
if (deploymentMode === "validate" && code != 0) {
throw new Error("Template validation failed.")
} else if (code != 0) {
core.warning("Template validation failed.")
}

if (deploymentMode != "validate") {
// execute the deployment
core.info("Creating deployment...")
var deploymentCode = await exec(`"${azPath}" deployment tenant create ${azDeployParameters} -o json`, [], deployOptions);

if (deploymentCode != 0) {
throw new Error("Deployment failed.")
}
if(commandStdErr && failOnStdErr) {
throw new Error("Deployment process failed as some lines were written to stderr");
}

core.debug(commandOutput);
core.info("Parsing outputs...")
return ParseOutputs(commandOutput)
}
return {}
}
8 changes: 6 additions & 2 deletions src/main.ts
Expand Up @@ -2,6 +2,7 @@ import { getBooleanInput, info, getInput } from '@actions/core';
import { which } from '@actions/io';
import { DeployResourceGroupScope } from './deploy/scope_resourcegroup';
import { exec } from '@actions/exec';
import { DeployTenantScope } from './deploy/scope_tenant';
import { DeployManagementGroupScope } from './deploy/scope_managementgroup';
import { DeploySubscriptionScope } from './deploy/scope_subscription';
import { Outputs } from './utils/utils';
Expand Down Expand Up @@ -31,7 +32,7 @@ export async function main(): Promise<Outputs> {
}

// change the subscription context
if (scope !== "managementgroup" && subscriptionId !== "") {
if (scope !== "tenant" && scope !== "managementgroup" && subscriptionId !== "") {
info("Changing subscription context...")
await exec(`"${azPath}" account set --subscription ${subscriptionId}`, [], { silent: true })
}
Expand All @@ -42,14 +43,17 @@ export async function main(): Promise<Outputs> {
case "resourcegroup":
result = await DeployResourceGroupScope(azPath, resourceGroupName, template, deploymentMode, deploymentName, parameters, failOnStdErr, additionalArguments)
break
case "tenant":
result = await DeployTenantScope(azPath, region, template, deploymentMode, deploymentName, parameters, failOnStdErr, additionalArguments)
break
case "managementgroup":
result = await DeployManagementGroupScope(azPath, region, template, deploymentMode, deploymentName, parameters, managementGroupId, failOnStdErr, additionalArguments)
break
case "subscription":
result = await DeploySubscriptionScope(azPath, region, template, deploymentMode, deploymentName, parameters, failOnStdErr, additionalArguments)
break
default:
throw new Error("Invalid scope. Valid values are: 'resourcegroup', 'managementgroup', 'subscription'")
throw new Error("Invalid scope. Valid values are: 'resourcegroup', 'tenant', 'managementgroup', 'subscription'")
}

return result
Expand Down
12 changes: 12 additions & 0 deletions test/tenant/negative/parameters.json
@@ -0,0 +1,12 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"mgName": {
"value": "mg-test01"
},
"mgDisplayName": {
"value": "Test Management Group"
}
}
}
24 changes: 24 additions & 0 deletions test/tenant/negative/template.json
@@ -0,0 +1,24 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"mgName": {
"type": "int",
"defaultValue": "[concat('mg-', uniqueString(newGuid()))]"
},
"mgDisplayName": {
"type": "int"
}
},
"resources": [
{
"type": "Microsoft.Management/managementGroups",
"apiVersion": "2021-04-01",
"name": "[parameters('mgName')]",
"properties": {
"displayName": "[parameters('mgDisplayName')]"
}
}
],
"outputs": {}
}
12 changes: 12 additions & 0 deletions test/tenant/parameters.json
@@ -0,0 +1,12 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"mgName": {
"value": "mg-test01"
},
"mgDisplayName": {
"value": "Test Management Group"
}
}
}
24 changes: 24 additions & 0 deletions test/tenant/template.json
@@ -0,0 +1,24 @@
{
"$schema": "https://schema.management.azure.com/schemas/2019-08-01/tenantDeploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"mgName": {
"type": "string",
"defaultValue": "[concat('mg-', uniqueString(newGuid()))]"
},
"mgDisplayName": {
"type": "string"
}
},
"resources": [
{
"type": "Microsoft.Management/managementGroups",
"apiVersion": "2021-04-01",
"name": "[parameters('mgName')]",
"properties": {
"displayName": "[parameters('mgDisplayName')]"
}
}
],
"outputs": {}
}

0 comments on commit 95b309a

Please sign in to comment.