From 81e7b3b551d78e055cbe49aa84ed30dc3f1acbbb Mon Sep 17 00:00:00 2001 From: AmmarKarachi Date: Wed, 26 Aug 2020 15:46:10 -0700 Subject: [PATCH 1/3] feat: resource Tagging Support Enable to customers to add tags using `/amplify/backend/tags.json` file --- .circleci/config.yml | 261 +++++++++--------- .../src/__tests__/tags.test.ts | 44 +++ packages/amplify-cli-core/src/index.ts | 1 + .../src/state-manager/pathManager.ts | 7 + .../src/state-manager/stateManager.ts | 10 + packages/amplify-cli-core/src/tags/Tags.ts | 35 +++ packages/amplify-cli-core/src/tags/index.ts | 1 + .../amplify-cli/src/domain/amplify-toolkit.ts | 5 + .../extensions/amplify-helpers/constants.ts | 1 + .../extensions/amplify-helpers/get-tags.ts | 21 ++ .../amplify-helpers/resource-status.ts | 28 +- .../amplify-helpers/update-amplify-meta.ts | 3 + .../src/init-steps/s9-onSuccess.ts | 20 +- .../amplify-e2e-core/src/utils/projectMeta.ts | 6 + .../src/__tests__/tags.test.ts | 49 ++++ .../src/aws-utils/aws-cfn.js | 4 +- 16 files changed, 366 insertions(+), 130 deletions(-) create mode 100644 packages/amplify-cli-core/src/__tests__/tags.test.ts create mode 100644 packages/amplify-cli-core/src/tags/Tags.ts create mode 100644 packages/amplify-cli-core/src/tags/index.ts create mode 100644 packages/amplify-cli/src/extensions/amplify-helpers/get-tags.ts create mode 100644 packages/amplify-e2e-tests/src/__tests__/tags.test.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 3012c593e9b..3f3e8c484cc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -769,141 +769,141 @@ jobs: when: always - store_artifacts: path: ../uitest_android_results - api_1-amplify_e2e_tests: + schema-key-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/api_1.test.ts + TEST_SUITE: src/__tests__/schema-key.test.ts CLI_REGION: us-east-2 - api_2-amplify_e2e_tests: + tags-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/api_2.test.ts + TEST_SUITE: src/__tests__/tags.test.ts CLI_REGION: us-west-2 - auth_1-amplify_e2e_tests: + api_2-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/auth_1.test.ts + TEST_SUITE: src/__tests__/api_2.test.ts CLI_REGION: eu-west-2 - auth_2-amplify_e2e_tests: + auth_1-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/auth_2.test.ts + TEST_SUITE: src/__tests__/auth_1.test.ts CLI_REGION: eu-central-1 - env-amplify_e2e_tests: + auth_2-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/env.test.ts + TEST_SUITE: src/__tests__/auth_2.test.ts CLI_REGION: ap-northeast-1 - function_1-amplify_e2e_tests: + schema-versioned-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/function_1.test.ts + TEST_SUITE: src/__tests__/schema-versioned.test.ts CLI_REGION: ap-southeast-1 - function_2-amplify_e2e_tests: + schema-searchable-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/function_2.test.ts + TEST_SUITE: src/__tests__/schema-searchable.test.ts CLI_REGION: ap-southeast-2 - init-special-case-amplify_e2e_tests: + env-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/init-special-case.test.ts + TEST_SUITE: src/__tests__/env.test.ts CLI_REGION: us-east-2 - layer-amplify_e2e_tests: + function_1-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/layer.test.ts + TEST_SUITE: src/__tests__/function_1.test.ts CLI_REGION: us-west-2 - schema-auth-1-amplify_e2e_tests: + function_2-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-auth-1.test.ts + TEST_SUITE: src/__tests__/function_2.test.ts CLI_REGION: eu-west-2 - schema-auth-2-amplify_e2e_tests: + schema-predictions-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-auth-2.test.ts + TEST_SUITE: src/__tests__/schema-predictions.test.ts CLI_REGION: eu-central-1 - schema-auth-3-amplify_e2e_tests: + schema-model-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-auth-3.test.ts + TEST_SUITE: src/__tests__/schema-model.test.ts CLI_REGION: ap-northeast-1 - schema-auth-4-amplify_e2e_tests: + init-special-case-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-auth-4.test.ts + TEST_SUITE: src/__tests__/init-special-case.test.ts CLI_REGION: ap-southeast-1 - schema-auth-5-amplify_e2e_tests: + api_1-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-auth-5.test.ts + TEST_SUITE: src/__tests__/api_1.test.ts CLI_REGION: ap-southeast-2 - schema-auth-6-amplify_e2e_tests: + schema-function-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-auth-6.test.ts + TEST_SUITE: src/__tests__/schema-function.test.ts CLI_REGION: us-east-2 - schema-auth-7-amplify_e2e_tests: + layer-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-auth-7.test.ts + TEST_SUITE: src/__tests__/layer.test.ts CLI_REGION: us-west-2 - schema-auth-8-amplify_e2e_tests: + schema-data-access-patterns-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-auth-8.test.ts + TEST_SUITE: src/__tests__/schema-data-access-patterns.test.ts CLI_REGION: eu-west-2 schema-connection-amplify_e2e_tests: working_directory: ~/repo @@ -913,62 +913,70 @@ jobs: environment: TEST_SUITE: src/__tests__/schema-connection.test.ts CLI_REGION: eu-central-1 - schema-data-access-patterns-amplify_e2e_tests: + schema-auth-8-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-data-access-patterns.test.ts + TEST_SUITE: src/__tests__/schema-auth-8.test.ts CLI_REGION: ap-northeast-1 - schema-function-amplify_e2e_tests: + schema-auth-7-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-function.test.ts + TEST_SUITE: src/__tests__/schema-auth-7.test.ts CLI_REGION: ap-southeast-1 - schema-key-amplify_e2e_tests: + schema-auth-1-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-key.test.ts + TEST_SUITE: src/__tests__/schema-auth-1.test.ts CLI_REGION: ap-southeast-2 - schema-model-amplify_e2e_tests: + schema-auth-2-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-model.test.ts + TEST_SUITE: src/__tests__/schema-auth-2.test.ts CLI_REGION: us-east-2 - schema-predictions-amplify_e2e_tests: + schema-auth-3-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-predictions.test.ts + TEST_SUITE: src/__tests__/schema-auth-3.test.ts CLI_REGION: us-west-2 - schema-searchable-amplify_e2e_tests: + schema-auth-4-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-searchable.test.ts + TEST_SUITE: src/__tests__/schema-auth-4.test.ts CLI_REGION: eu-west-2 - schema-versioned-amplify_e2e_tests: + schema-auth-5-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 resource_class: large steps: *ref_1 environment: - TEST_SUITE: src/__tests__/schema-versioned.test.ts + TEST_SUITE: src/__tests__/schema-auth-5.test.ts CLI_REGION: eu-central-1 + schema-auth-6-amplify_e2e_tests: + working_directory: ~/repo + docker: *ref_0 + resource_class: large + steps: *ref_1 + environment: + TEST_SUITE: src/__tests__/schema-auth-6.test.ts + CLI_REGION: ap-northeast-1 plugin-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -976,7 +984,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/plugin.test.ts - CLI_REGION: ap-northeast-1 + CLI_REGION: ap-southeast-1 datastore-modegen-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -984,7 +992,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/datastore-modegen.test.ts - CLI_REGION: ap-southeast-1 + CLI_REGION: ap-southeast-2 interactions-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -992,7 +1000,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/interactions.test.ts - CLI_REGION: ap-southeast-2 + CLI_REGION: us-east-2 hosting-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -1000,7 +1008,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/hosting.test.ts - CLI_REGION: us-east-2 + CLI_REGION: us-west-2 init-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -1008,7 +1016,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/init.test.ts - CLI_REGION: us-west-2 + CLI_REGION: eu-west-2 amplify-app-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -1016,7 +1024,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/amplify-app.test.ts - CLI_REGION: eu-west-2 + CLI_REGION: eu-central-1 analytics-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -1024,7 +1032,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/analytics.test.ts - CLI_REGION: eu-central-1 + CLI_REGION: ap-northeast-1 hostingPROD-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -1032,7 +1040,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/hostingPROD.test.ts - CLI_REGION: ap-northeast-1 + CLI_REGION: ap-southeast-1 predictions-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -1040,7 +1048,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/predictions.test.ts - CLI_REGION: ap-southeast-1 + CLI_REGION: ap-southeast-2 delete-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -1048,7 +1056,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/delete.test.ts - CLI_REGION: ap-southeast-2 + CLI_REGION: us-east-2 storage-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -1056,7 +1064,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/storage.test.ts - CLI_REGION: us-east-2 + CLI_REGION: us-west-2 migration-api-key-migration-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -1064,7 +1072,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/migration/api.key.migration.test.ts - CLI_REGION: us-west-2 + CLI_REGION: eu-west-2 migration-api-connection-migration-amplify_e2e_tests: working_directory: ~/repo docker: *ref_0 @@ -1072,7 +1080,7 @@ jobs: steps: *ref_1 environment: TEST_SUITE: src/__tests__/migration/api.connection.migration.test.ts - CLI_REGION: eu-west-2 + CLI_REGION: eu-central-1 workflows: version: 2 nightly_console_integration_tests: @@ -1165,34 +1173,34 @@ workflows: - amplify_console_integration_tests - amplify_migration_tests_latest - amplify_migration_tests_v4 - - schema-model-amplify_e2e_tests + - schema-auth-2-amplify_e2e_tests + - interactions-amplify_e2e_tests + - delete-amplify_e2e_tests + - schema-auth-3-amplify_e2e_tests - hosting-amplify_e2e_tests - storage-amplify_e2e_tests - - schema-predictions-amplify_e2e_tests + - schema-auth-4-amplify_e2e_tests - init-amplify_e2e_tests - migration-api-key-migration-amplify_e2e_tests - - schema-searchable-amplify_e2e_tests + - schema-auth-5-amplify_e2e_tests - amplify-app-amplify_e2e_tests - migration-api-connection-migration-amplify_e2e_tests - - schema-connection-amplify_e2e_tests - - schema-versioned-amplify_e2e_tests + - schema-auth-8-amplify_e2e_tests + - schema-auth-6-amplify_e2e_tests - analytics-amplify_e2e_tests - - schema-data-access-patterns-amplify_e2e_tests + - schema-auth-7-amplify_e2e_tests - plugin-amplify_e2e_tests - hostingPROD-amplify_e2e_tests - - schema-function-amplify_e2e_tests + - schema-auth-1-amplify_e2e_tests - datastore-modegen-amplify_e2e_tests - predictions-amplify_e2e_tests - - schema-key-amplify_e2e_tests - - interactions-amplify_e2e_tests - - delete-amplify_e2e_tests filters: branches: only: - release - master - beta - - api_1-amplify_e2e_tests: + - schema-key-amplify_e2e_tests: filters: &ref_2 branches: only: @@ -1200,30 +1208,34 @@ workflows: - graphqlschemae2e requires: - publish_to_local_registry - - init-special-case-amplify_e2e_tests: + - env-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-6-amplify_e2e_tests: + - schema-function-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-model-amplify_e2e_tests: + - schema-auth-2-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - api_1-amplify_e2e_tests - - hosting-amplify_e2e_tests: + - schema-key-amplify_e2e_tests + - interactions-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - init-special-case-amplify_e2e_tests - - storage-amplify_e2e_tests: + - env-amplify_e2e_tests + - delete-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-6-amplify_e2e_tests - - api_2-amplify_e2e_tests: + - schema-function-amplify_e2e_tests + - tags-amplify_e2e_tests: + filters: *ref_2 + requires: + - publish_to_local_registry + - function_1-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry @@ -1231,137 +1243,138 @@ workflows: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-7-amplify_e2e_tests: + - schema-auth-3-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-predictions-amplify_e2e_tests: + - tags-amplify_e2e_tests + - hosting-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - api_2-amplify_e2e_tests - - init-amplify_e2e_tests: + - function_1-amplify_e2e_tests + - storage-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - layer-amplify_e2e_tests - - migration-api-key-migration-amplify_e2e_tests: + - api_2-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-7-amplify_e2e_tests - - auth_1-amplify_e2e_tests: + - function_2-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-1-amplify_e2e_tests: + - schema-data-access-patterns-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-8-amplify_e2e_tests: + - schema-auth-4-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-searchable-amplify_e2e_tests: + - api_2-amplify_e2e_tests + - init-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - auth_1-amplify_e2e_tests - - amplify-app-amplify_e2e_tests: + - function_2-amplify_e2e_tests + - migration-api-key-migration-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-1-amplify_e2e_tests - - migration-api-connection-migration-amplify_e2e_tests: + - schema-data-access-patterns-amplify_e2e_tests + - auth_1-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-8-amplify_e2e_tests - - auth_2-amplify_e2e_tests: + - schema-predictions-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-2-amplify_e2e_tests: + - schema-connection-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-connection-amplify_e2e_tests: + - schema-auth-5-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-versioned-amplify_e2e_tests: + - auth_1-amplify_e2e_tests + - amplify-app-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - auth_2-amplify_e2e_tests - - analytics-amplify_e2e_tests: + - schema-predictions-amplify_e2e_tests + - migration-api-connection-migration-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-2-amplify_e2e_tests - - env-amplify_e2e_tests: + - schema-connection-amplify_e2e_tests + - auth_2-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-3-amplify_e2e_tests: + - schema-model-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-data-access-patterns-amplify_e2e_tests: + - schema-auth-8-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - plugin-amplify_e2e_tests: + - schema-auth-6-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - env-amplify_e2e_tests - - hostingPROD-amplify_e2e_tests: + - auth_2-amplify_e2e_tests + - analytics-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-3-amplify_e2e_tests - - function_1-amplify_e2e_tests: + - schema-model-amplify_e2e_tests + - schema-versioned-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-4-amplify_e2e_tests: + - init-special-case-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-function-amplify_e2e_tests: + - schema-auth-7-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - datastore-modegen-amplify_e2e_tests: + - plugin-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - function_1-amplify_e2e_tests - - predictions-amplify_e2e_tests: + - schema-versioned-amplify_e2e_tests + - hostingPROD-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-4-amplify_e2e_tests - - function_2-amplify_e2e_tests: + - init-special-case-amplify_e2e_tests + - schema-searchable-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-5-amplify_e2e_tests: + - api_1-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-key-amplify_e2e_tests: + - schema-auth-1-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - interactions-amplify_e2e_tests: + - datastore-modegen-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - function_2-amplify_e2e_tests - - delete-amplify_e2e_tests: + - schema-searchable-amplify_e2e_tests + - predictions-amplify_e2e_tests: filters: *ref_2 requires: - publish_to_local_registry - - schema-auth-5-amplify_e2e_tests + - api_1-amplify_e2e_tests diff --git a/packages/amplify-cli-core/src/__tests__/tags.test.ts b/packages/amplify-cli-core/src/__tests__/tags.test.ts new file mode 100644 index 00000000000..19692bc782b --- /dev/null +++ b/packages/amplify-cli-core/src/__tests__/tags.test.ts @@ -0,0 +1,44 @@ +import { validate } from '../tags/Tags'; + +describe('tags-validation:', () => { + describe('case: tags-validation receives a JSON file with duplicate keys', () => { + const json = [ + { Key: 'user:Stack', Value: 'dev' }, + { Key: 'user:Application', Value: 'foobar' }, + { Key: 'user:Application', Value: 'foobar' }, + ]; + + it('tags-validation should throw an error saying that the tags.json file contians duplicate keys', () => { + expect(() => validate(json)).toThrowError(new Error("'Key' should be unique")); + }); + }); + + describe('case: tags-validation receives a JSON file that contains more than 50 key-value pairs', () => { + const jsonObj: any = []; + + const json = [ + { Key: 'user:Stack', Value: 'dev', key: 'notgood' }, + { Key: 'user:Application', Value: 'foobar' }, + { Key: 'user:Application', Value: 'foobar' }, + ]; + + it('tags-validation should throw an error stating that the tags.json file has exceeded the tags amount limit', () => { + expect(() => validate(json)).toThrowError(new Error('Tag thould be of type Key: string, Value: string')); + }); + }); + + describe('case: tags-validation receives a JSON file that contains more than 50 key-value pairs', () => { + const jsonObj: any = []; + + for (let i = 0; i < 55; i++) { + jsonObj.push({ + Key: `user:key${i}`, + Value: `value${i}`, + }); + } + + it('tags-validation should throw an error stating that the tags.json file has exceeded the tags amount limit', () => { + expect(() => validate(jsonObj)).toThrowError(new Error('No. of tags cannot exceed 50')); + }); + }); +}); diff --git a/packages/amplify-cli-core/src/index.ts b/packages/amplify-cli-core/src/index.ts index 846f08e4789..9d82c838c20 100644 --- a/packages/amplify-cli-core/src/index.ts +++ b/packages/amplify-cli-core/src/index.ts @@ -5,6 +5,7 @@ export * from './feature-flags'; export * from './jsonUtilities'; export * from './jsonValidationError'; export * from './state-manager'; +export * from './tags'; // Temporary types until we can finish full type definition across the whole CLI diff --git a/packages/amplify-cli-core/src/state-manager/pathManager.ts b/packages/amplify-cli-core/src/state-manager/pathManager.ts index 02add8606e8..e464252bd6d 100644 --- a/packages/amplify-cli-core/src/state-manager/pathManager.ts +++ b/packages/amplify-cli-core/src/state-manager/pathManager.ts @@ -17,6 +17,7 @@ const PathConstants = { GitIgnoreFileName: '.gitignore', ProjectConfigFileName: 'project-config.json', AmplifyMetaFileName: 'amplify-meta.json', + TagsFileName: 'tags.json', LocalEnvFileName: 'local-env-info.json', LocalAWSInfoFileName: 'local-aws-info.json', @@ -68,6 +69,12 @@ 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]); + + getCurrentTagFilePath = (projectPath?: string): string => + this.constructPath(projectPath, [PathConstants.AmplifyDirName, PathConstants.CurrentCloudBackendDirName, PathConstants.TagsFileName]); + getCurrentAmplifyMetaFilePath = (projectPath?: string): string => this.constructPath(projectPath, [ PathConstants.AmplifyDirName, diff --git a/packages/amplify-cli-core/src/state-manager/stateManager.ts b/packages/amplify-cli-core/src/state-manager/stateManager.ts index 7a6638ab1ff..64a813665bd 100644 --- a/packages/amplify-cli-core/src/state-manager/stateManager.ts +++ b/packages/amplify-cli-core/src/state-manager/stateManager.ts @@ -2,6 +2,7 @@ import * as fs from 'fs-extra'; import { pathManager } from './pathManager'; import { $TSMeta, $TSTeamProviderInfo, $TSAny } from '..'; import { JSONUtilities } from '../jsonUtilities'; +import { Tag, ReadValidateTags } from '../tags'; export type GetOptions = { throwIfNotExist?: boolean; @@ -37,6 +38,10 @@ export class StateManager { return data; }; + getProjectTags = (projectPath?: string): Tag[] => ReadValidateTags(pathManager.getTagFilePath(projectPath)); + + getCurrentProjectTags = (projectPath?: string): Tag[] => ReadValidateTags(pathManager.getCurrentTagFilePath(projectPath)); + teamProviderInfoExists = (projectPath?: string): boolean => fs.existsSync(pathManager.getTeamProviderInfoFilePath(projectPath)); getTeamProviderInfo = (projectPath?: string, options?: GetOptions<$TSTeamProviderInfo>): $TSTeamProviderInfo => { @@ -105,6 +110,11 @@ export class StateManager { JSONUtilities.writeJson(filePath, localAWSInfo); }; + setProjectFileTags = (projectPath: string | undefined, tags: Tag[]): void => { + const tagFilePath = pathManager.getTagFilePath(projectPath); + JSONUtilities.writeJson(tagFilePath, tags); + }; + setProjectConfig = (projectPath: string | undefined, projectConfig: $TSAny): void => { const filePath = pathManager.getProjectConfigFilePath(projectPath); diff --git a/packages/amplify-cli-core/src/tags/Tags.ts b/packages/amplify-cli-core/src/tags/Tags.ts new file mode 100644 index 00000000000..dd64cc08d64 --- /dev/null +++ b/packages/amplify-cli-core/src/tags/Tags.ts @@ -0,0 +1,35 @@ +import { JSONUtilities } from '../jsonUtilities'; +import _ from 'lodash'; + +export interface Tag { + Key: string; + Value: String; +} + +export function ReadValidateTags(tagsFilePath: string): Tag[] { + const tags = JSONUtilities.readJson(tagsFilePath, { + throwIfNotExist: false, + preserveComments: false, + }); + + if (!tags) return []; + + validate(tags); + + return tags; +} + +export function validate(tags: Tag[]): void { + const set = new Set(['Key', 'Value']); + + //check if Tags have the right format + _.each(tags, tags => { + if (_.some(Object.keys(tags), r => !set.has(r))) throw new Error('Tag thould be of type Key: string, Value: string'); + }); + + //check if Tag Key is repeated + if (_.uniq(tags.map(r => r.Key)).length !== tags.length) throw new Error("'Key' should be unique"); + + //check If tags exceed limit + if (tags.length > 50) throw new Error('No. of tags cannot exceed 50'); +} diff --git a/packages/amplify-cli-core/src/tags/index.ts b/packages/amplify-cli-core/src/tags/index.ts new file mode 100644 index 00000000000..b7c10b6d563 --- /dev/null +++ b/packages/amplify-cli-core/src/tags/index.ts @@ -0,0 +1 @@ +export * from './Tags'; diff --git a/packages/amplify-cli/src/domain/amplify-toolkit.ts b/packages/amplify-cli/src/domain/amplify-toolkit.ts index 99425317bbf..1e0c06f091c 100644 --- a/packages/amplify-cli/src/domain/amplify-toolkit.ts +++ b/packages/amplify-cli/src/domain/amplify-toolkit.ts @@ -64,6 +64,7 @@ export class AmplifyToolkit { private _deleteAllTriggers: any; private _deleteDeselectedTriggers: any; private _dependsOnBlock: any; + private _getTags: any; private _getTriggerMetadata: any; private _getTriggerPermissions: any; private _getTriggerEnvVariables: any; @@ -370,6 +371,10 @@ export class AmplifyToolkit { this._dependsOnBlock = this._dependsOnBlock || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).dependsOnBlock; return this._dependsOnBlock; } + get getTags(): any { + this._getTags = this._getTags || require(path.join(this._amplifyHelpersDirPath, 'get-tags')).getTags; + return this._getTags; + } get getTriggerMetadata(): any { this._getTriggerMetadata = this._getTriggerMetadata || require(path.join(this._amplifyHelpersDirPath, 'trigger-flow')).getTriggerMetadata; diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/constants.ts b/packages/amplify-cli/src/extensions/amplify-helpers/constants.ts index b1d9d2cb8d3..412691aaf9c 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/constants.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/constants.ts @@ -16,6 +16,7 @@ export const amplifyCLIConstants = { LocalEnvFileName: 'local-env-info.json', ProviderInfoFileName: 'team-provider-info.json', BackendConfigFileName: 'backend-config.json', + TagsFileName: 'tags.json', PROJECT_CONFIG_VERSION: '3.0', BreadcrumbsFileName: 'amplify.state', }; diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/get-tags.ts b/packages/amplify-cli/src/extensions/amplify-helpers/get-tags.ts new file mode 100644 index 00000000000..2a96ae7b951 --- /dev/null +++ b/packages/amplify-cli/src/extensions/amplify-helpers/get-tags.ts @@ -0,0 +1,21 @@ +import { stateManager } from 'amplify-cli-core'; +import { getProjectDetails } from './get-project-details'; +export function getTags() { + const projectDetails = getProjectDetails(); + const { projectName } = projectDetails.projectConfig; + const { envName } = projectDetails.localEnvInfo; + return HydrateTags(stateManager.getProjectTags(), projectName, envName); +} + +function HydrateTags(tags: any[], envName: string, projectName: string) { + 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]), + }; + }); +} diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/resource-status.ts b/packages/amplify-cli/src/extensions/amplify-helpers/resource-status.ts index 4724b7dbf47..7d9b2b8df4d 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/resource-status.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/resource-status.ts @@ -7,7 +7,7 @@ import { hashElement } from 'folder-hash'; import { getEnvInfo } from './get-env-info'; import { CLOUD_INITIALIZED, CLOUD_NOT_INITIALIZED, getCloudInitStatus } from './get-cloud-init-status'; import { ServiceName as FunctionServiceName, hashLayerResource } from 'amplify-category-function'; -import { pathManager, stateManager, $TSMeta, $TSAny } from 'amplify-cli-core'; +import { pathManager, stateManager, $TSMeta, $TSAny, Tag } from 'amplify-cli-core'; async function isBackendDirModifiedSinceLastPush(resourceName, category, lastPushTimeStamp, isLambdaLayer = false) { // Pushing the resource for the first time hence no lastPushTimeStamp @@ -251,15 +251,31 @@ export async function getResourceStatus(category?, resourceName?, providerName?, resourcesToBeDeleted = resourcesToBeDeleted.filter(resource => resource.providerPlugin === providerName); allResources = allResources.filter(resource => resource.providerPlugin === providerName); } - + const tagsUpdated = compareTags(stateManager.getProjectTags(), stateManager.getCurrentProjectTags()); return { resourcesToBeCreated, resourcesToBeUpdated, resourcesToBeDeleted, + tagsUpdated, allResources, }; } +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(); if (amplifyProjectInitStatus === CLOUD_INITIALIZED) { @@ -270,7 +286,7 @@ export async function showResourceTable(category, resourceName, filteredResource print.info(''); } - const { resourcesToBeCreated, resourcesToBeUpdated, resourcesToBeDeleted, allResources } = await getResourceStatus( + const { resourcesToBeCreated, resourcesToBeUpdated, resourcesToBeDeleted, allResources, tagsUpdated } = await getResourceStatus( category, resourceName, undefined, @@ -321,7 +337,11 @@ export async function showResourceTable(category, resourceName, filteredResource const { table } = print; table(tableOptions, { format: 'markdown' }); + if (tagsUpdated) { + print.info('Resource Tags Update Detected'); + } + const changedResourceCount = + resourcesToBeCreated.length + resourcesToBeUpdated.length + resourcesToBeDeleted.length + tagsUpdated ? 1 : 0; - const changedResourceCount = resourcesToBeCreated.length + resourcesToBeUpdated.length + resourcesToBeDeleted.length; return changedResourceCount; } diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/update-amplify-meta.ts b/packages/amplify-cli/src/extensions/amplify-helpers/update-amplify-meta.ts index a1ecf88f5e3..4d269287cdd 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/update-amplify-meta.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/update-amplify-meta.ts @@ -38,6 +38,8 @@ function moveBackendResourcesToCurrentCloudBackend(resources) { const amplifyCloudMetaFilePath = pathManager.getCurrentAmplifyMetaFilePath(); const backendConfigFilePath = pathManager.getBackendConfigFilePath(); const backendConfigCloudFilePath = pathManager.getCurrentBackendConfigFilePath(); + const tagFilePath = pathManager.getTagFilePath(); + const tagCloudFilePath = pathManager.getCurrentTagFilePath(); for (let i = 0; i < resources.length; i += 1) { const sourceDir = path.normalize(path.join(pathManager.getBackendDirPath(), resources[i].category, resources[i].resourceName)); @@ -56,6 +58,7 @@ function moveBackendResourcesToCurrentCloudBackend(resources) { fs.copySync(amplifyMetaFilePath, amplifyCloudMetaFilePath, { overwrite: true }); fs.copySync(backendConfigFilePath, backendConfigCloudFilePath, { overwrite: true }); + fs.copySync(tagFilePath, tagCloudFilePath, { overwrite: true }); } export function updateamplifyMetaAfterResourceAdd(category, resourceName, options: { dependsOn? } = {}) { diff --git a/packages/amplify-cli/src/init-steps/s9-onSuccess.ts b/packages/amplify-cli/src/init-steps/s9-onSuccess.ts index 0ecaf48a475..da1c56ee313 100644 --- a/packages/amplify-cli/src/init-steps/s9-onSuccess.ts +++ b/packages/amplify-cli/src/init-steps/s9-onSuccess.ts @@ -66,6 +66,7 @@ export async function onSuccess(context: $TSContext) { function generateLocalRuntimeFiles(context: $TSContext) { generateLocalEnvInfoFile(context); generateAmplifyMetaFile(context); + generateLocalTagsFile(context); } export function generateLocalEnvInfoFile(context: $TSContext) { @@ -74,7 +75,24 @@ export function generateLocalEnvInfoFile(context: $TSContext) { stateManager.setLocalEnvInfo(projectPath, context.exeInfo.localEnvInfo); } -function generateAmplifyMetaFile(context: $TSContext) { +function generateLocalTagsFile(context) { + if (context.exeInfo.isNewProject) { + const { projectPath } = context.exeInfo.localEnvInfo; + const tags = [ + { + Key: 'user:Stack', + Value: '{project-env}', + }, + { + Key: 'user:Application', + Value: '{project-name}', + }, + ]; + stateManager.setProjectFileTags(projectPath, tags); + } +} + +export function generateAmplifyMetaFile(context: $TSContext) { if (context.exeInfo.isNewEnv) { const { projectPath } = context.exeInfo.localEnvInfo; diff --git a/packages/amplify-e2e-core/src/utils/projectMeta.ts b/packages/amplify-e2e-core/src/utils/projectMeta.ts index 543de6ffd7f..c0ef59ef648 100644 --- a/packages/amplify-e2e-core/src/utils/projectMeta.ts +++ b/packages/amplify-e2e-core/src/utils/projectMeta.ts @@ -26,6 +26,11 @@ function getProjectMeta(projectRoot: string) { return JSON.parse(fs.readFileSync(metaFilePath, 'utf8')); } +function getProjectTags(projectRoot: string) { + const metaFilePath = path.join(projectRoot, 'amplify', '#current-cloud-backend', 'tags.json'); + return JSON.parse(fs.readFileSync(metaFilePath, 'utf8')); +} + function getBackendAmplifyMeta(projectRoot: string) { const metaFilePath = path.join(projectRoot, 'amplify', 'backend', 'amplify-meta.json'); return JSON.parse(fs.readFileSync(metaFilePath, 'utf8')); @@ -51,6 +56,7 @@ function getAwsIOSConfig(projectRoot: string) { export { getProjectMeta, + getProjectTags, getBackendAmplifyMeta, getAwsAndroidConfig, getAwsIOSConfig, diff --git a/packages/amplify-e2e-tests/src/__tests__/tags.test.ts b/packages/amplify-e2e-tests/src/__tests__/tags.test.ts new file mode 100644 index 00000000000..b9797171260 --- /dev/null +++ b/packages/amplify-e2e-tests/src/__tests__/tags.test.ts @@ -0,0 +1,49 @@ +import { + initJSProjectWithProfile, + createNewProjectDir, + deleteProject, + deleteProjectDir, + amplifyPushWithoutCodegen, + getProjectMeta, + getProjectTags, + describeCloudFormationStack, + addDEVHosting, +} from 'amplify-e2e-core'; + +describe('generated tags test', () => { + let projRoot: string; + + beforeEach(async () => { + projRoot = await createNewProjectDir('tags'); + }); + + afterEach(async () => { + await deleteProject(projRoot); + deleteProjectDir(projRoot); + }); + + it('should compare the nested stack tags key with the tags.json file and return true', async () => { + await initJSProjectWithProfile(projRoot, {}); + await addDEVHosting(projRoot); + await amplifyPushWithoutCodegen(projRoot); + + // This block of code gets the necessary info to compare the values of both the local tags from the JSON file and tags on the stack + const amplifyMeta = getProjectMeta(projRoot); + const meta = amplifyMeta.providers.awscloudformation; + const rootStackInfo = await describeCloudFormationStack(meta.StackName, meta.Region); + const localTags = getProjectTags(projRoot); + + // Currently only checks to make sure that thhe pushed tags have the same amount and name of keys than the ones added locally on the tags.json file + expect(checkEquality(localTags, rootStackInfo.Tags)).toBe(true); + }); +}); + +// ? Not sure if this is the best way to indicate an array of objects in TS +function checkEquality(localTags: {}[], generatedTags: {}[]) { + localTags.forEach(tagObj => { + const rootTag = generatedTags.find(obj => obj['Key'] === tagObj['Key']); + if (tagObj['Key'] !== rootTag['Key']) return false; + }); + + return true; +} diff --git a/packages/amplify-provider-awscloudformation/src/aws-utils/aws-cfn.js b/packages/amplify-provider-awscloudformation/src/aws-utils/aws-cfn.js index b29ad9f0aed..2dff87c08c4 100644 --- a/packages/amplify-provider-awscloudformation/src/aws-utils/aws-cfn.js +++ b/packages/amplify-provider-awscloudformation/src/aws-utils/aws-cfn.js @@ -198,6 +198,8 @@ class CloudFormation { const authRoleName = projectDetails.amplifyMeta.providers ? projectDetails.amplifyMeta.providers[providerName].AuthRoleName : ''; const unauthRoleName = projectDetails.amplifyMeta.providers ? projectDetails.amplifyMeta.providers[providerName].UnauthRoleName : ''; + const Tags = this.context.amplify.getTags(); + if (!stackName) { throw new Error('Project stack has not been created yet. Use amplify init to initialize the project.'); } @@ -221,7 +223,6 @@ class CloudFormation { const cfnModel = this.cfn; const { context } = this; const self = this; - this.eventStartTime = new Date(); return new Promise((resolve, reject) => { this.describeStack(cfnStackCheckParams) @@ -244,6 +245,7 @@ class CloudFormation { ParameterValue: unauthRoleName, }, ], + Tags, }; cfnModel.updateStack(cfnParentStackParams, updateErr => { From 2bf0eb2486f31d86e75d450f3679b1c26990b7dd Mon Sep 17 00:00:00 2001 From: AmmarKarachi Date: Wed, 26 Aug 2020 16:25:57 -0700 Subject: [PATCH 2/3] refactor: refactored on suggestions --- packages/amplify-cli-core/src/__tests__/tags.test.ts | 8 +++----- packages/amplify-cli-core/src/tags/Tags.ts | 4 ++-- packages/amplify-cli/src/init-steps/s9-onSuccess.ts | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/amplify-cli-core/src/__tests__/tags.test.ts b/packages/amplify-cli-core/src/__tests__/tags.test.ts index 19692bc782b..b35b633e550 100644 --- a/packages/amplify-cli-core/src/__tests__/tags.test.ts +++ b/packages/amplify-cli-core/src/__tests__/tags.test.ts @@ -14,8 +14,6 @@ describe('tags-validation:', () => { }); describe('case: tags-validation receives a JSON file that contains more than 50 key-value pairs', () => { - const jsonObj: any = []; - const json = [ { Key: 'user:Stack', Value: 'dev', key: 'notgood' }, { Key: 'user:Application', Value: 'foobar' }, @@ -28,17 +26,17 @@ describe('tags-validation:', () => { }); describe('case: tags-validation receives a JSON file that contains more than 50 key-value pairs', () => { - const jsonObj: any = []; + const jsonObjects: any = []; for (let i = 0; i < 55; i++) { - jsonObj.push({ + jsonObjects.push({ Key: `user:key${i}`, Value: `value${i}`, }); } it('tags-validation should throw an error stating that the tags.json file has exceeded the tags amount limit', () => { - expect(() => validate(jsonObj)).toThrowError(new Error('No. of tags cannot exceed 50')); + expect(() => validate(jsonObjects)).toThrowError(new Error('No. of tags cannot exceed 50')); }); }); }); diff --git a/packages/amplify-cli-core/src/tags/Tags.ts b/packages/amplify-cli-core/src/tags/Tags.ts index dd64cc08d64..1df4e67e963 100644 --- a/packages/amplify-cli-core/src/tags/Tags.ts +++ b/packages/amplify-cli-core/src/tags/Tags.ts @@ -20,11 +20,11 @@ export function ReadValidateTags(tagsFilePath: string): Tag[] { } export function validate(tags: Tag[]): void { - const set = new Set(['Key', 'Value']); + const allowedKeySet = new Set(['Key', 'Value']); //check if Tags have the right format _.each(tags, tags => { - if (_.some(Object.keys(tags), r => !set.has(r))) throw new Error('Tag thould be of type Key: string, Value: string'); + if (_.some(Object.keys(tags), r => !allowedKeySet.has(r))) throw new Error('Tag thould be of type Key: string, Value: string'); }); //check if Tag Key is repeated diff --git a/packages/amplify-cli/src/init-steps/s9-onSuccess.ts b/packages/amplify-cli/src/init-steps/s9-onSuccess.ts index da1c56ee313..e76602da68a 100644 --- a/packages/amplify-cli/src/init-steps/s9-onSuccess.ts +++ b/packages/amplify-cli/src/init-steps/s9-onSuccess.ts @@ -75,7 +75,7 @@ export function generateLocalEnvInfoFile(context: $TSContext) { stateManager.setLocalEnvInfo(projectPath, context.exeInfo.localEnvInfo); } -function generateLocalTagsFile(context) { +function generateLocalTagsFile(context: $TSContext) { if (context.exeInfo.isNewProject) { const { projectPath } = context.exeInfo.localEnvInfo; const tags = [ From 758889f41de974ce1daa15385402b056a92817ed Mon Sep 17 00:00:00 2001 From: AmmarKarachi Date: Wed, 26 Aug 2020 18:47:55 -0700 Subject: [PATCH 3/3] refactor: change return type to bool --- .../src/extensions/amplify-helpers/resource-status.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/resource-status.ts b/packages/amplify-cli/src/extensions/amplify-helpers/resource-status.ts index 7d9b2b8df4d..f2b158df494 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/resource-status.ts +++ b/packages/amplify-cli/src/extensions/amplify-helpers/resource-status.ts @@ -340,8 +340,7 @@ export async function showResourceTable(category, resourceName, filteredResource if (tagsUpdated) { print.info('Resource Tags Update Detected'); } - const changedResourceCount = - resourcesToBeCreated.length + resourcesToBeUpdated.length + resourcesToBeDeleted.length + tagsUpdated ? 1 : 0; + const resourceChanged = resourcesToBeCreated.length + resourcesToBeUpdated.length + resourcesToBeDeleted.length > 0 || tagsUpdated; - return changedResourceCount; + return resourceChanged; }