Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

spk should allow a project to use an existing variable group #23

Merged
merged 14 commits into from
Apr 24, 2020
31 changes: 28 additions & 3 deletions docs/commands/data.json
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,31 @@
],
"markdown": "## Description\n\nBuilds a scaffold of an infrastructure deployment project containing a\n`definition.yaml` that enables a user to version, modify and organize terraform\ndeployments.\n\nIn detail, it will do the following:\n\n- Create a new folder with the `<name>` you provided.\n- Clone and cache the source repo to `~.spk/templates`.\n- Provide an infrastructure deployment scaffold based on a `<source>` git url\n for a repo that holds terraform template, a `<version>` respective to the\n repository tag or branch to pull from, and a `<template>` path to a terraform\n environment template from the root of the git repo.\n\n## Example\n\n```\nspk infra scaffold --name fabrikam --source https://github.com/microsoft/bedrock --version master --template /cluster/environments/azure-single-keyvault\n```\n\ndefinition.yaml output:\n\n```yaml\nname: fabrikam\nsource: \"https://github.com/microsoft/bedrock.git\"\ntemplate: cluster/environments/azure-single-keyvault\nversion: master\nbackend:\n storage_account_name: storage-account-name\n access_key: storage-account-access-key\n container_name: storage-account-container\n key: tfstate-key\nvariables:\n address_space: <insert value>\n agent_vm_count: <insert value>\n agent_vm_size: <insert value>\n cluster_name: <insert value>\n dns_prefix: <insert value>\n flux_recreate: <insert value>\n kubeconfig_recreate: <insert value>\n gitops_ssh_url: <insert value>\n gitops_ssh_key: <insert value>\n gitops_path: <insert value>\n keyvault_name: <insert value>\n keyvault_resource_group: <insert value>\n resource_group_name: <insert value>\n ssh_public_key: <insert value>\n service_principal_id: <insert value>\n service_principal_secret: <insert value>\n subnet_prefixes: <insert value>\n vnet_name: <insert value>\n subnet_name: <insert value>\n acr_name: <insert value>\n```\n\n**Note:** Definitions will only include variables that do not have a default\nvalue. To override default values, add the variable name to the variables\ndefinition and provide a new value.\n"
},
"project append-variable-group": {
"command": "append-variable-group <variable-group-name>",
"alias": "avg",
"description": "Appends the name of an existing variable group to the current bedrock.yaml file and the associated service build pipelines.",
"options": [
{
"arg": "-o, --org-name <organization-name>",
"description": "Organization Name for Azure DevOps",
"required": true,
"inherit": "azure_devops.org"
},
{
"arg": "-d, --devops-project <devops-project>",
"description": "Azure DevOps Project name",
"required": true,
"inherit": "azure_devops.project"
},
{
"arg": "-a, --personal-access-token <personal-access-token>",
"description": "Personal Access Token",
"required": true,
"inherit": "azure_devops.access_token"
}
]
},
"project create-variable-group": {
"command": "create-variable-group <variable-group-name>",
"alias": "cvg",
Expand Down Expand Up @@ -468,13 +493,13 @@
"arg": "-a, --personal-access-token <personal-access-token>",
"description": "Personal Access Token",
"required": true,
"inherit": "azureDevops.access_token"
"inherit": "azure_devops.access_token"
},
{
"arg": "-o, --org-name <organization-name>",
"description": "Organization Name for Azure DevOps",
"required": true,
"inherit": "azureDevops.org"
"inherit": "azure_devops.org"
},
{
"arg": "-u, --repo-url <repo-url>",
Expand All @@ -485,7 +510,7 @@
"arg": "-d, --devops-project <devops-project>",
"description": "Azure DevOps Project name",
"required": true,
"inherit": "azureDevops.project"
"inherit": "azure_devops.project"
},
{
"arg": "-b, --build-script-url <build-script-url>",
Expand Down
25 changes: 25 additions & 0 deletions src/commands/project/append-variable-group.decorator.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"command": "append-variable-group <variable-group-name>",
"alias": "avg",
"description": "Appends the name of an existing variable group to the current bedrock.yaml file and the associated service build pipelines.",
"options": [
{
"arg": "-o, --org-name <organization-name>",
"description": "Organization Name for Azure DevOps",
"required": true,
"inherit": "azure_devops.org"
},
{
"arg": "-d, --devops-project <devops-project>",
"description": "Azure DevOps Project name",
"required": true,
"inherit": "azure_devops.project"
},
{
"arg": "-a, --personal-access-token <personal-access-token>",
"description": "Personal Access Token",
"required": true,
"inherit": "azure_devops.access_token"
}
]
}
103 changes: 103 additions & 0 deletions src/commands/project/append-variable-group.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
execute,
CommandOptions,
validateValues,
} from "./append-variable-group";
import * as appendVariableGrp from "./append-variable-group";
import * as fileutils from "../../lib/fileutils";
import { createTestBedrockYaml } from "../../test/mockFactory";
import * as config from "../../config";
import { BedrockFile } from "../../types";
import { ConfigValues } from "./pipeline";
import * as bedrockYaml from "../../lib/bedrockYaml";
import * as variableGrp from "../../lib/pipelines/variableGroup";
import { deepClone } from "../../lib/util";

const mockValues: CommandOptions = {
devopsProject: "azDoProject",
orgName: "orgName",
personalAccessToken: "PAT",
};

describe("Test execute function", () => {
it("missing variable group name", async () => {
const exitFn = jest.fn();
await execute("my-path", "", mockValues, exitFn);
expect(exitFn).toBeCalledTimes(1);
expect(exitFn.mock.calls).toEqual([[1]]);
});
it("variable group does not exist", async () => {
const exitFn = jest.fn();
spyOn(fileutils, "appendVariableGroupToPipelineYaml");
jest
.spyOn(variableGrp, "hasVariableGroup")
.mockReturnValueOnce(Promise.resolve(false));

const bedrockFile = createTestBedrockYaml(false) as BedrockFile;

jest.spyOn(config, "Bedrock").mockReturnValue(bedrockFile as BedrockFile);
jest.spyOn(appendVariableGrp, "checkDependencies").mockReturnValueOnce();
jest
.spyOn(appendVariableGrp, "validateValues")
.mockReturnValueOnce(mockValues as ConfigValues);
jest.spyOn(bedrockYaml, "addVariableGroup").mockReturnValue();
jest
.spyOn(fileutils, "appendVariableGroupToPipelineYaml")
.mockReturnValue();

expect(bedrockFile.variableGroups?.length).toBe(0);
await execute("my-path", "my-vg", mockValues, exitFn);
expect(exitFn).toBeCalledTimes(1);
expect(exitFn.mock.calls).toEqual([[1]]);
expect(fileutils.appendVariableGroupToPipelineYaml).toHaveBeenCalledTimes(
0
);
});
it("appends variable group", async () => {
const exitFn = jest.fn();
spyOn(fileutils, "appendVariableGroupToPipelineYaml");
jest
.spyOn(variableGrp, "hasVariableGroup")
.mockReturnValue(Promise.resolve(true));

const bedrockFile = createTestBedrockYaml(false) as BedrockFile;

jest.spyOn(config, "Bedrock").mockReturnValue(bedrockFile as BedrockFile);
jest.spyOn(appendVariableGrp, "checkDependencies").mockReturnValueOnce();
jest
.spyOn(appendVariableGrp, "validateValues")
.mockReturnValueOnce(mockValues as ConfigValues);
jest.spyOn(bedrockYaml, "addVariableGroup").mockReturnValue();
jest
.spyOn(fileutils, "appendVariableGroupToPipelineYaml")
.mockReturnValue();

expect(bedrockFile.variableGroups?.length).toBe(0);
await execute("my-path", "my-vg", mockValues, exitFn);
expect(exitFn).toBeCalledTimes(1);
expect(exitFn.mock.calls).toEqual([[0]]);
expect(fileutils.appendVariableGroupToPipelineYaml).toHaveBeenCalledTimes(
3
dennisseah marked this conversation as resolved.
Show resolved Hide resolved
);
});
});
describe("test validateValues function", () => {
it("valid org and project name", () => {
const data = deepClone(mockValues);
validateValues(data);
});
it("invalid project name", () => {
const data = deepClone(mockValues);
data.devopsProject = "project\\abc";
expect(() => {
validateValues(data);
}).toThrow();
});
it("invalid org name", () => {
const data = deepClone(mockValues);
data.orgName = "org name";
expect(() => {
validateValues(data);
}).toThrow();
});
});
175 changes: 175 additions & 0 deletions src/commands/project/append-variable-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import commander from "commander";
import {
build as buildCmd,
exit as exitCmd,
populateInheritValueFromConfig,
validateForRequiredValues,
} from "../../lib/commandBuilder";
import {
hasValue,
validateProjectNameThrowable,
validateOrgNameThrowable,
} from "../../lib/validator";
import { Bedrock, Config } from "../../config";
import { logger } from "../../logger";
import { build as buildError, log as logError } from "../../lib/errorBuilder";
import { errorStatusCode } from "../../lib/errorStatusCode";
import decorator from "./append-variable-group.decorator.json";
import { BedrockFileInfo, BedrockFile } from "../../types";
import * as bedrockYaml from "../../lib/bedrockYaml";
import { hasVariableGroup } from "../../lib/pipelines/variableGroup";
import { AzureDevOpsOpts } from "../../lib/git";
import { appendVariableGroupToPipelineYaml } from "../../lib/fileutils";
import { SERVICE_PIPELINE_FILENAME } from "../../lib/constants";

// Values that need to be pulled out from the command operator
export interface CommandOptions {
orgName: string | undefined;
personalAccessToken: string | undefined;
devopsProject: string | undefined;
}

// Configuration values
interface ConfigValues {
orgName: string;
personalAccessToken: string;
devopsProject: string;
}

/**
* Validates values passed as options
* @param opts The initialized options
*/
export const validateValues = (opts: CommandOptions): ConfigValues => {
populateInheritValueFromConfig(decorator, Config(), opts);
validateForRequiredValues(decorator, opts, true);

// validateForRequiredValues already check required values
// || "" is just to satisfy eslint rule.
validateProjectNameThrowable(opts.devopsProject || "");
validateOrgNameThrowable(opts.orgName || "");

return {
orgName: opts.orgName || "",
personalAccessToken: opts.personalAccessToken || "",
devopsProject: opts.devopsProject || "",
};
};

/**
* Check project dependencies
* @param projectPath Path to the project directory
*/
export const checkDependencies = (projectPath: string): void => {
const fileInfo: BedrockFileInfo = bedrockYaml.fileInfo(projectPath);
if (fileInfo.exist === false) {
throw buildError(
errorStatusCode.VALIDATION_ERR,
"project-append-variable-group-cmd-err-dependency"
dennisseah marked this conversation as resolved.
Show resolved Hide resolved
);
}
};

/**
* Checks if a variable group exists in an Azure DevOps project
* @param variableGroupName Variable Group Name
* @param values The config values
*/
export const variableGroupExists = async (
variableGroupName: string,
values: ConfigValues
): Promise<boolean> => {
const accessOpts: AzureDevOpsOpts = {
orgName: values.orgName,
personalAccessToken: values.personalAccessToken,
project: values.devopsProject,
};

return await hasVariableGroup(accessOpts, variableGroupName);
};

/**
* Update the variable groups for the project services
* @param bedrockFile The bedrock.yaml file
* @param variableGroupName Variable Group Name
*/
export const updateServicesVariableGroups = (
bedrockFile: BedrockFile,
variableGroupName: string
): void => {
bedrockFile.services.forEach((service) => {
const path = service.path;
appendVariableGroupToPipelineYaml(
path,
SERVICE_PIPELINE_FILENAME,
variableGroupName
);
});
};

/**
* Executes the command.
*
* @param projectPath The path to the spk project
* @param variableGroupName Variable Group Name
* @param opts Option object from command
* @param exitFn The exit function
*/
export const execute = async (
projectPath: string,
variableGroupName: string,
opts: CommandOptions,
exitFn: (status: number) => Promise<void>
): Promise<void> => {
if (!hasValue(variableGroupName)) {
await exitFn(1);
return;
}

try {
edaena marked this conversation as resolved.
Show resolved Hide resolved
checkDependencies(projectPath);
const values = validateValues(opts);

if (!(await variableGroupExists(variableGroupName, values))) {
throw buildError(errorStatusCode.CMD_EXE_ERR, {
errorKey:
"project-append-variable-group-cmd-err-variable-group-invalid",
values: [variableGroupName, values.orgName, values.devopsProject],
});
}

const bedrockFile = Bedrock(projectPath);
bedrockYaml.addVariableGroup(bedrockFile, projectPath, variableGroupName);
updateServicesVariableGroups(bedrockFile, variableGroupName);
await exitFn(0);
} catch (err) {
logError(
buildError(
errorStatusCode.CMD_EXE_ERR,
"project-append-variable-group-cmd-failed",
err
)
);
await exitFn(1);
}
};

/**
* Adds the init command to the commander command object
* @param command Commander command object to decorate
*/
export const commandDecorator = (command: commander.Command): void => {
buildCmd(command, decorator).action(
async (variableGroupName: string, opts: CommandOptions) => {
const projectPath = process.cwd();
await execute(
projectPath,
variableGroupName,
opts,
async (status: number) => {
await exitCmd(logger, process.exit, status);
}
);
}
);
};
Loading