Skip to content

Commit 350fe1e

Browse files
Merge pull request #6 from Parfuemerie-Douglas/auth-token-refactor
Refactor reading of auth token and add pipeline URL as output
2 parents 48e59a8 + 746b4f0 commit 350fe1e

File tree

5 files changed

+156
-38
lines changed

5 files changed

+156
-38
lines changed

README.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
# scaffolder-backend-module-azure-pipelines
22

3-
Welcome to the Microsoft Azure pipelines actions for the `scaffolder-backend`.
3+
Welcome to the Microsoft Azure pipeline actions for the `scaffolder-backend`.
44

55
This plugin contains a collection of actions:
66

77
- `azure:pipeline:create`
88
- `azure:pipeline:run`
99
- `azure:pipeline:permit`
1010

11-
It utilizes Azure DevOps REST APIs to [create](https://docs.microsoft.com/en-us/rest/api/azure/devops/pipelines/pipelines/create?view=azure-devops-rest-6.1) and [run](https://docs.microsoft.com/en-us/rest/api/azure/devops/pipelines/runs/run-pipeline?view=azure-devops-rest-6.1) Azure pipelines.
11+
It utilizes Azure DevOps REST APIs to [create](https://docs.microsoft.com/en-us/rest/api/azure/devops/pipelines/pipelines/create?view=azure-devops-rest-6.1), [run](https://docs.microsoft.com/en-us/rest/api/azure/devops/pipelines/runs/run-pipeline?view=azure-devops-rest-6.1), and [authorize](https://docs.microsoft.com/en-us/rest/api/azure/devops/approvalsandchecks/pipeline-permissions/update-pipeline-permisions-for-resource?view=azure-devops-rest-7.1) Azure pipelines.
1212

1313
## Getting started
1414

@@ -33,13 +33,13 @@ Configure the actions (you can check the [docs](https://backstage.io/docs/featur
3333
import {
3434
createAzurePipelineAction,
3535
permitAzurePipelineAction,
36-
runAzurePipelineAction
37-
} from '@parfuemerie-douglas/scaffolder-backend-module-azure-pipelines';
36+
runAzurePipelineAction,
37+
} from "@parfuemerie-douglas/scaffolder-backend-module-azure-pipelines";
3838

3939
const actions = [
40-
createAzurePipelineAction(<azurePersonalAccessToken>),
41-
permitAzurePipelineAction(<azurePersonalAccessToken>),
42-
runAzurePipelineAction(<azurePersonalAccessToken>),
40+
createAzurePipelineAction({ integrations }),
41+
permitAzurePipelineAction({ integrations }),
42+
runAzurePipelineAction({ integrations }),
4343
...createBuiltInActions({
4444
containerRunner,
4545
catalogClient,
@@ -60,7 +60,18 @@ return await createRouter({
6060
});
6161
```
6262

63-
The Azure pipeline actions accepts an [Azure PAT (personal access token)](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate) parameter which should be a string. The PAT requires `Read & execute` permission for `Build` for the `azure:pipeline:create` and `azure:pipeline:run` actions. For the `azure:pipeline:permit` action the PAT requires `Read, query, & manage` permission for `Service Connections`. Simply replace `<azurePersonalAccessToken>` with your Azure PAT.
63+
The Azure pipeline actions use an [Azure PAT (personal access token)](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate) for authorization. The PAT requires `Read & execute` permission for `Build` for the `azure:pipeline:create` and `azure:pipeline:run` actions. For the `azure:pipeline:permit` action the PAT requires `Read, query, & manage` permission for `Service Connections`. Simply add the PAT to your `app-config.yaml`:
64+
65+
```yaml
66+
# app-config.yaml
67+
68+
integrations:
69+
azure:
70+
- host: dev.azure.com
71+
token: ${AZURE_TOKEN}
72+
```
73+
74+
Read more on integrations in Backstage in the [Integrations documentation](https://backstage.io/docs/integrations/).
6475
6576
## Using the template
6677
@@ -174,11 +185,13 @@ spec:
174185
links:
175186
- title: Repository
176187
url: ${{ steps.publish.output.remoteUrl }}
188+
- title: Pipeline
189+
url: ${{ steps.createAzurePipeline.output.pipelineUrl }}
177190
- title: Open in catalog
178191
icon: catalog
179192
entityRef: ${{ steps.register.output.entityRef }}
180193
```
181194
182195
**_Note_**: The `azure:pipeline:permit` action authorizes/unauthorizes a pipeline for a given resource. To authorize a pipeline for a [service endpoint](https://docs.microsoft.com/en-us/azure/virtual-network/virtual-network-service-endpoints-overview) set `resourceType` to `endpoint`, provide `resourceId` with the service endpoint ID (replace `<serviceEndpointId>` in the example code above), and set authorized to `true`.
183196

184-
You can also visit the `/create/actions` route in your Backstage application to find out more about the parameters these actions accepts when it's installed to configure how you like.
197+
You can find a list of all registred actions including their parameters at the `/create/actions` route in your Backstage application.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@parfuemerie-douglas/scaffolder-backend-module-azure-pipelines",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"description": "A collection of Backstage scaffolder backend modules for Azure pipelines.",
55
"main": "dist/index.cjs.js",
66
"types": "dist/index.d.ts",

src/actions/run/createAzurePipeline.ts

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,25 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { InputError } from "@backstage/errors";
18+
import { ScmIntegrationRegistry } from "@backstage/integration";
1719
import { createTemplateAction } from "@backstage/plugin-scaffolder-backend";
1820

1921
import fetch from "node-fetch";
2022

21-
export const createAzurePipelineAction = (azurePersonalAccessToken: string) => {
23+
export const createAzurePipelineAction = (options: {
24+
integrations: ScmIntegrationRegistry;
25+
}) => {
26+
const { integrations } = options;
27+
2228
return createTemplateAction<{
2329
organization: string;
2430
project: string;
2531
folder: string;
2632
name: string;
2733
repositoryId: string;
2834
repositoryName: string;
35+
token?: string;
2936
}>({
3037
id: "azure:pipeline:create",
3138
schema: {
@@ -70,37 +77,66 @@ export const createAzurePipelineAction = (azurePersonalAccessToken: string) => {
7077
title: "Repository Name",
7178
description: "The name of the repository.",
7279
},
80+
token: {
81+
title: "Authenticatino Token",
82+
type: "string",
83+
description: "The token to use for authorization.",
84+
},
7385
},
7486
},
7587
},
7688
async handler(ctx) {
89+
const {
90+
organization,
91+
project,
92+
folder,
93+
name,
94+
repositoryId,
95+
repositoryName,
96+
} = ctx.input;
97+
98+
const host = "dev.azure.com";
99+
const integrationConfig = integrations.azure.byHost(host);
100+
101+
if (!integrationConfig) {
102+
throw new InputError(
103+
`No matching integration configuration for host ${host}, please check your integrations config`
104+
);
105+
}
106+
107+
if (!integrationConfig.config.token && !ctx.input.token) {
108+
throw new InputError(`No token provided for Azure Integration ${host}`);
109+
}
110+
111+
const token = ctx.input.token ?? integrationConfig.config.token!;
112+
77113
ctx.logger.info(
78-
`Creating an Azure pipeline for the repository ${ctx.input.repositoryName} with the ID ${ctx.input.repositoryId}.`
114+
`Creating an Azure pipeline for the repository ${repositoryName} with the ID ${repositoryId}.`
79115
);
80116

81117
// See the Azure DevOps documentation for more information about the REST API:
82118
// https://docs.microsoft.com/en-us/rest/api/azure/devops/pipelines/pipelines/create?view=azure-devops-rest-6.1
83119
await fetch(
84-
`https://dev.azure.com/${ctx.input.organization}/${ctx.input.project}/_apis/pipelines?api-version=6.1-preview.1`,
120+
`https://dev.azure.com/${organization}/${project}/_apis/pipelines?api-version=6.1-preview.1`,
85121
{
86122
method: "POST",
87123
headers: {
88124
"Content-Type": "application/json",
89125
Accept: "application/json",
90-
Authorization: `Basic ${Buffer.from(
91-
`PAT:${azurePersonalAccessToken}`
92-
).toString("base64")}`,
126+
Authorization: `Basic ${Buffer.from(`PAT:${token}`).toString(
127+
"base64"
128+
)}`,
93129
"X-TFS-FedAuthRedirect": "Suppress",
94130
},
95131
body: JSON.stringify({
96-
folder: ctx.input.folder,
97-
name: ctx.input.name,
132+
folder: folder,
133+
name: name,
98134
configuration: {
99135
type: "yaml",
100136
path: "/azure-pipelines.yaml",
101137
repository: {
102-
id: ctx.input.repositoryId,
103-
name: ctx.input.repositoryName,
138+
id: repositoryId,
139+
name: repositoryName,
104140
type: "azureReposGit",
105141
},
106142
},
@@ -110,7 +146,7 @@ export const createAzurePipelineAction = (azurePersonalAccessToken: string) => {
110146
.then((response) => {
111147
if (response.ok) {
112148
ctx.logger.info(
113-
`Successfully created ${ctx.input.name} Azure pipeline in ${ctx.input.folder}.`
149+
`Successfully created ${name} Azure pipeline in ${folder}.`
114150
);
115151
} else {
116152
ctx.logger.error(
@@ -124,6 +160,7 @@ export const createAzurePipelineAction = (azurePersonalAccessToken: string) => {
124160
ctx.logger.info(`The Azure pipeline ID is ${data.id}.`);
125161

126162
ctx.output("pipelineId", data.id.toString());
163+
ctx.output("pipelineUrl", data._links.web.href);
127164
});
128165
},
129166
});

src/actions/run/permitAzurePipeline.ts

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,25 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { InputError } from "@backstage/errors";
18+
import { ScmIntegrationRegistry } from "@backstage/integration";
1719
import { createTemplateAction } from "@backstage/plugin-scaffolder-backend";
1820

1921
import fetch from "node-fetch";
2022

21-
export const permitAzurePipelineAction = (azurePersonalAccessToken: string) => {
23+
export const permitAzurePipelineAction = (options: {
24+
integrations: ScmIntegrationRegistry;
25+
}) => {
26+
const { integrations } = options;
27+
2228
return createTemplateAction<{
2329
organization: string;
2430
project: string;
2531
resourceId: string;
2632
resourceType: string;
2733
authorized: boolean;
2834
pipelineId: string;
35+
token?: string;
2936
}>({
3037
id: "azure:pipeline:permit",
3138
schema: {
@@ -60,44 +67,78 @@ export const permitAzurePipelineAction = (azurePersonalAccessToken: string) => {
6067
title: "Resource Type",
6168
description: "The type of the resource (e.g. endpoint).",
6269
},
70+
authorized: {
71+
type: "boolean",
72+
title: "Authorized",
73+
description: "A true or false authorization indicator.",
74+
},
6375
pipelineId: {
6476
type: "string",
6577
title: "Pipeline ID",
6678
description: "The pipeline ID.",
6779
},
80+
token: {
81+
title: "Authenticatino Token",
82+
type: "string",
83+
description: "The token to use for authorization.",
84+
},
6885
},
6986
},
7087
},
7188
async handler(ctx) {
89+
const {
90+
organization,
91+
project,
92+
resourceId,
93+
resourceType,
94+
authorized,
95+
pipelineId,
96+
} = ctx.input;
97+
98+
const host = "dev.azure.com";
99+
const integrationConfig = integrations.azure.byHost(host);
100+
101+
if (!integrationConfig) {
102+
throw new InputError(
103+
`No matching integration configuration for host ${host}, please check your integrations config`
104+
);
105+
}
106+
107+
if (!integrationConfig.config.token && !ctx.input.token) {
108+
throw new InputError(`No token provided for Azure Integration ${host}`);
109+
}
110+
111+
const token = ctx.input.token ?? integrationConfig.config.token!;
112+
72113
if (ctx.input.authorized == true) {
73114
ctx.logger.info(
74-
`Authorizing Azure pipeline with ID ${ctx.input.pipelineId} for ${ctx.input.resourceType} with ID ${ctx.input.resourceId}.`
115+
`Authorizing Azure pipeline with ID ${pipelineId} for ${resourceType} with ID ${resourceId}.`
75116
);
76117
} else {
77118
ctx.logger.info(
78-
`Unauthorizing Azure pipeline with ID ${ctx.input.pipelineId} for ${ctx.input.resourceType} with ID ${ctx.input.resourceId}.`
119+
`Unauthorizing Azure pipeline with ID ${pipelineId} for ${resourceType} with ID ${resourceId}.`
79120
);
80121
}
81122

82123
// See the Azure DevOps documentation for more information about the REST API:
83124
// https://docs.microsoft.com/en-us/rest/api/azure/devops/approvalsandchecks/pipeline-permissions/update-pipeline-permisions-for-resource?view=azure-devops-rest-7.1
84125
await fetch(
85-
`https://dev.azure.com/${ctx.input.organization}/${ctx.input.project}/_apis/pipelines/pipelinepermissions/${ctx.input.resourceType}/${ctx.input.resourceId}?api-version=7.1-preview.1`,
126+
`https://dev.azure.com/${organization}/${project}/_apis/pipelines/pipelinepermissions/${resourceType}/${resourceId}?api-version=7.1-preview.1`,
86127
{
87128
method: "PATCH",
88129
headers: {
89130
"Content-Type": "application/json",
90131
Accept: "application/json",
91-
Authorization: `Basic ${Buffer.from(
92-
`PAT:${azurePersonalAccessToken}`
93-
).toString("base64")}`,
132+
Authorization: `Basic ${Buffer.from(`PAT:${token}`).toString(
133+
"base64"
134+
)}`,
94135
"X-TFS-FedAuthRedirect": "Suppress",
95136
},
96137
body: JSON.stringify({
97138
pipelines: [
98139
{
99-
authorized: ctx.input.authorized,
100-
id: parseInt(ctx.input.pipelineId),
140+
authorized: authorized,
141+
id: parseInt(pipelineId),
101142
},
102143
],
103144
}),

src/actions/run/runAzurePipeline.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,22 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { InputError } from "@backstage/errors";
18+
import { ScmIntegrationRegistry } from "@backstage/integration";
1719
import { createTemplateAction } from "@backstage/plugin-scaffolder-backend";
1820

1921
import fetch from "node-fetch";
2022

21-
export const runAzurePipelineAction = (azurePersonalAccessToken: string) => {
23+
export const runAzurePipelineAction = (options: {
24+
integrations: ScmIntegrationRegistry;
25+
}) => {
26+
const { integrations } = options;
27+
2228
return createTemplateAction<{
2329
organization: string;
2430
pipelineId: string;
2531
project: string;
32+
token?: string;
2633
}>({
2734
id: "azure:pipeline:run",
2835
schema: {
@@ -45,26 +52,46 @@ export const runAzurePipelineAction = (azurePersonalAccessToken: string) => {
4552
title: "Project",
4653
description: "The name of the Azure project.",
4754
},
55+
token: {
56+
title: "Authenticatino Token",
57+
type: "string",
58+
description: "The token to use for authorization.",
59+
},
4860
},
4961
},
5062
},
5163
async handler(ctx) {
52-
ctx.logger.info(
53-
`Running Azure pipeline with the ID ${ctx.input.pipelineId}.`
54-
);
64+
const { organization, pipelineId, project } = ctx.input;
65+
66+
const host = "dev.azure.com";
67+
const integrationConfig = integrations.azure.byHost(host);
68+
69+
if (!integrationConfig) {
70+
throw new InputError(
71+
`No matching integration configuration for host ${host}, please check your integrations config`
72+
);
73+
}
74+
75+
if (!integrationConfig.config.token && !ctx.input.token) {
76+
throw new InputError(`No token provided for Azure Integration ${host}`);
77+
}
78+
79+
const token = ctx.input.token ?? integrationConfig.config.token!;
80+
81+
ctx.logger.info(`Running Azure pipeline with the ID ${pipelineId}.`);
5582

5683
// See the Azure DevOps documentation for more information about the REST API:
5784
// https://docs.microsoft.com/en-us/rest/api/azure/devops/pipelines/runs/run-pipeline?view=azure-devops-rest-6.1
5885
await fetch(
59-
`https://dev.azure.com/${ctx.input.organization}/${ctx.input.project}/_apis/pipelines/${ctx.input.pipelineId}/runs?api-version=6.1-preview.1`,
86+
`https://dev.azure.com/${organization}/${project}/_apis/pipelines/${pipelineId}/runs?api-version=6.1-preview.1`,
6087
{
6188
method: "POST",
6289
headers: {
6390
"Content-Type": "application/json",
6491
Accept: "application/json",
65-
Authorization: `Basic ${Buffer.from(
66-
`PAT:${azurePersonalAccessToken}`
67-
).toString("base64")}`,
92+
Authorization: `Basic ${Buffer.from(`PAT:${token}`).toString(
93+
"base64"
94+
)}`,
6895
"X-TFS-FedAuthRedirect": "Suppress",
6996
},
7097
body: JSON.stringify({

0 commit comments

Comments
 (0)