Skip to content
This repository was archived by the owner on Apr 13, 2020. It is now read-only.

Commit 7190967

Browse files
evanlouiemtarng
authored andcommitted
Added source to ACR workflow instructions (#103)
This PR focuses around ironing out the flow of running `spk project init` or `spk project init -m`, creating pipelines via the generated `azure-pipelines.yaml` files, and ensuring the pipeline can create appropriately named/tagged docker images in ACR. In order to allow for flexibility in how SRE's choose to deploy the pipelines, this PR does not automate the actual deployment of the pipeline. Instead, `info` level instructions have been added specifying how to run the `spk service create-pipeline` to deploy the `azure-pipelines.yaml` generated; If the the user has configured the spk config correctly, the outputted command is copy/paste ready to deploy the generated pipelines yaml: If ran on a mono-repo: `spk service create-pipeline --project-dir packages my-service` will be logged. If ran on a standard repo: `spk service create-pipeline my-service` will be logged. The `azure-pipelines.yaml` expects 4 environment variables to be present in order to login to `az` cli and build and push an image to ACR: - `ACR_NAME` - name of the target ACR - `SP_APP_ID` - service principal ID - `SP_PASS` - service principal password - `SP_TENANT` - service principal tenant These requirements are also `info` logged to the end user upon `azure-pipelines.yaml` generation. Previously `spk service create-pipeline` only worked for mono-repositories and the code has been refactored such that the existence of the `--packages-dir` options implies whether or not the repository is a mono-repository or a standard one. closes microsoft/bedrock#646
1 parent 0e77d21 commit 7190967

File tree

9 files changed

+227
-108
lines changed

9 files changed

+227
-108
lines changed

docs/service-management.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ Global options:
3333
Add a new service into this initialized spk project repository
3434

3535
```
36-
Usage:
37-
spk service create|c [options] <service-name>
36+
Usage: service create|c [options] <service-name>
37+
38+
Add a new service into this initialized spk project repository
3839
3940
Options:
4041
-c, --helm-chart-chart <helm-chart> bedrock helm chart name. --helm-chart-* and --helm-config-* are exclusive; you may only use one. (default: "")
@@ -61,7 +62,7 @@ Configure Azure DevOps for a bedrock managed service.
6162
```
6263
Usage: service create-pipeline|p [options] <service-name>
6364
64-
Configure Azure DevOps for a bedrock managed service
65+
Configure Azure DevOps for a bedrock managed service.
6566
6667
Options:
6768
-n, --pipeline-name <pipeline-name> Name of the pipeline to be created
@@ -70,7 +71,7 @@ Options:
7071
-r, --repo-name <repo-name> Repository Name in Azure DevOps
7172
-u, --repo-url <repo-url> Repository URL
7273
-d, --devops-project <devops-project> Azure DevOps Project
73-
-l, --packages-dir <packages-dir> The monorepository directory containing this service definition. ie. '--packages-dir packages' if my-service is located under ./packages/my-service.
74+
-l, --packages-dir <packages-dir> The mono-repository directory containing this service definition. ie. '--packages-dir packages' if my-service is located under ./packages/my-service. Omitting this option implies this is a not a mono-repository.
7475
-h, --help output usage information
7576
```
7677

src/commands/hld/pipeline.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const installHldToManifestPipelineDecorator = (
6363
);
6464
} catch (err) {
6565
logger.error(
66-
`Error occured installing pipeline for HLD to Manifest pipeline`
66+
`Error occurred installing pipeline for HLD to Manifest pipeline`
6767
);
6868
logger.error(err);
6969
process.exit(1);

src/commands/service/create.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import commander from "commander";
22
import path from "path";
33
import shelljs from "shelljs";
4-
import { logger } from "../../logger";
5-
64
import {
75
addNewServiceToBedrockFile,
86
addNewServiceToMaintainersFile,
@@ -11,6 +9,7 @@ import {
119
generateStarterAzurePipelinesYaml
1210
} from "../../lib/fileutils";
1311
import { checkoutCommitPushCreatePRLink } from "../../lib/gitutils";
12+
import { logger } from "../../logger";
1413
import { IHelmConfig, IUser } from "../../types";
1514

1615
/**

src/commands/service/pipeline.ts

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { IBuildApi } from "azure-devops-node-api/BuildApi";
2-
import commander = require("commander");
2+
import { BuildDefinition } from "azure-devops-node-api/interfaces/BuildInterfaces";
3+
import commander from "commander";
34
import path from "path";
45
import { Config } from "../../config";
56
import {
@@ -15,15 +16,13 @@ import {
1516
} from "../../lib/pipelines/pipelines";
1617
import { logger } from "../../logger";
1718

18-
import { BuildDefinition } from "azure-devops-node-api/interfaces/BuildInterfaces";
19-
2019
export const createPipelineCommandDecorator = (
2120
command: commander.Command
2221
): void => {
2322
command
2423
.command("create-pipeline <service-name>")
2524
.alias("p")
26-
.description("Configure Azure DevOps for a bedrock managed service")
25+
.description("Configure Azure DevOps for a bedrock managed service.")
2726
.option(
2827
"-n, --pipeline-name <pipeline-name>",
2928
"Name of the pipeline to be created"
@@ -38,7 +37,7 @@ export const createPipelineCommandDecorator = (
3837
.option("-d, --devops-project <devops-project>", "Azure DevOps Project")
3938
.option(
4039
"-l, --packages-dir <packages-dir>",
41-
"The monorepository directory containing this service definition. ie. '--packages-dir packages' if my-service is located under ./packages/my-service."
40+
"The mono-repository directory containing this service definition. ie. '--packages-dir packages' if my-service is located under ./packages/my-service. Omitting this option implies this is a not a mono-repository."
4241
)
4342
.action(async (serviceName, opts) => {
4443
const gitOriginUrl = await getOriginUrl();
@@ -49,7 +48,7 @@ export const createPipelineCommandDecorator = (
4948
personalAccessToken = azure_devops && azure_devops.access_token,
5049
devopsProject = azure_devops && azure_devops.project,
5150
pipelineName = serviceName + "-pipeline",
52-
packagesDir = "./",
51+
packagesDir, // allow to be undefined in the case of a mono-repo
5352
repoName = getRepositoryName(gitOriginUrl),
5453
repoUrl = getRepositoryUrl(gitOriginUrl)
5554
} = opts;
@@ -98,12 +97,6 @@ export const createPipelineCommandDecorator = (
9897
`--devops-project must be of type 'string', ${typeof devopsProject} given.`
9998
);
10099
}
101-
102-
if (typeof packagesDir !== "string") {
103-
throw new Error(
104-
`--packages-dir must be of type 'string', ${typeof packagesDir} given.`
105-
);
106-
}
107100
} catch (err) {
108101
logger.error(`Error occurred validating inputs for ${serviceName}`);
109102
logger.error(err);
@@ -123,7 +116,7 @@ export const createPipelineCommandDecorator = (
123116
process.exit
124117
);
125118
} catch (err) {
126-
logger.error(`Error occured installing pipeline for ${serviceName}`);
119+
logger.error(`Error occurred installing pipeline for ${serviceName}`);
127120
logger.error(err);
128121
process.exit(1);
129122
}
@@ -132,27 +125,30 @@ export const createPipelineCommandDecorator = (
132125

133126
/**
134127
* Install a pipeline for the service in an azure devops org.
135-
* @param serviceName
128+
*
129+
* @param serviceName Name of the service this pipeline belongs to; this is only used when `packagesDir` is defined as a means to locate the azure-pipelines.yaml file
136130
* @param orgName
137131
* @param personalAccessToken
138132
* @param pipelineName
139-
* @param repoName
140-
* @param repoUrl
133+
* @param repositoryName
134+
* @param repositoryUrl
141135
* @param project
136+
* @param packagesDir The directory containing the services for a mono-repo. If undefined; implies that we are operating on a standard service repository
137+
* @param exitFn
142138
*/
143139
export const installPipeline = async (
144140
serviceName: string,
145141
orgName: string,
146142
personalAccessToken: string,
147143
pipelineName: string,
148-
repoName: string,
149-
repoUrl: string,
144+
repositoryName: string,
145+
repositoryUrl: string,
150146
project: string,
151-
packagesDir: string,
147+
packagesDir: string | undefined,
152148
exitFn: (status: number) => void
153149
) => {
154-
let devopsClient;
155-
let builtDefinition;
150+
let devopsClient: IBuildApi | undefined;
151+
let builtDefinition: BuildDefinition | undefined;
156152

157153
try {
158154
devopsClient = await getBuildApiClient(orgName, personalAccessToken);
@@ -166,16 +162,23 @@ export const installPipeline = async (
166162
branchFilters: ["master"],
167163
maximumConcurrentBuilds: 1,
168164
/* tslint:disable-next-line object-literal-shorthand */
169-
pipelineName: pipelineName,
170-
repositoryName: repoName,
171-
repositoryUrl: repoUrl,
165+
pipelineName,
166+
repositoryName,
167+
repositoryUrl,
172168
yamlFileBranch: "master",
173-
yamlFilePath: path.join(packagesDir, serviceName, "azure-pipelines.yaml") // This may not work if we're using a non-mono repository and azure-pipelines.yaml is in the root directory.
169+
yamlFilePath: packagesDir // if a packages dir is supplied, its a mono-repo
170+
? path.join(packagesDir, serviceName, "azure-pipelines.yaml") // if a packages dir is supplied, its a mono-repo; concat <packages-dir>/<service-name>
171+
: "azure-pipelines.yaml" // if no packages dir, its a standard repo; so the azure-pipelines.yaml is in the root
174172
});
175173

176174
try {
175+
logger.debug(
176+
`Creating pipeline for project '${project}' with definition '${JSON.stringify(
177+
definition
178+
)}'`
179+
);
177180
builtDefinition = await createPipelineForDefinition(
178-
devopsClient as IBuildApi,
181+
devopsClient,
179182
project,
180183
definition
181184
);
@@ -184,16 +187,18 @@ export const installPipeline = async (
184187
logger.error(err);
185188
return exitFn(1);
186189
}
190+
if (typeof builtDefinition.id === "undefined") {
191+
const builtDefnString = JSON.stringify(builtDefinition);
192+
throw Error(
193+
`Invalid BuildDefinition created, parameter 'id' is missing from ${builtDefnString}`
194+
);
195+
}
187196

188197
logger.info(`Created pipeline for ${pipelineName}`);
189-
logger.info(`Pipeline ID: ${(builtDefinition as BuildDefinition).id}`);
198+
logger.info(`Pipeline ID: ${builtDefinition.id}`);
190199

191200
try {
192-
await queueBuild(
193-
devopsClient as IBuildApi,
194-
project,
195-
(builtDefinition as BuildDefinition).id as number
196-
);
201+
await queueBuild(devopsClient, project, builtDefinition.id);
197202
} catch (err) {
198203
logger.error(`Error occurred when queueing build for ${pipelineName}`);
199204
logger.error(err);

src/commands/variable-group/create.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { VariableGroup } from "azure-devops-node-api/interfaces/ReleaseInterfaces";
22
import commander from "commander";
33
import fs from "fs";
4+
import path from "path";
45
import { readYaml } from "../../config";
56
import { IAzureDevOpsOpts } from "../../lib/git";
67
import {
@@ -102,11 +103,13 @@ export const create = async (
102103
filepath: string,
103104
accessOpts: IAzureDevOpsOpts
104105
) => {
105-
logger.info("Creating variale group");
106+
logger.info(
107+
`Creating Variable Group from group definition '${path.resolve(filepath)}'`
108+
);
106109
try {
107110
fs.statSync(filepath);
108111
const data = readYaml<IVariableGroupData>(filepath);
109-
logger.debug(`Varible Group Yaml data: ${JSON.stringify(data)}`);
112+
logger.debug(`Variable Group Yaml data: ${JSON.stringify(data)}`);
110113

111114
// validate variable group type
112115

@@ -118,7 +121,7 @@ export const create = async (
118121
variableGroup = await addVariableGroup(data, accessOpts);
119122
} else {
120123
throw new Error(
121-
`Varible Group type "${data.type}" is not supported. Only "Vsts" and "AzureKeyVault" are valid types and case sensitive.`
124+
`Variable Group type "${data.type}" is not supported. Only "Vsts" and "AzureKeyVault" are valid types and case sensitive.`
122125
);
123126
}
124127
} catch (err) {

src/lib/fileutils.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,14 @@ export const starterAzurePipelines = async (opts: {
8989
vmImage?: string;
9090
branches?: string[];
9191
variableGroups?: string[];
92+
variables?: Array<{ name: string; value: string }>;
9293
}): Promise<IAzurePipelinesYaml> => {
9394
const {
9495
relProjectPaths = ["."],
9596
vmImage = "ubuntu-latest",
9697
branches = ["master"],
97-
variableGroups = []
98+
variableGroups = [],
99+
variables = []
98100
} = opts;
99101

100102
// Ensure any blank paths are turned into "./"
@@ -109,20 +111,11 @@ export const starterAzurePipelines = async (opts: {
109111
branches: { include: branches },
110112
paths: { include: cleanedPaths }
111113
},
112-
variables: [...variableGroups.map(group => ({ group }))],
114+
variables: [...variableGroups.map(group => ({ group })), ...variables],
113115
pool: {
114116
vmImage
115117
},
116118
steps: [
117-
{
118-
script: generateYamlScript([
119-
`printenv | sort`,
120-
`pwd`,
121-
`ls -la`,
122-
`echo "The name of this service is: $(BUILD.BUILDNUMBER)"`
123-
]),
124-
displayName: "Run a multi-line script"
125-
},
126119
{
127120
script: generateYamlScript([
128121
`echo "az login --service-principal --username $(SP_APP_ID) --password $(SP_PASS) --tenant $(SP_TENANT)"`,
@@ -131,23 +124,56 @@ export const starterAzurePipelines = async (opts: {
131124
displayName: "Azure Login"
132125
},
133126
...cleanedPaths.map(projectPath => {
127+
const projectPathParts = projectPath
128+
.split(path.sep)
129+
.filter(p => p !== "");
130+
// If a the projectPath contains more than 1 segment (a service in a
131+
// mono-repo), use the last part as the project name as it will the
132+
// folder containing the Dockerfile. Otherwise, its a standard service
133+
// and does not need a a project name
134+
const projectName =
135+
projectPathParts.length > 1
136+
? "-" + projectPathParts.slice(-1)[0]
137+
: "";
134138
return {
135139
script: generateYamlScript([
136140
`cd ${projectPath} # Need to make sure Build.DefinitionName matches directory. It's case sensitive`,
137141
`echo "az acr build -r $(ACR_NAME) --image $(Build.DefinitionName):$(build.SourceBranchName)-$(build.BuildId) ."`,
138-
`az acr build -r $(ACR_NAME) --image $(Build.DefinitionName):$(build.SourceBranchName)-$(build.BuildId) .`
142+
`az acr build -r $(ACR_NAME) --image $(Build.DefinitionName)${projectName}:$(build.SourceBranchName)-$(build.BuildId) .`
139143
]),
140144
displayName: "ACR Build and Publish"
141145
};
142-
}),
143-
{
144-
script: generateYamlScript([`echo Hello, world!`]),
145-
displayName: "Run a one-line script"
146-
}
146+
})
147147
]
148148
};
149149
// tslint:enable: object-literal-sort-keys
150150

151+
const requiredPipelineVariables = [
152+
`'ACR_NAME' (name of your ACR)`,
153+
`'SP_APP_ID' (service principal ID with access to your ACR)`,
154+
`'SP_PASS' (service principal secret)`,
155+
`'SP_TENANT' (service principal tenant)`
156+
].join(", ");
157+
158+
for (const relPath of cleanedPaths) {
159+
const relPathParts = relPath.split(path.sep).filter(p => p !== "");
160+
const packagesDir =
161+
relPathParts.length > 1 ? relPathParts.slice(-2)[0] : undefined;
162+
const packagesOption = packagesDir ? `--packages-dir ${packagesDir} ` : "";
163+
const serviceName =
164+
relPathParts.length > 1
165+
? relPathParts.slice(-2)[1]
166+
: process
167+
.cwd()
168+
.split(path.sep)
169+
.slice(-1)[0];
170+
const spkServiceCreatePipelineCmd =
171+
"spk service create-pipeline " + packagesOption + serviceName;
172+
logger.info(
173+
`Generated azure-pipelines.yaml for service in path '${relPath}'. Commit and push this file to master before attempting to deploy via the command '${spkServiceCreatePipelineCmd}'; before running the pipeline ensure the following environment variables are available to your pipeline: ${requiredPipelineVariables}`
174+
);
175+
}
176+
151177
return starter;
152178
};
153179

src/lib/pipelines/pipelines.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,23 @@ export const createPipelineForDefinition = async (
212212
logger.info("Creating pipeline for definition");
213213

214214
try {
215-
return await buildApi.createDefinition(definition, azdoProject);
215+
logger.debug(
216+
`Creating BuildDefinition based on ${JSON.stringify(definition)}`
217+
);
218+
const createdDefn = await buildApi.createDefinition(
219+
definition,
220+
azdoProject
221+
);
222+
// type definition for createDefinition is wrong. It will resolve a `null` if an error occurs in azdo
223+
if (!createdDefn) {
224+
throw Error(
225+
`Error creating BuildDefinition; buildApi.createDefinition() returned an invalid value of ${createdDefn}`
226+
);
227+
}
228+
return createdDefn;
216229
} catch (e) {
217230
logger.error(e);
218-
throw new Error("Error creating definition");
231+
throw Error("Error creating definition");
219232
}
220233
};
221234

@@ -231,16 +244,16 @@ export const queueBuild = async (
231244
azdoProject: string,
232245
definitionId: number
233246
): Promise<Build> => {
234-
const buildReference = {
247+
const buildReference: Build = {
235248
definition: {
236249
id: definitionId
237250
}
238-
} as Build;
251+
};
239252

240253
try {
241254
return await buildApi.queueBuild(buildReference, azdoProject);
242255
} catch (e) {
243256
logger.error(e);
244-
throw new Error("Error queueing build");
257+
throw Error("Error queueing build");
245258
}
246259
};

0 commit comments

Comments
 (0)