Skip to content

Commit

Permalink
fix: apply tags on create and push nested stack (#6321)
Browse files Browse the repository at this point in the history
* fix: apply tags on create and push nested stack
  • Loading branch information
ammarkarachi committed Jan 7, 2021
1 parent 9bf8b16 commit 4faa3e5
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { stateManager } from '../state-manager';
import { Tag } from '../tags';
describe('getTags', () => {
const mockedTags: Tag[] = [
{
Key: 'projectName',
Value: '{project-name}',
},
{
Key: 'projectenv',
Value: '{project-env}',
},
];
const mockConfig = {
projectConfig: {
projectName: 'foo',
},
localEnvInfo: {
envName: 'bar',
},
};
const mockGetProjectTags = jest.spyOn(stateManager, 'getProjectTags').mockReturnValue(mockedTags);
const mockgetProjectConfig = jest.spyOn(stateManager, 'getProjectConfig').mockReturnValue(mockConfig.projectConfig);
const mockgetLocalEnvInfo = jest.spyOn(stateManager, 'getLocalEnvInfo').mockReturnValue(mockConfig.localEnvInfo);

it('test for values', () => {
const readTags = stateManager.getHydratedTags(undefined);
expect(readTags).toBeDefined();
expect(readTags.filter(r => r.Key === 'projectName')[0].Value).toEqual(mockConfig.projectConfig.projectName);
expect(readTags.filter(r => r.Key === 'projectenv')[0].Value).toEqual(mockConfig.localEnvInfo.envName);
expect(mockGetProjectTags).toBeCalled();
expect(mockgetProjectConfig).toBeCalled();
expect(mockgetLocalEnvInfo).toBeCalled();
});
});
14 changes: 10 additions & 4 deletions packages/amplify-cli-core/src/state-manager/pathManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,17 @@ export class PathManager {
getBackendConfigFilePath = (projectPath?: string): string =>
this.constructPath(projectPath, [PathConstants.AmplifyDirName, PathConstants.BackendDirName, PathConstants.BackendConfigFileName]);

getTagFilePath = (projectPath?: string): string =>
this.constructPath(projectPath, [PathConstants.AmplifyDirName, PathConstants.BackendDirName, PathConstants.TagsFileName]);
getTagFilePath = (projectPath?: string): string => {
return this.constructPath(projectPath, [PathConstants.AmplifyDirName, PathConstants.BackendDirName, PathConstants.TagsFileName]);
};

getCurrentTagFilePath = (projectPath?: string): string =>
this.constructPath(projectPath, [PathConstants.AmplifyDirName, PathConstants.CurrentCloudBackendDirName, PathConstants.TagsFileName]);
getCurrentTagFilePath = (projectPath?: string): string => {
return this.constructPath(projectPath, [
PathConstants.AmplifyDirName,
PathConstants.CurrentCloudBackendDirName,
PathConstants.TagsFileName,
]);
};

getResourceParamatersFilePath = (projectPath: string | undefined, category: string, resourceName: string): string =>
this.constructPath(projectPath, [
Expand Down
15 changes: 14 additions & 1 deletion packages/amplify-cli-core/src/state-manager/stateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import * as fs from 'fs-extra';
import { pathManager } from './pathManager';
import { $TSMeta, $TSTeamProviderInfo, $TSAny, DeploymentSecrets } from '..';
import { JSONUtilities } from '../jsonUtilities';
import { Tag, ReadValidateTags } from '../tags';
import _ from 'lodash';
import { SecretFileMode } from '../cliConstants';
import { Tag, ReadValidateTags, HydrateTags } from '../tags';

export type GetOptions<T> = {
throwIfNotExist?: boolean;
preserveComments?: boolean;
Expand Down Expand Up @@ -168,6 +169,18 @@ export class StateManager {
JSONUtilities.writeJson(filePath, localAWSInfo);
};

getHydratedTags = (projectPath?: string | undefined): Tag[] => {
const tags = this.getProjectTags(projectPath);
const { projectName } = this.getProjectConfig(projectPath);
const { envName } = this.getLocalEnvInfo(projectPath);
return HydrateTags(tags, { projectName, envName });
};

isTagFilePresent = (projectPath?: string | undefined): boolean => {
if (pathManager.findProjectRoot()) return fs.existsSync(pathManager.getTagFilePath(projectPath));
return false;
};

setProjectFileTags = (projectPath: string | undefined, tags: Tag[]): void => {
const tagFilePath = pathManager.getTagFilePath(projectPath);
JSONUtilities.writeJson(tagFilePath, tags);
Expand Down
19 changes: 19 additions & 0 deletions packages/amplify-cli-core/src/tags/Tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,22 @@ export function validate(tags: Tag[]): void {
//check If tags exceed limit
if (tags.length > 50) throw new Error('No. of tags cannot exceed 50');
}

export function HydrateTags(tags: Tag[], tagVariables: TagVariables): Tag[] {
const { envName, projectName } = tagVariables;
const replace: any = {
'{project-name}': projectName,
'{project-env}': envName,
};
return tags.map(tag => {
return {
...tag,
Value: tag.Value.replace(/{project-name}|{project-env}/g, (matched: string) => replace[matched]),
};
});
}

type TagVariables = {
envName: string;
projectName: string;
};
12 changes: 10 additions & 2 deletions packages/amplify-cli/src/__tests__/get-tags.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { HydrateTags } from 'amplify-cli-core';
describe('getTags', () => {
const mockedTags = [
{
Expand All @@ -19,21 +20,28 @@ describe('getTags', () => {
};
jest.setMock('amplify-cli-core', {
stateManager: {
getProjectTags: jest.fn().mockReturnValue(mockedTags),
isTagFilePresent: jest.fn().mockReturnValue(false),
},
HydrateTags,
});
jest.setMock('../extensions/amplify-helpers/get-project-details', {
getProjectDetails: jest.fn().mockReturnValue(mockConfig),
});

const { getTags } = require('../extensions/amplify-helpers/get-tags');
const mockContext = {
exeInfo: {
...mockConfig,
initialTags: mockedTags,
},
};

it('getTags exists', () => {
expect(getTags).toBeDefined();
});

it('test for values', () => {
const readTags = getTags();
const readTags = getTags(mockContext);
expect(readTags).toBeDefined();
expect(readTags.filter(r => r.Key === 'projectName')[0].Value).toEqual(mockConfig.projectConfig.projectName);
expect(readTags.filter(r => r.Key === 'projectenv')[0].Value).toEqual(mockConfig.localEnvInfo.envName);
Expand Down
36 changes: 11 additions & 25 deletions packages/amplify-cli/src/extensions/amplify-helpers/get-tags.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
import { stateManager, Tag } from 'amplify-cli-core';
import { getProjectDetails } from './get-project-details';
export function getTags(): Tag[] {
const projectDetails = getProjectDetails();
const { projectName } = projectDetails.projectConfig;
const { envName } = projectDetails.localEnvInfo;
return HydrateTags(stateManager.getProjectTags(), { envName, projectName });
}
import { stateManager, Tag, HydrateTags } from 'amplify-cli-core';
import { Context } from '../../domain/context';

function HydrateTags(tags: Tag[], tagVariables: TagVariables): Tag[] {
const { envName, projectName } = tagVariables;
const replace = {
'{project-name}': projectName,
'{project-env}': envName,
};
return tags.map(tag => {
return {
...tag,
Value: tag.Value.replace(/{project-name}|{project-env}/g, (matched: string) => replace[matched]),
};
});
export function getTags(context: Context): Tag[] {
if (stateManager.isTagFilePresent()) {
return stateManager.getHydratedTags();
} else {
const tags = context.exeInfo.initialTags;
const { envName } = context.exeInfo.localEnvInfo;
const { projectName } = context.exeInfo.projectConfig;
return HydrateTags(tags, { envName, projectName });
}
}

type TagVariables = {
envName: string;
projectName: string;
};
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ function getResourcesToBeCreated(amplifyMeta, currentAmplifyMeta, category, reso
const dependsOnCategory = resources[i].dependsOn[j].category;
const dependsOnResourcename = resources[i].dependsOn[j].resourceName;
if (
amplifyMeta[dependsOnCategory] && (!amplifyMeta[dependsOnCategory][dependsOnResourcename].lastPushTimeStamp ||
amplifyMeta[dependsOnCategory] &&
(!amplifyMeta[dependsOnCategory][dependsOnResourcename].lastPushTimeStamp ||
!currentAmplifyMeta[dependsOnCategory] ||
!currentAmplifyMeta[dependsOnCategory][dependsOnResourcename]) &&
amplifyMeta[dependsOnCategory][dependsOnResourcename].serviceType !== 'imported'
Expand Down Expand Up @@ -348,12 +349,8 @@ export async function getResourceStatus(category?, resourceName?, providerName?,
resourcesToBeDeleted = resourcesToBeDeleted.filter(resource => resource.providerPlugin === providerName);
allResources = allResources.filter(resource => resource.providerPlugin === providerName);
}
let tagsUpdated = compareTags(stateManager.getProjectTags(), stateManager.getCurrentProjectTags());

// if tags updated but no resource to apply tags, ignore tags updated
if (allResources.filter(resource => resource.category === 'provider').length === 0) {
tagsUpdated = false;
}
const tagsUpdated = _.isEqual(stateManager.getProjectTags(), stateManager.getCurrentProjectTags());

return {
resourcesToBeCreated,
Expand All @@ -365,21 +362,6 @@ export async function getResourceStatus(category?, resourceName?, providerName?,
};
}

function compareTags(tags: Tag[], currenTags: Tag[]): boolean {
if (tags.length !== currenTags.length) return true;
const tagMap = new Map(tags.map(tag => [tag.Key, tag.Value]));
if (
_.some(currenTags, tag => {
if (tagMap.has(tag.Key)) {
if (tagMap.get(tag.Key) === tag.Value) return false;
}
})
)
return true;

return false;
}

export async function showResourceTable(category, resourceName, filteredResources) {
const amplifyProjectInitStatus = getCloudInitStatus();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ function moveBackendResourcesToCurrentCloudBackend(resources: $TSObject[]) {
const amplifyCloudMetaFilePath = pathManager.getCurrentAmplifyMetaFilePath();
const backendConfigFilePath = pathManager.getBackendConfigFilePath();
const backendConfigCloudFilePath = pathManager.getCurrentBackendConfigFilePath();
const tagFilePath = pathManager.getTagFilePath();
const tagCloudFilePath = pathManager.getCurrentTagFilePath();

for (const resource of resources) {
const sourceDir = path.normalize(path.join(pathManager.getBackendDirPath(), resource.category, resource.resourceName));
Expand All @@ -71,11 +69,6 @@ function moveBackendResourcesToCurrentCloudBackend(resources: $TSObject[]) {

fs.copySync(amplifyMetaFilePath, amplifyCloudMetaFilePath, { overwrite: true });
fs.copySync(backendConfigFilePath, backendConfigCloudFilePath, { overwrite: true });

// if project hasn't been initialized after tags has been released
if (fs.existsSync(tagFilePath)) {
fs.copySync(tagFilePath, tagCloudFilePath, { overwrite: true });
}
}

function removeNodeModulesDir(currentCloudBackendDir: string) {
Expand Down
11 changes: 11 additions & 0 deletions packages/amplify-cli/src/init-steps/s0-analyzeProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ function setExeInfo(context: $TSContext, projectPath: String, defaultEditor?: St
};
context.exeInfo.teamProviderInfo = {};
context.exeInfo.metaData = {};
context.exeInfo.initialTags = [
{
Key: 'user:Stack',
Value: '{project-env}',
},
{
Key: 'user:Application',
Value: '{project-name}',
},
];
return context;
}

/* Begin getProjectName */
Expand Down
10 changes: 10 additions & 0 deletions packages/amplify-provider-awscloudformation/src/initializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ async function run(context) {
const { amplifyAppId, verifiedStackName, deploymentBucketName } = await amplifyServiceManager.init(amplifyServiceParams);

stackName = verifiedStackName;
const Tags = context.amplify.getTags(context);

const authRoleName = `${stackName}-authRole`;
const unauthRoleName = `${stackName}-unauthRole`;

Expand Down Expand Up @@ -64,6 +66,7 @@ async function run(context) {
ParameterValue: unauthRoleName,
},
],
Tags,
};

const spinner = ora();
Expand Down Expand Up @@ -168,6 +171,13 @@ function storeCurrentCloudBackend(context) {
absolute: true,
});

// handle tag file
const tagFilePath = pathManager.getTagFilePath();
const tagCloudFilePath = pathManager.getCurrentTagFilePath();
if (fs.existsSync(tagFilePath)) {
fs.copySync(tagFilePath, tagCloudFilePath, { overwrite: true });
}

const zipFilePath = path.normalize(path.join(tempDir, zipFilename));
let log = null;

Expand Down
17 changes: 15 additions & 2 deletions packages/amplify-provider-awscloudformation/src/push-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ export async function run(context: $TSContext, resourceDefinition: $TSObject) {
let iterativeDeploymentWasInvoked = false;

try {
const { resourcesToBeCreated, resourcesToBeUpdated, resourcesToBeSynced, resourcesToBeDeleted, allResources } = resourceDefinition;
const {
resourcesToBeCreated,
resourcesToBeUpdated,
resourcesToBeSynced,
resourcesToBeDeleted,
tagsUpdated,
allResources,
} = resourceDefinition;
const clousformationMeta = context.amplify.getProjectMeta().providers.awscloudformation;
const {
parameters: { options },
Expand Down Expand Up @@ -162,7 +169,7 @@ export async function run(context: $TSContext, resourceDefinition: $TSObject) {
await updateS3Templates(context, resources, projectDetails.amplifyMeta);

// We do not need CloudFormation update if only syncable resources are the changes.
if (resourcesToBeCreated.length > 0 || resourcesToBeUpdated.length > 0 || resourcesToBeDeleted.length > 0) {
if (resourcesToBeCreated.length > 0 || resourcesToBeUpdated.length > 0 || resourcesToBeDeleted.length > 0 || tagsUpdated) {
// If there is an API change, there will be one deployment step. But when there needs an iterative update the step count is > 1
if (deploymentSteps.length > 1) {
// create deployment manager
Expand Down Expand Up @@ -409,6 +416,12 @@ export async function storeCurrentCloudBackend(context: $TSContext) {
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir);
}
// handle tag file
const tagFilePath = pathManager.getTagFilePath();
const tagCloudFilePath = pathManager.getCurrentTagFilePath();
if (fs.existsSync(tagFilePath)) {
fs.copySync(tagFilePath, tagCloudFilePath, { overwrite: true });
}

const cliJSONFiles = glob.sync(PathConstants.CLIJSONFileNameGlob, {
cwd: pathManager.getAmplifyDirPath(),
Expand Down

0 comments on commit 4faa3e5

Please sign in to comment.