├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── THIRD-PARTY-LICENSES.txt ├── cdk └── workshop │ ├── .gitignore │ ├── .npmignore │ ├── Makefile │ ├── README.md │ ├── bin │ └── workshop.ts │ ├── buildspec-destroy.yml │ ├── buildspec.yml │ ├── cdk.json │ ├── cfn.json │ ├── jest.config.js │ ├── lib │ ├── bootstrap-stack.ts │ ├── cdk-aspect │ │ └── destroy-policy-setter.ts │ ├── cloud9-stack.ts │ ├── constructs │ │ ├── admin │ │ │ └── admin-stack.ts │ │ ├── baseline-infra │ │ │ └── baseline-infra-stack.ts │ │ ├── generic │ │ │ ├── ddb-initializer.ts │ │ │ ├── ecr-initializer.ts │ │ │ └── userpool-user.ts │ │ └── tenant-infra │ │ │ ├── codebuild-role-policy-doc.ts │ │ │ └── tenant-infra-stack.ts │ ├── eks-stack.ts │ ├── helm-chart-stack.ts │ ├── instance-management-stack.ts │ ├── node-role-policy-doc.ts │ ├── root-stack.ts │ ├── send_cloud9_ssm │ │ ├── index.py │ │ └── requirements.txt │ └── update_instance_profile │ │ ├── index.py │ │ └── requirements.txt │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── client └── web │ ├── admin │ ├── .editorconfig │ ├── .gitignore │ ├── .yarn │ │ └── install-state.gz │ ├── .yarnrc.yml │ ├── Dockerfile │ ├── README.md │ ├── angular.json │ ├── config-asset-loader.ts │ ├── k8s │ │ ├── manifest.yaml │ │ └── template.txt │ ├── karma.conf.js │ ├── nginx.conf │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── _nav.ts │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.scss │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── models │ │ │ │ ├── index.ts │ │ │ │ └── interfaces.ts │ │ │ ├── nav │ │ │ │ ├── nav.component.css │ │ │ │ ├── nav.component.html │ │ │ │ └── nav.component.ts │ │ │ └── views │ │ │ │ ├── auth │ │ │ │ ├── auth.component.html │ │ │ │ ├── auth.component.scss │ │ │ │ └── auth.component.ts │ │ │ │ ├── dashboard │ │ │ │ ├── dashboard-routing.module.ts │ │ │ │ ├── dashboard.component.html │ │ │ │ ├── dashboard.component.scss │ │ │ │ ├── dashboard.component.ts │ │ │ │ └── dashboard.module.ts │ │ │ │ └── tenants │ │ │ │ ├── create │ │ │ │ ├── create.component.html │ │ │ │ ├── create.component.scss │ │ │ │ └── create.component.ts │ │ │ │ ├── detail │ │ │ │ ├── detail.component.html │ │ │ │ ├── detail.component.scss │ │ │ │ └── detail.component.ts │ │ │ │ ├── list │ │ │ │ ├── list.component.html │ │ │ │ ├── list.component.scss │ │ │ │ └── list.component.ts │ │ │ │ ├── models │ │ │ │ └── tenant.ts │ │ │ │ ├── tenants-routing.module.ts │ │ │ │ ├── tenants.module.ts │ │ │ │ └── tenants.service.ts │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ ├── config │ │ │ │ └── config.json │ │ │ └── logo.svg │ │ ├── custom-theme.scss │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.scss │ │ ├── styles │ │ │ ├── _variables.scss │ │ │ └── reset.scss │ │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── yarn.lock │ └── application │ ├── .editorconfig │ ├── .gitignore │ ├── .pnp.cjs │ ├── .pnp.loader.mjs │ ├── .yarn │ └── install-state.gz │ ├── Dockerfile │ ├── README.md │ ├── angular.json │ ├── config-asset-loader.ts │ ├── k8s │ ├── manifest.yaml │ ├── template.istio.txt │ └── template.txt │ ├── karma.conf.js │ ├── nginx.conf │ ├── package.json │ ├── src │ ├── app │ │ ├── _nav.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.scss │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── interceptors │ │ │ ├── auth.interceptor.ts │ │ │ └── index.ts │ │ ├── models │ │ │ ├── index.ts │ │ │ └── interfaces.ts │ │ ├── nav │ │ │ ├── nav.component.html │ │ │ ├── nav.component.scss │ │ │ └── nav.component.ts │ │ └── views │ │ │ ├── auth │ │ │ ├── auth.component.html │ │ │ ├── auth.component.scss │ │ │ └── auth.component.ts │ │ │ ├── dashboard │ │ │ ├── dashboard-routing.module.ts │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.scss │ │ │ ├── dashboard.component.ts │ │ │ └── dashboard.module.ts │ │ │ ├── orders │ │ │ ├── create │ │ │ │ ├── create.component.html │ │ │ │ ├── create.component.scss │ │ │ │ └── create.component.ts │ │ │ ├── detail │ │ │ │ ├── detail.component.html │ │ │ │ ├── detail.component.scss │ │ │ │ └── detail.component.ts │ │ │ ├── list │ │ │ │ ├── list.component.html │ │ │ │ ├── list.component.scss │ │ │ │ └── list.component.ts │ │ │ ├── models │ │ │ │ ├── order.interface.ts │ │ │ │ └── orderproduct.interface.ts │ │ │ ├── orders-routing.module.ts │ │ │ ├── orders.module.ts │ │ │ └── orders.service.ts │ │ │ └── products │ │ │ ├── create │ │ │ ├── create.component.html │ │ │ ├── create.component.scss │ │ │ └── create.component.ts │ │ │ ├── edit │ │ │ ├── edit.component.html │ │ │ ├── edit.component.scss │ │ │ └── edit.component.ts │ │ │ ├── list │ │ │ ├── list.component.html │ │ │ ├── list.component.scss │ │ │ └── list.component.ts │ │ │ ├── models │ │ │ └── product.interface.ts │ │ │ ├── product.service.ts │ │ │ ├── products-routing.module.ts │ │ │ └── products.module.ts │ ├── assets │ │ ├── .gitkeep │ │ ├── config │ │ │ └── config.json │ │ └── logo.svg │ ├── custom-theme.scss │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ ├── styles │ │ ├── _variables.scss │ │ └── reset.scss │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── yarn.lock ├── package-lock.json ├── scripts ├── at-an-event │ └── setup.sh ├── on-your-own │ ├── deploy-baseline.sh │ ├── deploy-eks.sh │ ├── install-karpenter.sh │ ├── setup.sh │ └── update-stack-services.sh ├── onboard-premium-tenant.sh ├── onboard-tenants.sh ├── policy │ └── deny-traffic-policy.yaml └── resize-cloud9-ebs-volume.sh ├── services ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── Dockerfile.order ├── Dockerfile.product ├── Dockerfile.tenant-management ├── Dockerfile.tenant-registration ├── Dockerfile.user-management ├── README.md ├── apps │ ├── application │ │ ├── order │ │ │ ├── k8s │ │ │ │ ├── manifest.yaml │ │ │ │ ├── template.istio.txt │ │ │ │ └── template.txt │ │ │ ├── src │ │ │ │ ├── main.ts │ │ │ │ └── orders │ │ │ │ │ ├── dto │ │ │ │ │ ├── create-order.dto.ts │ │ │ │ │ ├── order-product.dto.ts │ │ │ │ │ └── update-order.dto.ts │ │ │ │ │ ├── entities │ │ │ │ │ ├── order.entity.ts │ │ │ │ │ └── product.entity.ts │ │ │ │ │ ├── orders.controller.ts │ │ │ │ │ ├── orders.module.ts │ │ │ │ │ └── orders.service.ts │ │ │ ├── test │ │ │ │ └── jest-e2e.json │ │ │ └── tsconfig.app.json │ │ └── product │ │ │ ├── k8s │ │ │ ├── manifest.yaml │ │ │ ├── template.istio.txt │ │ │ └── template.txt │ │ │ ├── src │ │ │ ├── main.ts │ │ │ └── products │ │ │ │ ├── dto │ │ │ │ ├── create-product.dto.ts │ │ │ │ └── update-product.dto.ts │ │ │ │ ├── entities │ │ │ │ └── product.entity.ts │ │ │ │ ├── products.controller.ts │ │ │ │ ├── products.module.ts │ │ │ │ └── products.service.ts │ │ │ └── tsconfig.app.json │ └── shared │ │ ├── tenant-management │ │ ├── k8s │ │ │ ├── manifest.yaml │ │ │ ├── partial-template.txt │ │ │ ├── template.istio.txt │ │ │ └── template.txt │ │ ├── src │ │ │ ├── main.ts │ │ │ └── tenants │ │ │ │ ├── dto │ │ │ │ ├── create-tenant.dto.ts │ │ │ │ └── update-tenant.dto.ts │ │ │ │ ├── entities │ │ │ │ └── tenant.entity.ts │ │ │ │ ├── tenants.controller.ts │ │ │ │ ├── tenants.module.ts │ │ │ │ └── tenants.service.ts │ │ ├── test │ │ │ └── jest-e2e.json │ │ └── tsconfig.app.json │ │ ├── tenant-registration │ │ ├── k8s │ │ │ ├── manifest.yaml │ │ │ ├── partial-template.txt │ │ │ └── template.txt │ │ ├── src │ │ │ ├── idp-service │ │ │ │ └── idp.service.ts │ │ │ ├── main.ts │ │ │ ├── models │ │ │ │ └── types.ts │ │ │ ├── registration │ │ │ │ ├── constants.ts │ │ │ │ ├── dto │ │ │ │ │ ├── create-registration.dto.ts │ │ │ │ │ └── update-registration.dto.ts │ │ │ │ ├── entities │ │ │ │ │ └── registration.entity.ts │ │ │ │ ├── registration.controller.ts │ │ │ │ ├── registration.module.ts │ │ │ │ └── registration.service.ts │ │ │ └── utils │ │ │ │ └── utils.ts │ │ └── tsconfig.app.json │ │ └── user-management │ │ ├── k8s │ │ ├── manifest.yaml │ │ ├── partial-template.txt │ │ └── template.txt │ │ ├── src │ │ ├── main.ts │ │ └── users │ │ │ ├── dto │ │ │ └── create-tenant-user.dto.ts.ts │ │ │ ├── users.controller.ts │ │ │ ├── users.module.ts │ │ │ └── users.service.ts │ │ └── tsconfig.app.json ├── libs │ ├── auth │ │ ├── src │ │ │ ├── auth-config.ts │ │ │ ├── auth.decorator.ts │ │ │ ├── auth.module.ts │ │ │ ├── credential-vendor.ts │ │ │ ├── index.ts │ │ │ ├── jwt-auth.guard.ts │ │ │ ├── jwt.strategy.ts │ │ │ └── policies.json │ │ └── tsconfig.lib.json │ └── client-factory │ │ ├── src │ │ ├── client-factory.module.ts │ │ ├── client-factory.service.ts │ │ └── index.ts │ │ └── tsconfig.lib.json ├── nest-cli.json ├── package-lock.json ├── package.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock └── tenant-artifacts ├── README.md ├── buildspecPremium.yaml ├── buildspecStandard.yaml ├── deploy-pooled.sh ├── get_helm.sh ├── helm ├── basic-tier-chart │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ ├── application.yaml │ │ ├── destinationrule.yaml │ │ ├── order.yaml │ │ ├── product.yaml │ │ └── virtual-services.yaml │ └── values.yaml ├── core-services │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ ├── admin.yaml │ │ ├── istio-gateway.yaml │ │ ├── tenant-management.yaml │ │ ├── tenant-registration.yaml │ │ ├── user-management.yaml │ │ └── virtual-services.yaml │ └── values.yaml ├── premium-tier-chart │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ ├── application.yaml │ │ ├── order.yaml │ │ ├── product.yaml │ │ ├── provisioner.yaml │ │ └── virtual-services.yaml │ └── values.yaml └── standard-tier-chart │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── application.yaml │ ├── order.yaml │ ├── product.yaml │ └── virtual-services.yaml │ └── values.yaml ├── set-core-values.sh ├── set-values.sh ├── template.txt └── update-provisioning-status.sh /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SaaS Factory SaaS EKS Workshop 2 | 3 | The code provided here is intended to accompany the SaaS Factory EKS Workshop. The workshop is a series of hands-on labs through which participants build up, step-by-step, a functioning multi-tenant EKS environment. This workshop explores key architectural strategies that are used to address the various key tenets of SaaS solutions including isolation, identity, data partitioning, routing, deployment, and operational considerations and specifically how these are realized in EKS. Once complete with this workshop, participants will have a better understanding of the EKS-flavor of multi-tenancy, and be better able to understand the EKS SaaS reference solution, also produced by the SaaS Factory team. 4 | 5 | [Workshop Narrative Here](https://catalog.us-east-1.prod.workshops.aws/workshops/02d7d511-9af8-417f-abda-3708e9c0c749/en-US) 6 | 7 | -------------------------------------------------------------------------------- /cdk/workshop/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | dist -------------------------------------------------------------------------------- /cdk/workshop/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /cdk/workshop/Makefile: -------------------------------------------------------------------------------- 1 | ZIPFILE ?= eks-workshop-stack-app.zip 2 | S3_ASSET_BUCKET ?= ws-assets-us-east-1 3 | S3_ASSET_PREFIX ?= 02d7d511-9af8-417f-abda-3708e9c0c749/ 4 | S3_URL := s3://$(S3_ASSET_BUCKET)/$(S3_ASSET_PREFIX)$(ZIPFILE) 5 | 6 | .PHONY: upload 7 | upload: $(ZIPFILE) 8 | @echo Note: If this fails, ensure the proper AWS credentials are set in the environment. These can be found in the Event Engine module configuration. 9 | aws s3 cp $(ZIPFILE) $(S3_URL) 10 | 11 | zip: $(ZIPFILE) 12 | 13 | $(ZIPFILE): $(shell git ls-files) 14 | rm -f $@ 15 | cd ../.. && git ls-files | xargs zip cdk/workshop/$@ 16 | 17 | .PHONY: clean 18 | clean: 19 | rm -f $(ZIPFILE) 20 | find . -name '*.js' ! -name 'jest.config.js' -not -path './node_modules/*' -delete 21 | find . -name '*.d.ts' -not -path './node_modules/*' -delete 22 | rm -rf cdk.out/ 23 | 24 | .PHONY: build 25 | build: 26 | npm run build 27 | 28 | .PHONY: synth-bootstrap 29 | synth-bootstrap: build 30 | npx cdk synth BootstrapStack 31 | 32 | .PHONY: deploy 33 | deploy: build upload 34 | npx cdk deploy BootstrapStack --require-approval=never \ 35 | --previous-parameters=false \ 36 | --parameters BootstrapStack:EEAssetsBucket=$(S3_ASSET_BUCKET) \ 37 | --parameters BootstrapStack:EEAssetsKeyPrefix=$(S3_ASSET_PREFIX) \ 38 | --parameters BootstrapStack:SourceZipFile=$(ZIPFILE) \ 39 | --parameters BootstrapStack:SourceZipFileChecksum=$$(openssl sha256 -hex -r $(ZIPFILE) | cut -d' ' -f1) 40 | 41 | .PHONY: destroy 42 | destroy: 43 | npx cdk destroy BootstrapStack --force -------------------------------------------------------------------------------- /cdk/workshop/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project 2 | 3 | This is a blank project for CDK development with TypeScript. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `cdk deploy` deploy this stack to your default AWS account/region 13 | * `cdk diff` compare deployed stack with current state 14 | * `cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /cdk/workshop/bin/workshop.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { RootStack } from '../lib/root-stack'; 5 | import { DestroyPolicySetter } from '../lib/cdk-aspect/destroy-policy-setter'; 6 | import { BootstrapStack } from '../lib/bootstrap-stack'; 7 | 8 | const app = new cdk.App(); 9 | 10 | new BootstrapStack(app, 'BootstrapStack', { 11 | sourceZipFile: process.env.ZIPFILE || 'eks-workshop-stack-app.zip', 12 | sourceZipFileChecksum: process.env.ZIPFILE_CHECKSUM || '', 13 | }); 14 | const wsParticipantRoleArn = process.env.WS_PARTICIPANT_ROLE_ARN; 15 | const rootStack = new RootStack(app, 'root-stack', { 16 | clusterName: 'eksworkshop-eksctl', 17 | cloud9EnvironmentName: 'eks-saas-workshop', 18 | wsParticipantRoleArn, 19 | }); 20 | 21 | //cdk.Aspects.of(eksStack).add(new DestroyPolicySetter()); 22 | cdk.Aspects.of(rootStack).add(new DestroyPolicySetter()); 23 | -------------------------------------------------------------------------------- /cdk/workshop/buildspec-destroy.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | VPC_ID: VPC_ID_NOT_SET 6 | CLOUD9_ENVIRONMENT_ID: CLOUD9_ENVIRONMENT_ID_NOT_SET 7 | CFN_RESPONSE_URL: CFN_RESPONSE_URL_NOT_SET 8 | CFN_STACK_ID: CFN_STACK_ID_NOT_SET 9 | CFN_REQUEST_ID: CFN_REQUEST_ID_NOT_SET 10 | CFN_LOGICAL_RESOURCE_ID: CFN_LOGICAL_RESOURCE_ID_NOT_SET 11 | 12 | phases: 13 | install: 14 | on-failure: ABORT 15 | runtime-versions: 16 | nodejs: 18 17 | commands: 18 | - cd $CODEBUILD_SRC_DIR/cdk/workshop 19 | - npm install 20 | pre_build: 21 | on-failure: ABORT 22 | commands: 23 | - cd $CODEBUILD_SRC_DIR/cdk/workshop 24 | - npm run build 25 | - "export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)" 26 | - 'echo "AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID"' 27 | - npx cdk bootstrap aws://$AWS_ACCOUNT_ID/$AWS_REGION 28 | build: 29 | on-failure: ABORT 30 | commands: 31 | - cd $CODEBUILD_SRC_DIR/cdk/workshop 32 | - "export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)" 33 | - 'echo "AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID"' 34 | - 'echo "VPC ID: $VPC_ID"' 35 | - 'echo "CLOUD9_ENVIRONMENT_ID: $CLOUD9_ENVIRONMENT_ID"' 36 | - "eval $(aws cloudformation describe-stacks --stack-name root-stack --query 'Stacks[0].Outputs[?contains(OutputKey, `ClusterConfigCommand`)].OutputValue' --output text | sed -e 's/--role-arn.*//')" 37 | - "/tmp/kubectl delete -f attack/complete-demo.yaml || :" 38 | - npx cdk destroy root-stack --force 39 | -------------------------------------------------------------------------------- /cdk/workshop/buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 0.2 2 | 3 | env: 4 | variables: 5 | VPC_ID: VPC_ID_NOT_SET 6 | CLOUD9_ENVIRONMENT_ID: CLOUD9_ENVIRONMENT_ID_NOT_SET 7 | CFN_RESPONSE_URL: CFN_RESPONSE_URL_NOT_SET 8 | CFN_STACK_ID: CFN_STACK_ID_NOT_SET 9 | CFN_REQUEST_ID: CFN_REQUEST_ID_NOT_SET 10 | CFN_LOGICAL_RESOURCE_ID: CFN_LOGICAL_RESOURCE_ID_NOT_SET 11 | 12 | phases: 13 | install: 14 | on-failure: ABORT 15 | runtime-versions: 16 | nodejs: 18 17 | commands: 18 | - cd $CODEBUILD_SRC_DIR/cdk/workshop 19 | - npm install 20 | pre_build: 21 | on-failure: ABORT 22 | commands: 23 | - cd $CODEBUILD_SRC_DIR/cdk/workshop 24 | - npm run build 25 | - "export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)" 26 | - 'echo "AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID"' 27 | - npx cdk bootstrap aws://$AWS_ACCOUNT_ID/$AWS_REGION 28 | build: 29 | on-failure: ABORT 30 | commands: 31 | - cd $CODEBUILD_SRC_DIR/cdk/workshop 32 | - "export AWS_ACCOUNT_ID=$(echo $CODEBUILD_BUILD_ARN | cut -d: -f5)" 33 | - 'echo "AWS_ACCOUNT_ID: $AWS_ACCOUNT_ID"' 34 | - 'echo "VPC ID: $VPC_ID"' 35 | - 'echo "CLOUD9_ENVIRONMENT_ID: $CLOUD9_ENVIRONMENT_ID"' 36 | - 'echo "WS_PARTICIPANT_ROLE_ARN: $WS_PARTICIPANT_ROLE_ARN"' 37 | - npx cdk deploy root-stack --require-approval never --no-rollback 38 | -------------------------------------------------------------------------------- /cdk/workshop/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /cdk/workshop/lib/cdk-aspect/destroy-policy-setter.ts: -------------------------------------------------------------------------------- 1 | import { IConstruct } from "constructs"; 2 | import { CfnResource, IAspect, RemovalPolicy } from "aws-cdk-lib"; 3 | 4 | export class DestroyPolicySetter implements IAspect { 5 | public visit(node: IConstruct): void { 6 | if (node instanceof CfnResource) { 7 | node.applyRemovalPolicy(RemovalPolicy.DESTROY); 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /cdk/workshop/lib/constructs/generic/ddb-initializer.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { CfnUserPoolUserToGroupAttachment, IUserPool } from 'aws-cdk-lib/aws-cognito'; 3 | import { 4 | AwsCustomResource, 5 | AwsCustomResourcePolicy, 6 | AwsSdkCall, 7 | PhysicalResourceId, 8 | } from 'aws-cdk-lib/custom-resources'; 9 | import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 10 | import { RetentionDays } from 'aws-cdk-lib/aws-logs'; 11 | import { Duration } from 'aws-cdk-lib'; 12 | 13 | export class DynamoDbInitializer extends Construct { 14 | constructor( 15 | scope: Construct, 16 | id: string, 17 | props: { tableName: string; tableArn: string; records: any } 18 | ) { 19 | super(scope, id); 20 | this.insertRecord(props.tableName, props.tableArn, props.records); 21 | } 22 | 23 | private insertRecord(tableName: string, tableArn: string, item: any) { 24 | const awsSdkCall: AwsSdkCall = { 25 | service: 'DynamoDB', 26 | action: 'putItem', 27 | physicalResourceId: PhysicalResourceId.of(tableName + '_insert'), 28 | parameters: { 29 | TableName: tableName, 30 | Item: item, 31 | }, 32 | }; 33 | const customResource: AwsCustomResource = new AwsCustomResource( 34 | this, 35 | tableName + '_custom_resource', 36 | { 37 | onCreate: awsSdkCall, 38 | onUpdate: awsSdkCall, 39 | logRetention: RetentionDays.ONE_WEEK, 40 | policy: AwsCustomResourcePolicy.fromStatements([ 41 | new PolicyStatement({ 42 | sid: 'DynamoWriteAccess', 43 | effect: Effect.ALLOW, 44 | actions: ['dynamodb:PutItem'], 45 | resources: [tableArn], 46 | }), 47 | ]), 48 | timeout: Duration.minutes(5), 49 | } 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cdk/workshop/lib/constructs/generic/ecr-initializer.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as ecr from 'aws-cdk-lib/aws-ecr'; 3 | import { Construct } from 'constructs'; 4 | 5 | export interface EcrRepositoriesProps { 6 | repositoryNames: string[]; 7 | } 8 | 9 | export class EcrRepositories extends Construct { 10 | constructor(scope: Construct, id: string, props: EcrRepositoriesProps) { 11 | super(scope, id); 12 | 13 | // Create ECR repositories from provided names 14 | props.repositoryNames.forEach((repoName, index) => { 15 | new ecr.Repository(this, `Repository${index + 1}`, { 16 | repositoryName: repoName, 17 | removalPolicy: cdk.RemovalPolicy.DESTROY, 18 | imageScanOnPush: false 19 | }); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cdk/workshop/lib/constructs/tenant-infra/codebuild-role-policy-doc.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import * as iam from 'aws-cdk-lib/aws-iam'; 7 | import { Construct } from 'constructs'; 8 | 9 | export function getCodeBuildRole(parent: Construct, account: string, region: string): iam.Role { 10 | return new iam.Role(parent, 'CodeBuildRole', { 11 | assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'), 12 | description: 'Role assigned to our tenant onboarding code build project', 13 | inlinePolicies: { 14 | TenantOnboardingPolicy: getCodeBuildPolicyDoc(account, region), 15 | }, 16 | }); 17 | } 18 | 19 | function getCodeBuildPolicyDoc(account: string, region: string): iam.PolicyDocument { 20 | return new iam.PolicyDocument({ 21 | assignSids: false, 22 | statements: [ 23 | new iam.PolicyStatement({ 24 | effect: iam.Effect.ALLOW, 25 | actions: ['sts:AssumeRole'], 26 | resources: [`arn:aws:iam::${account}:role/*`], 27 | }), 28 | new iam.PolicyStatement({ 29 | effect: iam.Effect.ALLOW, 30 | actions: ['cloudformation:DescribeStacks'], 31 | resources: [`arn:aws:cloudformation:${region}:${account}:stack/*`], 32 | }), 33 | ], 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /cdk/workshop/lib/instance-management-stack.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, NestedStack, NestedStackProps, Stack, StackProps } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as iam from 'aws-cdk-lib/aws-iam'; 4 | 5 | export class InstanceManagementStack extends NestedStack { 6 | instanceRoleArn: string; 7 | 8 | constructor(scope: Construct, id: string, props?: NestedStackProps) { 9 | super(scope, id, props); 10 | // Create an EC2 instance role for the Cloud9 environment. This instance 11 | // role is powerful, allowing the participant to have unfettered access to 12 | // the provisioned account. This might be too broad. It's possible to 13 | // tighten this down, but there may be unintended consequences. 14 | // We'll need this role later when we run aws eks update-kubeconfig 15 | const instanceRole = new iam.Role(this, 'WorkspaceInstanceRole', { 16 | assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'), 17 | managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')], 18 | description: 'Workspace EC2 instance role', 19 | }); 20 | instanceRole.addManagedPolicy( 21 | iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore') 22 | ); 23 | 24 | this.instanceRoleArn = instanceRole.roleArn; 25 | new CfnOutput(this, 'InstanceRoleArn', { value: this.instanceRoleArn }); 26 | new CfnOutput(this, 'WorkspaceInstanceRoleName', { value: instanceRole.roleName }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cdk/workshop/lib/send_cloud9_ssm/requirements.txt: -------------------------------------------------------------------------------- 1 | crhelper 2 | -------------------------------------------------------------------------------- /cdk/workshop/lib/update_instance_profile/requirements.txt: -------------------------------------------------------------------------------- 1 | crhelper 2 | -------------------------------------------------------------------------------- /cdk/workshop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workshop", 3 | "version": "0.1.0", 4 | "bin": "bin/workshop.js", 5 | "scripts": { 6 | "build": "tsc", 7 | "watch": "tsc -w", 8 | "test": "jest", 9 | "cdk": "cdk" 10 | }, 11 | "devDependencies": { 12 | "@types/jest": "^29.5.6", 13 | "@types/node": "20.8.9", 14 | "aws-cdk": "2.103.1", 15 | "jest": "^29.7.0", 16 | "ts-jest": "^29.1.1", 17 | "ts-node": "^10.9.1", 18 | "typescript": "~5.2.2" 19 | }, 20 | "dependencies": { 21 | "@aws-cdk/aws-cloud9-alpha": "^2.103.1-alpha.0", 22 | "@aws-cdk/aws-lambda-python-alpha": "^2.103.1-alpha.0", 23 | "@aws-cdk/lambda-layer-kubectl-v27": "^2.0.0", 24 | "@types/aws-lambda": "^8.10.125", 25 | "@types/js-yaml": "^4.0.5", 26 | "aws-cdk-lib": "2.103.1", 27 | "aws-sdk": "^2.1484.0", 28 | "cdk-eks-karpenter": "^0.0.41", 29 | "constructs": "^10.3.0", 30 | "js-yaml": "^4.1.0", 31 | "source-map-support": "^0.5.21" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cdk/workshop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": ["es2020", "dom"], 6 | "declaration": true, 7 | "outDir": "dist", 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "strictNullChecks": true, 11 | "noImplicitThis": true, 12 | "alwaysStrict": true, 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": false, 17 | "inlineSourceMap": true, 18 | "inlineSources": true, 19 | "experimentalDecorators": true, 20 | "strictPropertyInitialization": false, 21 | "typeRoots": ["./node_modules/@types"] 22 | }, 23 | "exclude": ["node_modules", "cdk.out", "dist"] 24 | } 25 | -------------------------------------------------------------------------------- /client/web/admin/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /client/web/admin/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | #amplify-do-not-edit-begin 45 | amplify/\#current-cloud-backend 46 | amplify/.config/local-* 47 | amplify/logs 48 | amplify/mock-data 49 | amplify/backend/amplify-meta.json 50 | amplify/backend/.temp 51 | build/ 52 | dist/ 53 | node_modules/ 54 | aws-exports.js 55 | awsconfiguration.json 56 | amplifyconfiguration.json 57 | amplifyconfiguration.dart 58 | amplify-build-config.json 59 | amplify-gradle-config.json 60 | amplifytools.xcconfig 61 | .secret-* 62 | **.sample 63 | #amplify-do-not-edit-end 64 | -------------------------------------------------------------------------------- /client/web/admin/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-saas-factory-eks-saas-workshop/3f33453451e3d47a8f770448b9eed80f5fb85fcc/client/web/admin/.yarn/install-state.gz -------------------------------------------------------------------------------- /client/web/admin/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | -------------------------------------------------------------------------------- /client/web/admin/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/bitnami/node:18.18.0 AS build 2 | WORKDIR /usr/src/app 3 | COPY package.json ./ 4 | RUN yarn 5 | COPY . . 6 | RUN yarn build --configuration production 7 | 8 | FROM public.ecr.aws/nginx/nginx:1.23 9 | COPY nginx.conf /etc/nginx/conf.d/default.conf 10 | COPY --from=build /usr/src/app/dist /usr/share/nginx/html/admin 11 | -------------------------------------------------------------------------------- /client/web/admin/README.md: -------------------------------------------------------------------------------- 1 | # Dashboard 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /client/web/admin/config-asset-loader.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { shareReplay } from 'rxjs/operators'; 5 | 6 | interface Configuration { 7 | apiUrl: string; 8 | stage: string; 9 | } 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class ConfigAssetLoaderService { 13 | private readonly CONFIG_URL = './assets/config/config.json'; 14 | private configuration$: Observable | undefined; 15 | 16 | constructor(private http: HttpClient) {} 17 | 18 | public loadConfigurations(): Observable { 19 | console.log('IN LOADCONFIGURATIONS'); 20 | if (!this.configuration$) { 21 | this.configuration$ = this.http 22 | .get(this.CONFIG_URL) 23 | .pipe(shareReplay(1)); 24 | } 25 | return this.configuration$; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/web/admin/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/dashboard'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /client/web/admin/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | location /admin { 5 | alias /usr/share/nginx/html/admin/; 6 | index index.html; 7 | try_files $uri $uri/ index.html =404; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/web/admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "upgrade": "ng update", 9 | "watch": "ng build --watch --configuration development", 10 | "test": "ng test" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^15.2.10", 15 | "@angular/cdk": "14.2.7", 16 | "@angular/common": "^15.2.10", 17 | "@angular/compiler": "^15.2.10", 18 | "@angular/core": "^15.2.10", 19 | "@angular/forms": "^15.2.10", 20 | "@angular/material": "14.2.7", 21 | "@angular/platform-browser": "^15.2.10", 22 | "@angular/platform-browser-dynamic": "^15.2.10", 23 | "@angular/router": "^15.2.10", 24 | "@aws-amplify/ui-angular": "^3.2.19", 25 | "aws-amplify": "^4.3.46", 26 | "bootstrap": "5.3.2", 27 | "chart.js": "^3.9.1", 28 | "chartjs-plugin-datalabels": "^2.2.0", 29 | "ng2-charts": "^4.1.1", 30 | "rxjs": "~7.8.1", 31 | "tslib": "^2.6.2", 32 | "zone.js": "~0.13.3" 33 | }, 34 | "devDependencies": { 35 | "@angular-devkit/build-angular": "^15.2.10", 36 | "@angular/cli": "~15.2.10", 37 | "@angular/compiler-cli": "^15.2.10", 38 | "@types/jasmine": "~4.3.6", 39 | "jasmine-core": "~4.1.1", 40 | "karma": "~6.4.2", 41 | "karma-chrome-launcher": "~3.2.0", 42 | "karma-coverage": "~2.2.1", 43 | "karma-jasmine": "~5.1.0", 44 | "karma-jasmine-html-reporter": "~2.0.0", 45 | "typescript": "~4.9.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /client/web/admin/src/app/_nav.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { INavData } from './models'; 6 | 7 | export const navItems: INavData[] = [ 8 | { 9 | name: 'Dashboard', 10 | url: '/dashboard', 11 | icon: 'insights', 12 | }, 13 | { 14 | name: 'Tenants', 15 | url: '/tenants', 16 | icon: 'groups', 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /client/web/admin/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { NavComponent } from './nav/nav.component'; 4 | import { AuthComponent } from './views/auth/auth.component'; 5 | 6 | export const routes: Routes = [ 7 | { 8 | path: '', 9 | redirectTo: 'dashboard', 10 | pathMatch: 'full', 11 | }, 12 | { 13 | path: '', 14 | component: NavComponent, 15 | data: { 16 | title: 'Home', 17 | }, 18 | children: [ 19 | { 20 | path: 'auth/info', 21 | component: AuthComponent, 22 | }, 23 | { 24 | path: 'dashboard', 25 | loadChildren: () => 26 | import('./views/dashboard/dashboard.module').then( 27 | (m) => m.DashboardModule 28 | ), 29 | }, 30 | { 31 | path: 'tenants', 32 | loadChildren: () => 33 | import('./views/tenants/tenants.module').then((m) => m.TenantsModule), 34 | }, 35 | ], 36 | }, 37 | ]; 38 | 39 | @NgModule({ 40 | imports: [RouterModule.forRoot(routes)], 41 | exports: [RouterModule], 42 | }) 43 | export class AppRoutingModule {} 44 | -------------------------------------------------------------------------------- /client/web/admin/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-saas-factory-eks-saas-workshop/3f33453451e3d47a8f770448b9eed80f5fb85fcc/client/web/admin/src/app/app.component.scss -------------------------------------------------------------------------------- /client/web/admin/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatIconRegistry } from '@angular/material/icon'; 3 | import { DomSanitizer } from '@angular/platform-browser'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | template: ` 8 | 13 | 14 | 15 | `, 16 | styleUrls: ['./app.component.scss'], 17 | }) 18 | export class AppComponent { 19 | constructor( 20 | private matIconRegistry: MatIconRegistry, 21 | private domSanitizer: DomSanitizer 22 | ) { 23 | this.matIconRegistry.addSvgIcon( 24 | 'saas-commerce', 25 | this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') 26 | ); 27 | } 28 | title = 'dashboard'; 29 | } 30 | -------------------------------------------------------------------------------- /client/web/admin/src/app/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | -------------------------------------------------------------------------------- /client/web/admin/src/app/models/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface INavData { 2 | name?: string; 3 | url?: string | any[]; 4 | href?: string; 5 | icon?: string; 6 | title?: boolean; 7 | children?: INavData[]; 8 | variant?: string; 9 | divider?: boolean; 10 | class?: string; 11 | } 12 | -------------------------------------------------------------------------------- /client/web/admin/src/app/nav/nav.component.css: -------------------------------------------------------------------------------- 1 | .sidenav-container { 2 | height: 100%; 3 | } 4 | 5 | .sidenav-content-container { 6 | background-color: lightgray; 7 | } 8 | 9 | .sidenav { 10 | width: 200px; 11 | background-color: #2f353a; 12 | } 13 | 14 | .mat-list-item { 15 | color: whitesmoke; 16 | } 17 | 18 | .mat-toolbar.mat-primary { 19 | position: sticky; 20 | top: 0; 21 | z-index: 1; 22 | } 23 | 24 | .spacer { 25 | flex: 1 1 auto; 26 | } 27 | 28 | .material-symbols-outlined { 29 | font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; 30 | } 31 | 32 | .logo { 33 | width: 100px; 34 | height: auto; 35 | } 36 | 37 | .nav-icon { 38 | color: #20a8d8; 39 | } 40 | 41 | .spinner-container { 42 | height: 80%; 43 | display: flex; 44 | justify-content: center; 45 | align-items: center; 46 | box-sizing: border-box; 47 | } 48 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/auth/auth.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 8 |
9 |
10 |
11 |
12 | 13 | Access Token 14 |
15 |
16 |               
17 |                 {{accessToken$ | async}}
18 |               
19 |             
20 |
21 |
22 |
23 |
24 | 25 | ID Token 26 |
27 |
28 |               
29 |                 {{idToken$ | async}}
30 |               
31 |             
32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 | 40 |
41 |
42 | 43 |

User Data

44 | Is Authenticated: {{ isAuthenticated$ | async }} 45 |
{{ session$ | async | json }}
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/auth/auth.component.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | * software and associated documentation files (the "Software"), to deal in the Software 6 | * without restriction, including without limitation the rights to use, copy, modify, 7 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | * permit persons to whom the Software is furnished to do so. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | pre, 18 | code { 19 | font-family: monospace, monospace; 20 | } 21 | pre { 22 | white-space: pre-wrap; /* css-3 */ 23 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 24 | white-space: -pre-wrap; /* Opera 4-6 */ 25 | white-space: -o-pre-wrap; /* Opera 7 */ 26 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 27 | overflow: auto; 28 | } 29 | 30 | .card { 31 | margin: 20px; 32 | } 33 | 34 | .card-header { 35 | justify-content: center; 36 | font-size: larger; 37 | font-weight: bold; 38 | } 39 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/auth/auth.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { from, Observable, pipe } from 'rxjs'; 3 | import { Auth } from 'aws-amplify'; 4 | import { CognitoUserSession } from 'amazon-cognito-identity-js'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | @Component({ 8 | templateUrl: './auth.component.html', 9 | styleUrls: ['./auth.component.scss'], 10 | }) 11 | export class AuthComponent implements OnInit { 12 | session$: Observable | undefined; 13 | userData$: Observable | undefined; 14 | isAuthenticated$: Observable | undefined; 15 | checkSessionChanged$: Observable | undefined; 16 | idToken$: Observable | undefined; 17 | accessToken$: Observable | undefined; 18 | checkSessionChanged: any; 19 | 20 | constructor() {} 21 | 22 | ngOnInit(): void { 23 | this.session$ = from(Auth.currentSession()); 24 | this.accessToken$ = this.session$.pipe( 25 | map((sesh) => sesh.getAccessToken().getJwtToken()) 26 | ); 27 | this.idToken$ = this.session$.pipe( 28 | map((sesh) => sesh.getIdToken().getJwtToken()) 29 | ); 30 | this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); 31 | } 32 | 33 | async logout() { 34 | await Auth.signOut({ global: true }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { Routes, RouterModule } from '@angular/router'; 7 | 8 | import { DashboardComponent } from './dashboard.component'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: DashboardComponent, 14 | data: { 15 | title: 'Dashboard', 16 | }, 17 | }, 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [RouterModule.forChild(routes)], 22 | exports: [RouterModule], 23 | }) 24 | export class DashboardRoutingModule {} 25 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |

Traffic

6 |
December 2020
7 |
8 |
9 | 10 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | margin: 20px; 3 | } 4 | 5 | .dashboard-card { 6 | position: absolute; 7 | top: 15px; 8 | left: 15px; 9 | right: 15px; 10 | bottom: 15px; 11 | } 12 | 13 | .more-button { 14 | position: absolute; 15 | top: 5px; 16 | right: 10px; 17 | border: none; 18 | } 19 | 20 | .dashboard-card-content { 21 | text-align: center; 22 | } 23 | 24 | .chart-wrapper { 25 | padding-right: 20px; 26 | position: relative; 27 | margin: auto; 28 | height: 80vh; 29 | width: 80vw; 30 | } 31 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { DashboardComponent } from './dashboard.component'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatGridListModule } from '@angular/material/grid-list'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatListModule } from '@angular/material/list'; 9 | import { MatMenuModule } from '@angular/material/menu'; 10 | 11 | import { NgChartsModule } from 'ng2-charts'; 12 | 13 | import { DashboardRoutingModule } from './dashboard-routing.module'; 14 | 15 | @NgModule({ 16 | declarations: [DashboardComponent], 17 | imports: [ 18 | CommonModule, 19 | DashboardRoutingModule, 20 | MatButtonModule, 21 | MatCardModule, 22 | MatGridListModule, 23 | MatIconModule, 24 | MatListModule, 25 | MatMenuModule, 26 | NgChartsModule, 27 | ], 28 | }) 29 | export class DashboardModule {} 30 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/create/create.component.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | margin: 20px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: flex-start; 6 | width: fit-content; 7 | } 8 | 9 | .product-form { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: flex-start; 13 | } 14 | 15 | .mat-card-content { 16 | width: 100%; 17 | } 18 | 19 | .mat-form-field { 20 | flex-flow: column; 21 | display: flex; 22 | } 23 | 24 | .button-panel { 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | margin-bottom: 8px; 29 | } 30 | 31 | button { 32 | margin: 4px; 33 | } 34 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/create/create.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { 3 | FormBuilder, 4 | FormControl, 5 | FormGroup, 6 | Validators, 7 | } from '@angular/forms'; 8 | import { Router } from '@angular/router'; 9 | import { environment } from 'src/environments/environment'; 10 | import { TenantsService } from '../tenants.service'; 11 | 12 | @Component({ 13 | selector: 'app-create', 14 | templateUrl: './create.component.html', 15 | styleUrls: ['./create.component.scss'], 16 | }) 17 | export class CreateComponent implements OnInit { 18 | submitting = false; 19 | tenantForm = new FormGroup({ 20 | name: new FormControl('', [Validators.required]), 21 | email: new FormControl('', [Validators.required]), 22 | companyName: new FormControl('', [Validators.required]), 23 | plan: new FormControl('', [Validators.required]), 24 | }); 25 | constructor(private tenantSvc: TenantsService, private router: Router) {} 26 | 27 | ngOnInit(): void {} 28 | 29 | submit() { 30 | this.submitting = true; 31 | const user = { 32 | ...this.tenantForm.value, 33 | }; 34 | 35 | this.tenantSvc.post(user).subscribe({ 36 | next: () => { 37 | this.submitting = false; 38 | this.router.navigate(['tenants']); 39 | }, 40 | error: (err: any) => { 41 | console.error(err); 42 | this.submitting = false; 43 | }, 44 | }); 45 | } 46 | 47 | public get name() { 48 | return this.tenantForm.get('name'); 49 | } 50 | 51 | public get email() { 52 | return this.tenantForm.get('email'); 53 | } 54 | 55 | public get companyName() { 56 | return this.tenantForm.get('companyName'); 57 | } 58 | 59 | public get plan() { 60 | return this.tenantForm.get('plan'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/detail/detail.component.html: -------------------------------------------------------------------------------- 1 |

detail works!

2 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/detail/detail.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-saas-factory-eks-saas-workshop/3f33453451e3d47a8f770448b9eed80f5fb85fcc/client/web/admin/src/app/views/tenants/detail/detail.component.scss -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/detail/detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-detail', 5 | templateUrl: './detail.component.html', 6 | styleUrls: ['./detail.component.scss'] 7 | }) 8 | export class DetailComponent implements OnInit { 9 | 10 | constructor() { } 11 | 12 | ngOnInit(): void { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/list/list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Tenant List

3 |
4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
Id{{ element.tenant_id }}Company Name{{ element.companyName }}E-Mail{{ element.email }}Plan{{ element.plan }}
30 |
31 | 39 |
40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/list/list.component.scss: -------------------------------------------------------------------------------- 1 | .tenant-list { 2 | margin: 20px; 3 | } 4 | table { 5 | width: 100%; 6 | } 7 | 8 | .button-panel { 9 | margin-top: 20px; 10 | } 11 | 12 | .mat-cell { 13 | margin: 20px 5px; 14 | } 15 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/list/list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { Tenant } from '../models/tenant'; 4 | import { TenantsService } from '../tenants.service'; 5 | 6 | @Component({ 7 | selector: 'app-list', 8 | templateUrl: './list.component.html', 9 | styleUrls: ['./list.component.scss'], 10 | }) 11 | export class ListComponent implements OnInit { 12 | tenants$ = new Observable(); 13 | displayedColumns = ['tenant_id', 'companyName', 'email', 'plan']; 14 | constructor(private tenantSvc: TenantsService) {} 15 | 16 | ngOnInit(): void { 17 | this.tenants$ = this.tenantSvc.fetch(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/models/tenant.ts: -------------------------------------------------------------------------------- 1 | export interface Tenant { 2 | tenant_id?: string; 3 | companyName?: string | undefined | null; 4 | email?: string | undefined | null; 5 | plan?: string | undefined | null; 6 | } 7 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/tenants-routing.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { Routes, RouterModule } from '@angular/router'; 7 | import { CreateComponent } from './create/create.component'; 8 | 9 | import { ListComponent } from './list/list.component'; 10 | 11 | const routes: Routes = [ 12 | { 13 | path: '', 14 | redirectTo: 'list', 15 | pathMatch: 'full', 16 | }, 17 | { 18 | path: 'list', 19 | component: ListComponent, 20 | data: { 21 | title: 'Tenant List', 22 | }, 23 | }, 24 | { 25 | path: 'create', 26 | component: CreateComponent, 27 | data: { 28 | title: 'Provision new tenant', 29 | }, 30 | }, 31 | ]; 32 | 33 | @NgModule({ 34 | imports: [RouterModule.forChild(routes)], 35 | exports: [RouterModule], 36 | }) 37 | export class TenantsRoutingModule {} 38 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/tenants.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatCardModule } from '@angular/material/card'; 7 | import { MatDatepickerModule } from '@angular/material/datepicker'; 8 | import { MatDialogModule } from '@angular/material/dialog'; 9 | import { 10 | MatFormFieldModule, 11 | MAT_FORM_FIELD_DEFAULT_OPTIONS, 12 | } from '@angular/material/form-field'; 13 | import { MatIconModule } from '@angular/material/icon'; 14 | import { MatInputModule } from '@angular/material/input'; 15 | import { MatRadioModule } from '@angular/material/radio'; 16 | import { MatSelectModule } from '@angular/material/select'; 17 | import { MatTableModule } from '@angular/material/table'; 18 | 19 | import { DetailComponent } from './detail/detail.component'; 20 | import { ListComponent } from './list/list.component'; 21 | import { TenantsRoutingModule } from './tenants-routing.module'; 22 | import { CreateComponent } from './create/create.component'; 23 | 24 | @NgModule({ 25 | declarations: [DetailComponent, ListComponent, CreateComponent], 26 | imports: [ 27 | CommonModule, 28 | TenantsRoutingModule, 29 | MatButtonModule, 30 | MatCardModule, 31 | MatDatepickerModule, 32 | MatDialogModule, 33 | MatFormFieldModule, 34 | MatIconModule, 35 | MatInputModule, 36 | MatRadioModule, 37 | MatSelectModule, 38 | MatTableModule, 39 | ReactiveFormsModule, 40 | ], 41 | providers: [ 42 | { 43 | provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, 44 | useValue: { appearance: 'outline' }, 45 | }, 46 | ], 47 | }) 48 | export class TenantsModule {} 49 | -------------------------------------------------------------------------------- /client/web/admin/src/app/views/tenants/tenants.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { HttpClient } from '@angular/common/http'; 6 | import { Injectable } from '@angular/core'; 7 | import { Observable } from 'rxjs'; 8 | import { environment } from 'src/environments/environment'; 9 | 10 | import { Tenant } from './models/tenant'; 11 | 12 | @Injectable({ 13 | providedIn: 'root', 14 | }) 15 | export class TenantsService { 16 | constructor(private http: HttpClient) {} 17 | baseUrl = `${environment.apiUrl}/api`; 18 | tenantsApiUrl = `${this.baseUrl}/tenants`; 19 | registrationApiUrl = `${this.baseUrl}/registration`; 20 | 21 | // TODO strongly-type these anys as tenants once we dial in what the tenant call should return 22 | fetch(): Observable { 23 | return this.http.get(this.tenantsApiUrl); 24 | } 25 | 26 | post(tenant: Tenant): Observable { 27 | return this.http.post(this.registrationApiUrl, tenant); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/web/admin/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-saas-factory-eks-saas-workshop/3f33453451e3d47a8f770448b9eed80f5fb85fcc/client/web/admin/src/assets/.gitkeep -------------------------------------------------------------------------------- /client/web/admin/src/assets/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiUrl": "//a25b5b5c96dd347a386cc663c23bb4ba-8fab114c7f1be266.elb.us-east-1.amazonaws.com", 3 | "stage": "develop", 4 | "amplifyConfig": { 5 | "aws_project_region": "us-east-1", 6 | "aws_cognito_region": "us-east-1", 7 | "aws_user_pools_id": "us-east-1_dPKm93kvh", 8 | "aws_user_pools_web_client_id": "6rcgor54bpfr813qj43m793lsh" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/web/admin/src/custom-theme.scss: -------------------------------------------------------------------------------- 1 | 2 | // Custom Theming for Angular Material 3 | // For more information: https://material.angular.io/guide/theming 4 | @use '@angular/material' as mat; 5 | // Plus imports for other components in your app. 6 | 7 | // Include the common styles for Angular Material. We include this here so that you only 8 | // have to load a single css file for Angular Material in your app. 9 | // Be sure that you only ever include this mixin once! 10 | @include mat.core(); 11 | 12 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 13 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 14 | // hue. Available color palettes: https://material.io/design/color/ 15 | $dashboard-primary: mat.define-palette(mat.$indigo-palette); 16 | $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); 17 | 18 | // The warn palette is optional (defaults to red). 19 | $dashboard-warn: mat.define-palette(mat.$red-palette); 20 | 21 | // Create the theme object. A theme consists of configurations for individual 22 | // theming systems such as "color" or "typography". 23 | $dashboard-theme: mat.define-light-theme(( 24 | color: ( 25 | primary: $dashboard-primary, 26 | accent: $dashboard-accent, 27 | warn: $dashboard-warn, 28 | ) 29 | )); 30 | 31 | // Include theme styles for core and each component used in your app. 32 | // Alternatively, you can import and @include the theme mixins for each component 33 | // that you are using. 34 | @include mat.all-component-themes($dashboard-theme); 35 | 36 | 37 | html, body { height: 100%; } 38 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 39 | -------------------------------------------------------------------------------- /client/web/admin/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | apiUrl: '', 4 | }; 5 | -------------------------------------------------------------------------------- /client/web/admin/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | apiUrl: '', 8 | }; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /client/web/admin/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-saas-factory-eks-saas-workshop/3f33453451e3d47a8f770448b9eed80f5fb85fcc/client/web/admin/src/favicon.ico -------------------------------------------------------------------------------- /client/web/admin/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | Dashboard 13 | 14 | 15 | 16 | 17 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/web/admin/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /client/web/admin/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); 3 | @import "@aws-amplify/ui-angular/theme.css"; 4 | 5 | @import "styles/variables"; 6 | $enable-dark-mode: false; 7 | // Import functions, variables, and mixins needed by other Bootstrap files 8 | 9 | @import "bootstrap/scss/functions"; 10 | @import "bootstrap/scss/variables"; 11 | @import "bootstrap/scss/maps"; 12 | @import "bootstrap/scss/mixins"; 13 | 14 | // Import Bootstrap Reboot 15 | @import "bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files 16 | @import "bootstrap/scss/reboot"; 17 | 18 | @import "bootstrap/scss/containers"; // Add .container and .container-fluid classes 19 | @import "bootstrap/scss/grid"; // Add the grid system 20 | 21 | @import "bootstrap/scss/utilities"; // Configures the utility classes that should be generated 22 | @import "bootstrap/scss/utilities/api"; // Generates the actual utility classes 23 | 24 | @import "styles/reset"; 25 | -------------------------------------------------------------------------------- /client/web/admin/src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $link-color: #673ab7; // 1 2 | $label-margin-bottom: 0; // 2 3 | $grid-breakpoints: ( 4 | xs: 0, 5 | // handset portrait (small, medium, large) | handset landscape (small) 6 | sm: 600px, 7 | // handset landscape (medium, large) | tablet portrait (small, large) 8 | md: 960px, 9 | // tablet landscape (small, large) 10 | lg: 1280px, 11 | // laptops and desktops 12 | xl: 1600px // large desktops, 13 | ); 14 | 15 | $container-max-widths: ( 16 | sm: 600px, 17 | md: 960px, 18 | lg: 1280px, 19 | xl: 1600px, 20 | ); 21 | -------------------------------------------------------------------------------- /client/web/admin/src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | a { 2 | &.mat-button, 3 | &.mat-raised-button, 4 | &.mat-fab, 5 | &.mat-mini-fab, 6 | &.mat-list-item { 7 | &:hover { 8 | color: currentColor; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/web/admin/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), 14 | ); 15 | -------------------------------------------------------------------------------- /client/web/admin/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /client/web/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "strictNullChecks": false, 20 | "target": "ES2022", 21 | "module": "es2020", 22 | "lib": ["es2020", "dom"], 23 | "useDefineForClassFields": false 24 | }, 25 | "angularCompilerOptions": { 26 | "enableI18nLegacyMessageIdFormat": false, 27 | "strictInjectionParameters": true, 28 | "strictInputAccessModifiers": true, 29 | "strictTemplates": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/web/admin/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /client/web/application/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /client/web/application/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | #amplify-do-not-edit-begin 45 | amplify/\#current-cloud-backend 46 | amplify/.config/local-* 47 | amplify/logs 48 | amplify/mock-data 49 | amplify/backend/amplify-meta.json 50 | amplify/backend/.temp 51 | build/ 52 | dist/ 53 | node_modules/ 54 | aws-exports.js 55 | awsconfiguration.json 56 | amplifyconfiguration.json 57 | amplifyconfiguration.dart 58 | amplify-build-config.json 59 | amplify-gradle-config.json 60 | amplifytools.xcconfig 61 | .secret-* 62 | **.sample 63 | #amplify-do-not-edit-end 64 | -------------------------------------------------------------------------------- /client/web/application/.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-saas-factory-eks-saas-workshop/3f33453451e3d47a8f770448b9eed80f5fb85fcc/client/web/application/.yarn/install-state.gz -------------------------------------------------------------------------------- /client/web/application/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/bitnami/node:18.18.0 AS build 2 | WORKDIR /usr/src/app 3 | COPY package.json ./ 4 | RUN yarn 5 | COPY . . 6 | RUN yarn build --configuration production 7 | 8 | FROM public.ecr.aws/nginx/nginx:1.23 9 | COPY nginx.conf /etc/nginx/conf.d/default.conf 10 | COPY --from=build /usr/src/app/dist /usr/share/nginx/html/app 11 | -------------------------------------------------------------------------------- /client/web/application/README.md: -------------------------------------------------------------------------------- 1 | # Dashboard 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.0.5. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /client/web/application/config-asset-loader.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | import { shareReplay } from 'rxjs/operators'; 5 | 6 | interface Configuration { 7 | apiUrl: string; 8 | stage: string; 9 | } 10 | 11 | @Injectable({ providedIn: 'root' }) 12 | export class ConfigAssetLoaderService { 13 | private readonly CONFIG_URL = './assets/config/config.json'; 14 | private configuration$: Observable | undefined; 15 | 16 | constructor(private http: HttpClient) {} 17 | 18 | public loadConfigurations(): Observable { 19 | console.log('IN LOADCONFIGURATIONS'); 20 | if (!this.configuration$) { 21 | this.configuration$ = this.http 22 | .get(this.CONFIG_URL) 23 | .pipe(shareReplay(1)); 24 | } 25 | return this.configuration$; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/web/application/k8s/manifest.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | config.json: | 4 | { 5 | "apiUrl": "//a6827acb0d6ef4172b966aa6ba3da833-2faefb68832aa130.elb.us-west-2.amazonaws.com", 6 | "stage": "develop" 7 | } 8 | kind: ConfigMap 9 | metadata: 10 | name: app-config 11 | --- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | name: application 16 | spec: 17 | replicas: 1 18 | selector: 19 | matchLabels: 20 | app: application 21 | template: 22 | metadata: 23 | labels: 24 | app: application 25 | spec: 26 | containers: 27 | - name: application 28 | image: public.ecr.aws/o2b5n0j5/eks-saas-application:latest 29 | resources: 30 | requests: 31 | cpu: 1 32 | imagePullPolicy: Always 33 | ports: 34 | - containerPort: 80 35 | name: "http" 36 | volumeMounts: 37 | - name: config-volume 38 | mountPath: /usr/share/nginx/html/app/assets/config 39 | volumes: 40 | - name: config-volume 41 | configMap: 42 | name: app-config 43 | --- 44 | apiVersion: v1 45 | kind: Service 46 | metadata: 47 | name: application-service 48 | spec: 49 | selector: 50 | app: application 51 | ports: 52 | - name: http 53 | protocol: TCP 54 | port: 80 55 | targetPort: 80 56 | type: NodePort 57 | --- 58 | apiVersion: networking.k8s.io/v1 59 | kind: Ingress 60 | metadata: 61 | name: application-service-ingress 62 | annotations: 63 | kubernetes.io/ingress.class: "nginx" 64 | nginx.org/mergeable-ingress-type: "minion" 65 | spec: 66 | rules: 67 | - host: a6827acb0d6ef4172b966aa6ba3da833-2faefb68832aa130.elb.us-west-2.amazonaws.com 68 | http: 69 | paths: 70 | - backend: 71 | service: 72 | name: application-service 73 | port: 74 | number: 80 75 | path: /app 76 | pathType: Prefix 77 | -------------------------------------------------------------------------------- /client/web/application/k8s/template.istio.txt: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | config.json: | 4 | { 5 | "apiUrl": "//$ELBURL", 6 | "stage": "develop" 7 | } 8 | kind: ConfigMap 9 | metadata: 10 | name: app-config 11 | --- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | name: application 16 | spec: 17 | replicas: 1 18 | selector: 19 | matchLabels: 20 | app: application 21 | template: 22 | metadata: 23 | labels: 24 | app: application 25 | spec: 26 | containers: 27 | - name: application 28 | image: public.ecr.aws/o2b5n0j5/eks-saas-application:latest 29 | imagePullPolicy: Always 30 | ports: 31 | - containerPort: 80 32 | name: "http" 33 | volumeMounts: 34 | - name: config-volume 35 | mountPath: /usr/share/nginx/html/app/assets/config 36 | volumes: 37 | - name: config-volume 38 | configMap: 39 | name: app-config 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: application-service 45 | spec: 46 | selector: 47 | app: application 48 | ports: 49 | - name: http 50 | protocol: TCP 51 | port: 80 52 | targetPort: 80 53 | type: NodePort 54 | 55 | -------------------------------------------------------------------------------- /client/web/application/k8s/template.txt: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | config.json: | 4 | { 5 | "apiUrl": "//$ELBURL", 6 | "stage": "develop" 7 | } 8 | kind: ConfigMap 9 | metadata: 10 | name: app-config 11 | --- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | name: application 16 | spec: 17 | replicas: 1 18 | selector: 19 | matchLabels: 20 | app: application 21 | template: 22 | metadata: 23 | labels: 24 | app: application 25 | spec: 26 | containers: 27 | - name: application 28 | image: public.ecr.aws/o2b5n0j5/eks-saas-application:latest 29 | resources: 30 | requests: 31 | cpu: 1 32 | imagePullPolicy: Always 33 | ports: 34 | - containerPort: 80 35 | name: "http" 36 | volumeMounts: 37 | - name: config-volume 38 | mountPath: /usr/share/nginx/html/app/assets/config 39 | tolerations: 40 | - key: "premiumnodes" 41 | operator: "Exists" 42 | effect: "NoSchedule" 43 | nodeSelector: 44 | tenant-tier: premium 45 | volumes: 46 | - name: config-volume 47 | configMap: 48 | name: app-config 49 | --- 50 | apiVersion: v1 51 | kind: Service 52 | metadata: 53 | name: application-service 54 | spec: 55 | selector: 56 | app: application 57 | ports: 58 | - name: http 59 | protocol: TCP 60 | port: 80 61 | targetPort: 80 62 | type: NodePort 63 | --- 64 | apiVersion: networking.k8s.io/v1 65 | kind: Ingress 66 | metadata: 67 | name: application-service-ingress 68 | annotations: 69 | kubernetes.io/ingress.class: "nginx" 70 | nginx.org/mergeable-ingress-type: "minion" 71 | spec: 72 | rules: 73 | - host: $ELBURL 74 | http: 75 | paths: 76 | - backend: 77 | service: 78 | name: application-service 79 | port: 80 | number: 80 81 | path: /$TENANTPATH 82 | pathType: Prefix 83 | -------------------------------------------------------------------------------- /client/web/application/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/dashboard'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /client/web/application/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | location ~(?:\/.*?)(\/.*) { 5 | alias /usr/share/nginx/html/app/; 6 | index /index.html; 7 | try_files $uri $uri/ $1 index.html =404; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /client/web/application/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^15.2.10", 14 | "@angular/cdk": "14.2.7", 15 | "@angular/common": "^15.2.10", 16 | "@angular/compiler": "^15.2.10", 17 | "@angular/core": "^15.2.10", 18 | "@angular/forms": "^15.2.10", 19 | "@angular/material": "14.2.7", 20 | "@angular/platform-browser": "^15.2.10", 21 | "@angular/platform-browser-dynamic": "^15.2.10", 22 | "@angular/router": "^15.2.10", 23 | "@aws-amplify/ui-angular": "^3.2.19", 24 | "aws-amplify": "^4.3.46", 25 | "bootstrap": "5.3.2", 26 | "chart.js": "^3.9.1", 27 | "chartjs-plugin-datalabels": "^2.2.0", 28 | "ng2-charts": "^4.1.1", 29 | "rxjs": "~7.8.1", 30 | "tslib": "^2.6.2", 31 | "zone.js": "~0.13.3" 32 | }, 33 | "devDependencies": { 34 | "@angular-devkit/build-angular": "^15.2.10", 35 | "@angular/cli": "~15.2.10", 36 | "@angular/compiler-cli": "^15.2.10", 37 | "@types/jasmine": "~4.0.3", 38 | "jasmine-core": "~4.1.1", 39 | "karma": "~6.4.2", 40 | "karma-chrome-launcher": "~3.2.0", 41 | "karma-coverage": "~2.2.1", 42 | "karma-jasmine": "~5.1.0", 43 | "karma-jasmine-html-reporter": "~2.0.0", 44 | "typescript": "~4.9.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/web/application/src/app/_nav.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { INavData } from './models'; 6 | 7 | export const navItems: INavData[] = [ 8 | { 9 | name: 'Dashboard', 10 | url: '/dashboard', 11 | icon: 'insights', 12 | }, 13 | { 14 | name: 'Products', 15 | url: '/products', 16 | icon: 'sell', 17 | }, 18 | { 19 | name: 'Orders', 20 | url: '/orders', 21 | icon: 'shopping_cart', 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /client/web/application/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { NavComponent } from './nav/nav.component'; 4 | import { AuthComponent } from './views/auth/auth.component'; 5 | 6 | export const routes: Routes = [ 7 | { 8 | path: '', 9 | redirectTo: 'dashboard', 10 | pathMatch: 'full', 11 | }, 12 | // { 13 | // path: '404', 14 | // component: P404Component, 15 | // data: { 16 | // title: 'Page 404', 17 | // }, 18 | // }, 19 | // { 20 | // path: '500', 21 | // component: P500Component, 22 | // data: { 23 | // title: 'Page 500', 24 | // }, 25 | // }, 26 | { 27 | path: '', 28 | component: NavComponent, 29 | data: { 30 | title: 'Home', 31 | }, 32 | children: [ 33 | { 34 | path: 'auth/info', 35 | component: AuthComponent, 36 | }, 37 | { 38 | path: 'dashboard', 39 | loadChildren: () => 40 | import('./views/dashboard/dashboard.module').then( 41 | (m) => m.DashboardModule 42 | ), 43 | }, 44 | { 45 | path: 'orders', 46 | loadChildren: () => 47 | import('./views/orders/orders.module').then((m) => m.OrdersModule), 48 | }, 49 | { 50 | path: 'products', 51 | loadChildren: () => 52 | import('./views/products/products.module').then( 53 | (m) => m.ProductsModule 54 | ), 55 | }, 56 | // { 57 | // path: 'users', 58 | // loadChildren: () => 59 | // import('./users/users.module').then((m) => m.UsersModule), 60 | // }, 61 | ], 62 | }, 63 | // { path: '**', component: P404Component }, 64 | ]; 65 | 66 | @NgModule({ 67 | imports: [RouterModule.forRoot(routes)], 68 | exports: [RouterModule], 69 | }) 70 | export class AppRoutingModule {} 71 | -------------------------------------------------------------------------------- /client/web/application/src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-saas-factory-eks-saas-workshop/3f33453451e3d47a8f770448b9eed80f5fb85fcc/client/web/application/src/app/app.component.scss -------------------------------------------------------------------------------- /client/web/application/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatIconRegistry } from '@angular/material/icon'; 3 | import { DomSanitizer } from '@angular/platform-browser'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | template: ` 8 | 13 | 14 | 15 | `, 16 | styleUrls: ['./app.component.scss'], 17 | }) 18 | export class AppComponent { 19 | constructor( 20 | private matIconRegistry: MatIconRegistry, 21 | private domSanitizer: DomSanitizer 22 | ) { 23 | this.matIconRegistry.addSvgIcon( 24 | 'saas-commerce', 25 | this.domSanitizer.bypassSecurityTrustResourceUrl('./assets/logo.svg') 26 | ); 27 | } 28 | title = 'dashboard'; 29 | } 30 | -------------------------------------------------------------------------------- /client/web/application/src/app/interceptors/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Injectable } from '@angular/core'; 6 | import { 7 | HttpRequest, 8 | HttpHandler, 9 | HttpEvent, 10 | HttpInterceptor, 11 | } from '@angular/common/http'; 12 | import { from, Observable } from 'rxjs'; 13 | import { Auth } from 'aws-amplify'; 14 | import { filter, map, switchMap } from 'rxjs/operators'; 15 | 16 | @Injectable() 17 | export class AuthInterceptor implements HttpInterceptor { 18 | constructor() {} 19 | idToken = ''; 20 | 21 | intercept( 22 | req: HttpRequest, 23 | next: HttpHandler 24 | ): Observable> { 25 | if (req.url.includes('auth-info') || req.url.includes('config.json')) { 26 | return next.handle(req); 27 | } 28 | const s = Auth.currentSession().catch((err) => console.log(err)); 29 | const session$ = from(s); 30 | 31 | return session$.pipe( 32 | filter((sesh) => !!sesh), 33 | map((sesh) => (!!sesh ? sesh.getIdToken().getJwtToken() : '')), 34 | switchMap((tok) => { 35 | req = req.clone({ 36 | headers: req.headers.set('Authorization', 'Bearer ' + tok), 37 | }); 38 | return next.handle(req); 39 | }) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/web/application/src/app/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { HTTP_INTERCEPTORS } from '@angular/common/http'; 6 | 7 | import { AuthInterceptor } from './auth.interceptor'; 8 | 9 | /** Http interceptor providers in outside-in order */ 10 | export const httpInterceptorProviders = [ 11 | { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, 12 | ]; 13 | -------------------------------------------------------------------------------- /client/web/application/src/app/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interfaces'; 2 | -------------------------------------------------------------------------------- /client/web/application/src/app/models/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface INavData { 2 | name?: string; 3 | url?: string | any[]; 4 | href?: string; 5 | icon?: string; 6 | title?: boolean; 7 | children?: INavData[]; 8 | variant?: string; 9 | divider?: boolean; 10 | class?: string; 11 | } 12 | -------------------------------------------------------------------------------- /client/web/application/src/app/nav/nav.component.scss: -------------------------------------------------------------------------------- 1 | .sidenav { 2 | width: 200px; 3 | background-color: #2f353a; 4 | } 5 | 6 | .mat-list-item { 7 | color: whitesmoke; 8 | } 9 | 10 | .sidebar-icon-container { 11 | height: 64px; 12 | background-color: whitesmoke; 13 | display: flex; 14 | align-items: center; 15 | justify-content: center; 16 | } 17 | 18 | .spacer { 19 | flex: 1 1 auto; 20 | } 21 | 22 | .material-symbols-outlined { 23 | font-variation-settings: "FILL" 0, "wght" 400, "GRAD" 0, "opsz" 48; 24 | } 25 | 26 | .logo { 27 | width: 100px; 28 | height: auto; 29 | } 30 | 31 | .nav-icon { 32 | color: #20a8d8; 33 | } 34 | 35 | .footer { 36 | position: fixed; 37 | bottom: 0; 38 | width: 100%; 39 | height: 40px; 40 | background-color: whitesmoke; 41 | color: #2d3337; 42 | // text-align: center; 43 | margin: 0px; 44 | } 45 | 46 | .footer-text { 47 | display: flex; 48 | } 49 | 50 | .content { 51 | position: absolute; 52 | width: 100%; 53 | height: 90%; 54 | max-height: 90%; 55 | overflow: auto; 56 | background-color: lightgray; 57 | } 58 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/auth/auth.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 8 |
9 |
10 |
11 |
12 | 13 | Access Token 14 |
15 |
16 |               
17 |                 {{accessToken$ | async}}
18 |               
19 |             
20 |
21 |
22 |
23 |
24 | 25 | ID Token 26 |
27 |
28 |               
29 |                 {{idToken$ | async}}
30 |               
31 |             
32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 | 40 |
41 |
42 | 43 |

User Data

44 | Is Authenticated: {{ isAuthenticated$ | async }} 45 |
{{ session$ | async | json }}
46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/auth/auth.component.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | * software and associated documentation files (the "Software"), to deal in the Software 6 | * without restriction, including without limitation the rights to use, copy, modify, 7 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | * permit persons to whom the Software is furnished to do so. 9 | * 10 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | pre, 18 | code { 19 | font-family: monospace, monospace; 20 | } 21 | pre { 22 | white-space: pre-wrap; /* css-3 */ 23 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 24 | white-space: -pre-wrap; /* Opera 4-6 */ 25 | white-space: -o-pre-wrap; /* Opera 7 */ 26 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 27 | overflow: auto; 28 | } 29 | 30 | .card { 31 | margin: 20px; 32 | } 33 | 34 | .card-header { 35 | justify-content: center; 36 | font-size: larger; 37 | font-weight: bold; 38 | } 39 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/auth/auth.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { from, Observable, pipe } from 'rxjs'; 3 | import { Auth } from 'aws-amplify'; 4 | import { CognitoUserSession } from 'amazon-cognito-identity-js'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | @Component({ 8 | templateUrl: './auth.component.html', 9 | styleUrls: ['./auth.component.scss'], 10 | }) 11 | export class AuthComponent implements OnInit { 12 | session$: Observable | undefined; 13 | userData$: Observable | undefined; 14 | isAuthenticated$: Observable | undefined; 15 | checkSessionChanged$: Observable | undefined; 16 | idToken$: Observable | undefined; 17 | accessToken$: Observable | undefined; 18 | checkSessionChanged: any; 19 | 20 | constructor() {} 21 | 22 | ngOnInit(): void { 23 | this.session$ = from(Auth.currentSession()); 24 | this.accessToken$ = this.session$.pipe( 25 | map((sesh) => sesh.getAccessToken().getJwtToken()) 26 | ); 27 | this.idToken$ = this.session$.pipe( 28 | map((sesh) => sesh.getIdToken().getJwtToken()) 29 | ); 30 | this.isAuthenticated$ = this.session$.pipe(map((sesh) => sesh.isValid())); 31 | } 32 | 33 | async logout() { 34 | await Auth.signOut({ global: true }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/dashboard/dashboard-routing.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { Routes, RouterModule } from '@angular/router'; 7 | 8 | import { DashboardComponent } from './dashboard.component'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: DashboardComponent, 14 | data: { 15 | title: 'Dashboard', 16 | }, 17 | }, 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [RouterModule.forChild(routes)], 22 | exports: [RouterModule], 23 | }) 24 | export class DashboardRoutingModule {} 25 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Dashboard

3 | 4 | 9 | 10 | 11 | 12 | {{ card.title }} 13 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | .grid-container { 2 | margin: 20px; 3 | } 4 | 5 | .dashboard-card { 6 | position: absolute; 7 | top: 15px; 8 | left: 15px; 9 | right: 15px; 10 | bottom: 15px; 11 | } 12 | 13 | .more-button { 14 | position: absolute; 15 | top: 5px; 16 | right: 10px; 17 | border: none; 18 | } 19 | 20 | .dashboard-card-content { 21 | text-align: center; 22 | } 23 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/dashboard/dashboard.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { DashboardComponent } from './dashboard.component'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatGridListModule } from '@angular/material/grid-list'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatListModule } from '@angular/material/list'; 9 | import { MatMenuModule } from '@angular/material/menu'; 10 | 11 | import { NgChartsModule } from 'ng2-charts'; 12 | 13 | import { DashboardRoutingModule } from './dashboard-routing.module'; 14 | 15 | @NgModule({ 16 | declarations: [DashboardComponent], 17 | imports: [ 18 | CommonModule, 19 | DashboardRoutingModule, 20 | MatButtonModule, 21 | MatCardModule, 22 | MatGridListModule, 23 | MatIconModule, 24 | MatListModule, 25 | MatMenuModule, 26 | NgChartsModule, 27 | ], 28 | }) 29 | export class DashboardModule {} 30 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/orders/create/create.component.scss: -------------------------------------------------------------------------------- 1 | // .card { 2 | // margin: 20px; 3 | // display: flex; 4 | // flex-direction: column; 5 | // align-items: flex-start; 6 | // } 7 | 8 | .order-form { 9 | margin: 20px; 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | 14 | .mat-card-title { 15 | display: flex; 16 | justify-content: center; 17 | } 18 | 19 | .button-panel { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | margin-bottom: 8px; 24 | } 25 | 26 | .row { 27 | width: 100%; 28 | align-items: center; 29 | } 30 | 31 | button { 32 | margin: 4px; 33 | } 34 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/orders/list/list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Order List

3 |
4 |
5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 |
Name 10 | 11 | {{ element.name }} 12 | 13 | Line Items 19 | {{ element.products?.length }} 20 | Total 26 | {{ sum(element) | currency }} 27 |
33 |
34 | 42 |
43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/orders/list/list.component.scss: -------------------------------------------------------------------------------- 1 | .order-list { 2 | margin: 20px; 3 | } 4 | table { 5 | width: 100%; 6 | } 7 | 8 | .button-panel { 9 | margin-top: 20px; 10 | } 11 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/orders/list/list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Observable, of } from 'rxjs'; 4 | import { Order } from '../models/order.interface'; 5 | import { OrdersService } from '../orders.service'; 6 | 7 | @Component({ 8 | selector: 'app-list', 9 | templateUrl: './list.component.html', 10 | styleUrls: ['./list.component.scss'], 11 | }) 12 | export class ListComponent implements OnInit { 13 | displayedColumns: string[] = ['name', 'lineItems', 'total']; 14 | orders$ = new Observable(); 15 | constructor(private orderSvc: OrdersService, private router: Router) {} 16 | 17 | ngOnInit(): void { 18 | this.orders$ = this.orderSvc.fetch(); 19 | } 20 | 21 | sum(order: Order): number { 22 | return order.products 23 | .map((p) => p.price * p.quantity) 24 | .reduce((acc, curr) => acc + curr); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/orders/models/order.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { OrderProduct } from './orderproduct.interface'; 6 | 7 | export interface Order { 8 | order_id: string; 9 | name: string; 10 | products: OrderProduct[]; 11 | tax?: number; 12 | } 13 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/orders/models/orderproduct.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export interface OrderProduct { 6 | productId: string; 7 | price: number; 8 | quantity: number; 9 | name?: string; 10 | } 11 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/orders/orders-routing.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { Routes, RouterModule } from '@angular/router'; 7 | // import { CognitoGuard } from '../cognito.guard'; 8 | import { CreateComponent } from './create/create.component'; 9 | import { DetailComponent } from './detail/detail.component'; 10 | import { ListComponent } from './list/list.component'; 11 | 12 | const routes: Routes = [ 13 | { 14 | path: '', 15 | redirectTo: 'list', 16 | pathMatch: 'full', 17 | }, 18 | { 19 | path: 'list', 20 | data: { 21 | title: 'All Orders', 22 | }, 23 | component: ListComponent, 24 | // canActivate: [CognitoGuard], 25 | }, 26 | { 27 | path: 'create', 28 | data: { 29 | title: 'Create Order', 30 | }, 31 | component: CreateComponent, 32 | // canActivate: [CognitoGuard], 33 | }, 34 | { 35 | path: 'detail/:orderId', 36 | data: { 37 | title: 'View Order Detail', 38 | }, 39 | component: DetailComponent, 40 | // canActivate: [CognitoGuard], 41 | }, 42 | ]; 43 | 44 | @NgModule({ 45 | imports: [RouterModule.forChild(routes)], 46 | exports: [RouterModule], 47 | }) 48 | export class OrdersRoutingModule {} 49 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/orders/orders.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatCardModule } from '@angular/material/card'; 7 | import { MatDatepickerModule } from '@angular/material/datepicker'; 8 | import { MatDialogModule } from '@angular/material/dialog'; 9 | import { 10 | MatFormFieldModule, 11 | MAT_FORM_FIELD_DEFAULT_OPTIONS, 12 | } from '@angular/material/form-field'; 13 | import { MatIconModule } from '@angular/material/icon'; 14 | import { MatInputModule } from '@angular/material/input'; 15 | import { MatRadioModule } from '@angular/material/radio'; 16 | import { MatSelectModule } from '@angular/material/select'; 17 | import { MatTableModule } from '@angular/material/table'; 18 | 19 | import { CreateComponent } from './create/create.component'; 20 | import { DetailComponent } from './detail/detail.component'; 21 | import { ListComponent } from './list/list.component'; 22 | import { OrdersRoutingModule } from './orders-routing.module'; 23 | 24 | @NgModule({ 25 | declarations: [CreateComponent, ListComponent, DetailComponent], 26 | imports: [ 27 | CommonModule, 28 | OrdersRoutingModule, 29 | ReactiveFormsModule, 30 | MatButtonModule, 31 | MatCardModule, 32 | MatDatepickerModule, 33 | MatDialogModule, 34 | MatFormFieldModule, 35 | MatIconModule, 36 | MatInputModule, 37 | MatRadioModule, 38 | MatSelectModule, 39 | MatTableModule, 40 | ], 41 | providers: [ 42 | { 43 | provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, 44 | useValue: { appearance: 'outline' }, 45 | }, 46 | ], 47 | }) 48 | export class OrdersModule {} 49 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/orders/orders.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { HttpClient } from '@angular/common/http'; 6 | import { Injectable } from '@angular/core'; 7 | import { Observable, of } from 'rxjs'; 8 | import { Order } from './models/order.interface'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class OrdersService { 14 | orders: Order[] = []; 15 | baseUrl = `./api/orders`; 16 | constructor(private http: HttpClient) {} 17 | 18 | fetch(): Observable { 19 | return this.http.get(this.baseUrl); 20 | } 21 | 22 | get(orderId: string): Observable { 23 | const url = `${this.baseUrl}/${orderId}`; 24 | return this.http.get(url); 25 | } 26 | 27 | create(order: Order): Observable { 28 | return this.http.post(this.baseUrl, order); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/create/create.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Create new Product 5 | 6 | 7 | Enter product name 8 | 14 | Name is required 15 | 16 | 17 | Enter product price 18 | 25 | Price is required 26 | 27 | 28 | Description 29 | 35 | 36 | 37 |
38 | 46 | 49 |
50 |
51 |
52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/create/create.component.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | margin: 20px; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: flex-start; 6 | } 7 | 8 | .product-form { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: flex-start; 12 | } 13 | 14 | .mat-form-field { 15 | display: flex; 16 | } 17 | 18 | .button-panel { 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | margin-bottom: 8px; 23 | } 24 | 25 | button { 26 | margin: 4px; 27 | } 28 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/create/create.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | 5 | import { ProductService } from '../product.service'; 6 | 7 | @Component({ 8 | selector: 'app-create', 9 | templateUrl: './create.component.html', 10 | styleUrls: ['./create.component.scss'], 11 | }) 12 | export class CreateComponent implements OnInit { 13 | productForm: FormGroup; 14 | constructor( 15 | private fb: FormBuilder, 16 | private productSvc: ProductService, 17 | private router: Router 18 | ) { 19 | this.productForm = this.fb.group({}); 20 | } 21 | 22 | ngOnInit(): void { 23 | this.productForm = this.fb.group({ 24 | name: ['', Validators.required], 25 | price: ['', Validators.required], 26 | description: '', 27 | }); 28 | } 29 | 30 | get name() { 31 | return this.productForm.get('name'); 32 | } 33 | 34 | get price() { 35 | return this.productForm.get('price'); 36 | } 37 | 38 | get description() { 39 | return this.productForm.get('description'); 40 | } 41 | 42 | submit() { 43 | this.productSvc.post(this.productForm.value).subscribe({ 44 | next: () => this.router.navigate(['products']), 45 | error: (err) => { 46 | alert(err.message); 47 | console.error(err); 48 | }, 49 | }); 50 | } 51 | 52 | cancel() { 53 | this.router.navigate(['products']); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/edit/edit.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Create new Product 5 | 6 | 7 | Enter product name 8 | 14 | Name is required 15 | 16 | 17 | Enter product price 18 | 25 | Price is required 26 | 27 | 28 | Description 29 | 35 | 36 | 37 |
38 | 41 | 44 |
45 |
46 |
47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/edit/edit.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-saas-factory-eks-saas-workshop/3f33453451e3d47a8f770448b9eed80f5fb85fcc/client/web/application/src/app/views/products/edit/edit.component.scss -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/list/list.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Product List

3 |
4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Name{{ element.name }}Price 16 | {{ element.price | currency }} 17 | Description{{ element.description }}
29 |
30 | 33 |
34 |
35 |
36 |
37 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/list/list.component.scss: -------------------------------------------------------------------------------- 1 | .product-list { 2 | margin: 20px; 3 | } 4 | table { 5 | width: 100%; 6 | } 7 | 8 | .button-panel { 9 | margin-top: 20px; 10 | } 11 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/list/list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Observable, of } from 'rxjs'; 4 | import { Product } from '../models/product.interface'; 5 | import { ProductService } from '../product.service'; 6 | 7 | @Component({ 8 | selector: 'app-list', 9 | templateUrl: './list.component.html', 10 | styleUrls: ['./list.component.scss'], 11 | }) 12 | export class ListComponent implements OnInit { 13 | products$: Observable; 14 | displayedColumns: string[] = ['name', 'price', 'description']; 15 | 16 | constructor(private productSvc: ProductService, private router: Router) { 17 | this.products$ = new Observable(); 18 | } 19 | 20 | ngOnInit(): void { 21 | this.refresh(); 22 | } 23 | 24 | onEdit(product: Product) { 25 | this.router.navigate(['products', 'edit', product.product_id]); 26 | return false; 27 | } 28 | 29 | onRemove(product: Product) { 30 | this.productSvc.delete(product); 31 | this.refresh(); 32 | } 33 | 34 | onCreate() { 35 | this.router.navigate(['products', 'create']); 36 | } 37 | 38 | refresh() { 39 | this.products$ = this.productSvc.fetch(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/models/product.interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export interface Product { 6 | product_id: string; 7 | name: string; 8 | price: number; 9 | description: string; 10 | } 11 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/product.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { HttpClient } from '@angular/common/http'; 6 | import { Injectable } from '@angular/core'; 7 | import { Observable, of } from 'rxjs'; 8 | import { Product } from './models/product.interface'; 9 | 10 | @Injectable({ 11 | providedIn: 'root', 12 | }) 13 | export class ProductService { 14 | constructor(private http: HttpClient) {} 15 | baseUrl = `./api/products`; 16 | 17 | fetch(): Observable { 18 | return this.http.get(this.baseUrl); 19 | } 20 | 21 | get(productId: string): Observable { 22 | const url = `${this.baseUrl}/${productId}`; 23 | return this.http.get(url); 24 | } 25 | 26 | delete(product: Product) { 27 | const url = `${this.baseUrl}/${product.product_id}`; 28 | return this.http.delete(url); 29 | } 30 | 31 | patch(product: Product) { 32 | const url = `${this.baseUrl}/${product.product_id}`; 33 | return this.http.patch(url, product); 34 | } 35 | 36 | post(product: Product) { 37 | return this.http.post(this.baseUrl, product); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/products-routing.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NgModule } from '@angular/core'; 6 | import { Routes, RouterModule } from '@angular/router'; 7 | // import { CognitoGuard } from '../cognito.guard'; 8 | import { CreateComponent } from './create/create.component'; 9 | import { EditComponent } from './edit/edit.component'; 10 | import { ListComponent } from './list/list.component'; 11 | 12 | const routes: Routes = [ 13 | { 14 | path: '', 15 | redirectTo: 'list', 16 | pathMatch: 'prefix', 17 | }, 18 | { 19 | path: 'list', 20 | data: { 21 | title: 'Product List', 22 | }, 23 | component: ListComponent, 24 | // canActivate: [CognitoGuard], 25 | }, 26 | { 27 | path: 'create', 28 | data: { 29 | title: 'Create new Product', 30 | }, 31 | component: CreateComponent, 32 | // canActivate: [CognitoGuard], 33 | }, 34 | { 35 | path: 'edit/:productId', 36 | data: { 37 | title: 'Edit Product', 38 | }, 39 | component: EditComponent, 40 | // canActivate: [CognitoGuard], 41 | }, 42 | ]; 43 | 44 | @NgModule({ 45 | imports: [RouterModule.forChild(routes)], 46 | exports: [RouterModule], 47 | }) 48 | export class ProductsRoutingModule {} 49 | -------------------------------------------------------------------------------- /client/web/application/src/app/views/products/products.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { MatCardModule } from '@angular/material/card'; 5 | import { MatDatepickerModule } from '@angular/material/datepicker'; 6 | import { 7 | MatFormFieldModule, 8 | MAT_FORM_FIELD_DEFAULT_OPTIONS, 9 | } from '@angular/material/form-field'; 10 | import { MatButtonModule } from '@angular/material/button'; 11 | import { MatInputModule } from '@angular/material/input'; 12 | import { MatSelectModule } from '@angular/material/select'; 13 | import { MatRadioModule } from '@angular/material/radio'; 14 | import { MatDialogModule } from '@angular/material/dialog'; 15 | import { MatTableModule } from '@angular/material/table'; 16 | 17 | import { ProductsRoutingModule } from './products-routing.module'; 18 | import { CreateComponent } from './create/create.component'; 19 | import { EditComponent } from './edit/edit.component'; 20 | import { ListComponent } from './list/list.component'; 21 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 22 | 23 | @NgModule({ 24 | declarations: [CreateComponent, EditComponent, ListComponent], 25 | imports: [ 26 | CommonModule, 27 | ReactiveFormsModule, 28 | ProductsRoutingModule, 29 | MatButtonModule, 30 | MatCardModule, 31 | MatDatepickerModule, 32 | MatDialogModule, 33 | MatFormFieldModule, 34 | MatInputModule, 35 | MatRadioModule, 36 | MatSelectModule, 37 | MatTableModule, 38 | ], 39 | providers: [ 40 | { 41 | provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, 42 | useValue: { appearance: 'outline' }, 43 | }, 44 | ], 45 | }) 46 | export class ProductsModule {} 47 | -------------------------------------------------------------------------------- /client/web/application/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-saas-factory-eks-saas-workshop/3f33453451e3d47a8f770448b9eed80f5fb85fcc/client/web/application/src/assets/.gitkeep -------------------------------------------------------------------------------- /client/web/application/src/assets/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiUrl": "//a5bab3202dabd4bfcb14389eb4efa146-c31c0ebb28634857.elb.us-west-2.amazonaws.com", 3 | "stage": "develop" 4 | } 5 | -------------------------------------------------------------------------------- /client/web/application/src/custom-theme.scss: -------------------------------------------------------------------------------- 1 | 2 | // Custom Theming for Angular Material 3 | // For more information: https://material.angular.io/guide/theming 4 | @use '@angular/material' as mat; 5 | // Plus imports for other components in your app. 6 | 7 | // Include the common styles for Angular Material. We include this here so that you only 8 | // have to load a single css file for Angular Material in your app. 9 | // Be sure that you only ever include this mixin once! 10 | @include mat.core(); 11 | 12 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 13 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 14 | // hue. Available color palettes: https://material.io/design/color/ 15 | $dashboard-primary: mat.define-palette(mat.$indigo-palette); 16 | $dashboard-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); 17 | 18 | // The warn palette is optional (defaults to red). 19 | $dashboard-warn: mat.define-palette(mat.$red-palette); 20 | 21 | // Create the theme object. A theme consists of configurations for individual 22 | // theming systems such as "color" or "typography". 23 | $dashboard-theme: mat.define-light-theme(( 24 | color: ( 25 | primary: $dashboard-primary, 26 | accent: $dashboard-accent, 27 | warn: $dashboard-warn, 28 | ) 29 | )); 30 | 31 | // Include theme styles for core and each component used in your app. 32 | // Alternatively, you can import and @include the theme mixins for each component 33 | // that you are using. 34 | @include mat.all-component-themes($dashboard-theme); 35 | 36 | 37 | html, body { height: 100%; } 38 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 39 | -------------------------------------------------------------------------------- /client/web/application/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /client/web/application/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /client/web/application/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-saas-factory-eks-saas-workshop/3f33453451e3d47a8f770448b9eed80f5fb85fcc/client/web/application/src/favicon.ico -------------------------------------------------------------------------------- /client/web/application/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | Dashboard 13 | 14 | 15 | 16 | 17 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /client/web/application/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)); 14 | -------------------------------------------------------------------------------- /client/web/application/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import url("https://fonts.googleapis.com/icon?family=Material+Symbols|Material+Symbols+Outlined"); 3 | @import "@aws-amplify/ui-angular/theme.css"; 4 | 5 | @import "styles/variables"; 6 | $enable-dark-mode: false; 7 | 8 | // Import functions, variables, and mixins needed by other Bootstrap files 9 | @import "bootstrap/scss/functions"; 10 | @import "bootstrap/scss/variables"; 11 | @import "bootstrap/scss/maps"; 12 | @import "bootstrap/scss/mixins"; 13 | 14 | // Import Bootstrap Reboot 15 | @import "bootstrap/scss/root"; // Contains :root CSS variables used by other Bootstrap files 16 | @import "bootstrap/scss/reboot"; 17 | 18 | @import "bootstrap/scss/containers"; // Add .container and .container-fluid classes 19 | @import "bootstrap/scss/grid"; // Add the grid system 20 | 21 | @import "bootstrap/scss/utilities"; // Configures the utility classes that should be generated 22 | @import "bootstrap/scss/utilities/api"; // Generates the actual utility classes 23 | 24 | @import "styles/reset"; 25 | -------------------------------------------------------------------------------- /client/web/application/src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $link-color: #673ab7; // 1 2 | $label-margin-bottom: 0; // 2 3 | $grid-breakpoints: ( 4 | xs: 0, 5 | // handset portrait (small, medium, large) | handset landscape (small) 6 | sm: 600px, 7 | // handset landscape (medium, large) | tablet portrait (small, large) 8 | md: 960px, 9 | // tablet landscape (small, large) 10 | lg: 1280px, 11 | // laptops and desktops 12 | xl: 1600px // large desktops, 13 | ); 14 | 15 | $container-max-widths: ( 16 | sm: 600px, 17 | md: 960px, 18 | lg: 1280px, 19 | xl: 1600px, 20 | ); 21 | -------------------------------------------------------------------------------- /client/web/application/src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | a { 2 | &.mat-button, 3 | &.mat-raised-button, 4 | &.mat-fab, 5 | &.mat-mini-fab, 6 | &.mat-list-item { 7 | &:hover { 8 | color: currentColor; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /client/web/application/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), 14 | ); 15 | -------------------------------------------------------------------------------- /client/web/application/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /client/web/application/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "strictNullChecks": false, 20 | "target": "ES2022", 21 | "module": "es2020", 22 | "lib": ["es2020", "dom"], 23 | "useDefineForClassFields": false 24 | }, 25 | "angularCompilerOptions": { 26 | "enableI18nLegacyMessageIdFormat": false, 27 | "strictInjectionParameters": true, 28 | "strictInputAccessModifiers": true, 29 | "strictTemplates": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/web/application/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-saas-factory-eks-saas-workshop", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /scripts/on-your-own/deploy-eks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #Deploys an Amazon EKS cluster. 4 | #Once deployment is complete, copy the output values and apply to cdk/root/lib/root-stack.ts 5 | CWD=$(pwd) 6 | 7 | cd cdk/on-your-own/eks 8 | yarn && yarn run build 9 | cdk bootstrap 10 | cdk deploy 11 | 12 | export ELBURL=$(aws cloudformation describe-stacks --stack-name ClusterStack --query "Stacks[0].Outputs[?OutputKey=='ELBURL'].OutputValue" --output text) 13 | export CODEBUILD_ARN=$(aws cloudformation describe-stacks --stack-name ClusterStack --query "Stacks[0].Outputs[?OutputKey=='EksCodebuildArn'].OutputValue" --output text) 14 | export IAM_ROLE_ARN=$(aws cloudformation describe-stacks --stack-name ClusterStack --query "Stacks[0].Outputs[?OutputKey=='RoleUsedByTVM'].OutputValue" --output text) 15 | 16 | echo "export ELBURL=${ELBURL}" | tee -a ~/.bash_profile 17 | echo "export IAM_ROLE_ARN=${IAM_ROLE_ARN}" | tee -a ~/.bash_profile 18 | echo "export CODEBUILD_ARN=${CODEBUILD_ARN}" | tee -a ~/.bash_profile 19 | 20 | cd $CWD -------------------------------------------------------------------------------- /scripts/onboard-premium-tenant.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | 6 | export AWS_PAGER="" 7 | STACKS=$(aws cloudformation describe-stacks) 8 | export USERPOOLID=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="PooledTenantUserPoolId") | .OutputValue') 9 | export EMAIL='premium-tier-user@saasco.com' 10 | export PASSWORD='ABCdef123*' 11 | 12 | export HOST=$(kubectl get svc -l istio=ingress -n istio-ingress -o json | jq -r '.items[0].status.loadBalancer.ingress[0].hostname') 13 | export SERVER=http://$HOST/api/registration 14 | 15 | #Onboard premium tier tenant 16 | curl -H 'Content-Type: application/json' \ 17 | -H "Host: $HOST" \ 18 | -d "{\"name\":\"premium-tier-user\",\"email\":\"EMAIL\",\"companyName\":\"premium-tier-tenant\",\"plan\":\"premium\"}" \ 19 | -X POST \ 20 | $SERVER -------------------------------------------------------------------------------- /scripts/onboard-tenants.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | 6 | export AWS_PAGER="" 7 | STACKS=$(aws cloudformation describe-stacks) 8 | export USERPOOLID=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="PooledTenantUserPoolId") | .OutputValue') 9 | export EMAILBASIC='basic-tier-user@saasco.com' 10 | export EMAILSTANDARD='standard-tier-user@saasco.com' 11 | export PASSWORD='ABCdef123*' 12 | 13 | export HOST=$(kubectl get svc -l istio=ingress -n istio-ingress -o json | jq -r '.items[0].status.loadBalancer.ingress[0].hostname') 14 | export SERVER=http://$HOST/api/registration 15 | 16 | #Onboard basic tier tenant 17 | curl -H 'Content-Type: application/json' \ 18 | -H "Host: $HOST" \ 19 | -d "{\"name\":\"basic-tier-user\",\"email\":\"$EMAILBASIC\",\"companyName\":\"Basic-tier-tenant\",\"plan\":\"basic\"}" \ 20 | -X POST \ 21 | $SERVER 22 | 23 | 24 | #Onboard standard tier tenant 25 | curl -H 'Content-Type: application/json' \ 26 | -H "Host: $HOST" \ 27 | -d "{\"name\":\"standard-tier-user\",\"email\":\"$EMAILSTANDARD\",\"companyName\":\"standard-tier-tenant\",\"plan\":\"standard\"}" \ 28 | -X POST \ 29 | $SERVER 30 | 31 | sleep 2 32 | 33 | aws cognito-idp admin-set-user-password \ 34 | --user-pool-id $USERPOOLID \ 35 | --username $EMAILBASIC \ 36 | --password "$PASSWORD" \ 37 | --permanent -------------------------------------------------------------------------------- /scripts/policy/deny-traffic-policy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: security.istio.io/v1beta1 2 | kind: AuthorizationPolicy 3 | metadata: 4 | name: deny-traffic-from-silotenanta-namespace 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: web 9 | action: DENY 10 | rules: 11 | - from: 12 | - source: 13 | namespaces: ["silotenanta"] -------------------------------------------------------------------------------- /scripts/resize-cloud9-ebs-volume.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | #Specify the desired volume size in GiB as a command-line argument. If not specified, default to 20 GiB. 6 | SIZE=${1:-40} 7 | 8 | # Get the ID of the environment host Amazon EC2 instance. 9 | INSTANCEID=$(curl http://169.254.169.254/latest/meta-data//instance-id) 10 | 11 | # Get the ID of the Amazon EBS volume associated with the instance. 12 | VOLUMEID=$(aws ec2 describe-instances \ 13 | --instance-id $INSTANCEID \ 14 | --query "Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId" \ 15 | --output text) 16 | 17 | # Resize the EBS volume. 18 | aws ec2 modify-volume --volume-id $VOLUMEID --size $SIZE 19 | 20 | # Wait for the resize to finish. 21 | while [ \ 22 | "$(aws ec2 describe-volumes-modifications \ 23 | --volume-id $VOLUMEID \ 24 | --filters Name=modification-state,Values="optimizing","completed" \ 25 | --query "length(VolumesModifications)"\ 26 | --output text)" != "1" ]; do 27 | sleep 1 28 | done 29 | 30 | #Check if we're on an NVMe filesystem 31 | if [ $(readlink -f /dev/xvda) = "/dev/xvda" ] 32 | then 33 | # Rewrite the partition table so that the partition takes up all the space that it can. 34 | sudo growpart /dev/xvda 1 35 | 36 | # Expand the size of the file system. 37 | # Check if we are on AL2 38 | STR=$(cat /etc/os-release) 39 | SUB="VERSION_ID=\"2\"" 40 | if [[ "$STR" == *"$SUB"* ]] 41 | then 42 | sudo xfs_growfs -d / 43 | else 44 | sudo resize2fs /dev/xvda1 45 | fi 46 | 47 | else 48 | # Rewrite the partition table so that the partition takes up all the space that it can. 49 | sudo growpart /dev/nvme0n1 1 50 | 51 | # Expand the size of the file system. 52 | # Check if we're on AL2 53 | STR=$(cat /etc/os-release) 54 | SUB="VERSION_ID=\"2\"" 55 | if [[ "$STR" == *"$SUB"* ]] 56 | then 57 | sudo xfs_growfs -d / 58 | else 59 | sudo resize2fs /dev/nvme0n1p1 60 | fi 61 | fi -------------------------------------------------------------------------------- /services/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /services/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /services/.prettierignore: -------------------------------------------------------------------------------- 1 | *.yaml -------------------------------------------------------------------------------- /services/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /services/Dockerfile.order: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/bitnami/node:18.18.0 AS build 2 | WORKDIR /app 3 | COPY package.json ./ 4 | COPY yarn.lock ./ 5 | RUN corepack enable 6 | RUN yarn set version stable 7 | RUN yarn install 8 | COPY . . 9 | RUN yarn build order 10 | 11 | FROM public.ecr.aws/bitnami/node:18.18.0 12 | WORKDIR /app 13 | COPY --from=build /app ./ 14 | EXPOSE 3003 15 | CMD ["npm", "run", "start", "order"] 16 | -------------------------------------------------------------------------------- /services/Dockerfile.product: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/bitnami/node:18.18.0 AS build 2 | WORKDIR /app 3 | COPY package.json ./ 4 | COPY yarn.lock ./ 5 | RUN corepack enable 6 | RUN yarn set version stable 7 | RUN yarn install 8 | COPY . . 9 | RUN yarn build product 10 | 11 | FROM public.ecr.aws/bitnami/node:18.18.0 12 | WORKDIR /app 13 | COPY --from=build /app ./ 14 | EXPOSE 3005 15 | CMD ["npm", "run", "start", "product"] 16 | -------------------------------------------------------------------------------- /services/Dockerfile.tenant-management: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/bitnami/node:18.18.0 AS build 2 | WORKDIR /app 3 | COPY package.json ./ 4 | COPY yarn.lock ./ 5 | RUN corepack enable 6 | RUN yarn set version stable 7 | RUN yarn install 8 | COPY . . 9 | RUN yarn build tenant-management 10 | 11 | FROM public.ecr.aws/bitnami/node:18.18.0 12 | WORKDIR /app 13 | COPY --from=build /app ./ 14 | EXPOSE 3001 15 | CMD ["npm", "run", "start", "tenant-management"] 16 | -------------------------------------------------------------------------------- /services/Dockerfile.tenant-registration: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/bitnami/node:18.18.0 AS build 2 | WORKDIR /app 3 | COPY package.json ./ 4 | COPY yarn.lock ./ 5 | RUN corepack enable 6 | RUN yarn set version stable 7 | RUN yarn install 8 | COPY . . 9 | RUN yarn build tenant-registration 10 | 11 | FROM public.ecr.aws/bitnami/node:18.18.0 12 | WORKDIR /app 13 | COPY --from=build /app ./ 14 | EXPOSE 3000 15 | CMD ["npm", "run", "start", "tenant-registration"] 16 | -------------------------------------------------------------------------------- /services/Dockerfile.user-management: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/bitnami/node:18.18.0 AS build 2 | WORKDIR /app 3 | COPY package.json ./ 4 | COPY yarn.lock ./ 5 | RUN corepack enable 6 | RUN yarn set version stable 7 | RUN yarn install 8 | COPY . . 9 | RUN yarn build user-management 10 | 11 | FROM public.ecr.aws/bitnami/node:18.18.0 12 | WORKDIR /app 13 | COPY --from=build /app ./ 14 | CMD ["npm", "run", "start", "user-management"] 15 | -------------------------------------------------------------------------------- /services/apps/application/order/src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import { NestFactory } from '@nestjs/core'; 7 | import { OrdersModule } from './orders/orders.module'; 8 | 9 | async function bootstrap() { 10 | const app = await NestFactory.create(OrdersModule); 11 | app.setGlobalPrefix('/*'); 12 | await app.listen(3010); 13 | } 14 | bootstrap(); 15 | -------------------------------------------------------------------------------- /services/apps/application/order/src/orders/dto/create-order.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { OrderProductDto } from './order-product.dto'; 6 | 7 | export class CreateOrderDto { 8 | name: string; 9 | products: OrderProductDto[]; 10 | } 11 | -------------------------------------------------------------------------------- /services/apps/application/order/src/orders/dto/order-product.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class OrderProductDto { 6 | productId: string; 7 | price: number; 8 | quantity: number; 9 | } 10 | -------------------------------------------------------------------------------- /services/apps/application/order/src/orders/dto/update-order.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { PartialType } from '@nestjs/mapped-types'; 6 | import { CreateOrderDto } from './create-order.dto'; 7 | 8 | export class UpdateOrderDto extends PartialType(CreateOrderDto) {} 9 | -------------------------------------------------------------------------------- /services/apps/application/order/src/orders/entities/order.entity.ts: -------------------------------------------------------------------------------- 1 | import { Product } from './product.entity'; 2 | 3 | /* 4 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 5 | * SPDX-License-Identifier: MIT-0 6 | */ 7 | export interface Order { 8 | products: Product[]; 9 | lineItems?: number; 10 | tax?: number; 11 | } 12 | -------------------------------------------------------------------------------- /services/apps/application/order/src/orders/entities/product.entity.ts: -------------------------------------------------------------------------------- 1 | export interface Product { 2 | productId: string; 3 | price: number; 4 | quantity: number; 5 | } 6 | -------------------------------------------------------------------------------- /services/apps/application/order/src/orders/orders.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { AuthModule } from '@app/auth'; 6 | import { ClientFactoryModule } from '@app/client-factory'; 7 | import { Module } from '@nestjs/common'; 8 | import { ConfigModule } from '@nestjs/config'; 9 | import { 10 | PrometheusModule, 11 | makeCounterProvider, 12 | } from '@willsoto/nestjs-prometheus'; 13 | import { OrdersController } from './orders.controller'; 14 | import { OrdersService } from './orders.service'; 15 | 16 | @Module({ 17 | imports: [ 18 | AuthModule, 19 | ConfigModule.forRoot({ isGlobal: true }), 20 | ClientFactoryModule, 21 | PrometheusModule.register({ 22 | path: '/orders/metrics', 23 | customMetricPrefix: 'saas', 24 | }), 25 | ], 26 | controllers: [OrdersController], 27 | providers: [ 28 | OrdersService, 29 | makeCounterProvider({ 30 | name: 'api_order_requests_total', 31 | help: 'The number of total API Requests to this order service endpoint', 32 | labelNames: ['tenantId', 'method'], 33 | }), 34 | ], 35 | exports: [PrometheusModule], 36 | }) 37 | export class OrdersModule {} 38 | -------------------------------------------------------------------------------- /services/apps/application/order/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /services/apps/application/order/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../../dist/apps/application/order" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/apps/application/product/k8s/template.istio.txt: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: product 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: product 10 | template: 11 | metadata: 12 | labels: 13 | app: product 14 | spec: 15 | containers: 16 | - name: product 17 | image: $CONTAINERIMAGE 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 3005 21 | name: "http" 22 | env: 23 | - name: AWS_REGION 24 | value: "$REGION" 25 | - name: IAM_ROLE_ARN 26 | value: "$IAM_ROLE_ARN" 27 | - name: COGNITO_USER_POOL_ID 28 | value: "$COGNITO_USER_POOL_ID" 29 | - name: COGNITO_CLIENT_ID 30 | value: "$COGNITO_CLIENT_ID" 31 | - name: COGNITO_REGION 32 | value: "$REGION" 33 | - name: PRODUCT_TABLE_NAME 34 | value: "$PRODUCTTABLE" 35 | --- 36 | apiVersion: v1 37 | kind: Service 38 | metadata: 39 | name: product-service 40 | spec: 41 | selector: 42 | app: product 43 | ports: 44 | - name: http 45 | protocol: TCP 46 | port: 80 47 | targetPort: 3005 48 | type: NodePort 49 | -------------------------------------------------------------------------------- /services/apps/application/product/src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NestFactory } from '@nestjs/core'; 6 | import { ProductsModule } from './products/products.module'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(ProductsModule); 10 | app.setGlobalPrefix('/*'); 11 | await app.listen(3005); 12 | } 13 | bootstrap(); 14 | -------------------------------------------------------------------------------- /services/apps/application/product/src/products/dto/create-product.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class CreateProductDto { 6 | name: string; 7 | price: number; 8 | description: string; 9 | } 10 | -------------------------------------------------------------------------------- /services/apps/application/product/src/products/dto/update-product.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { PartialType } from '@nestjs/mapped-types'; 6 | import { CreateProductDto } from './create-product.dto'; 7 | 8 | export class UpdateProductDto extends PartialType(CreateProductDto) {} 9 | -------------------------------------------------------------------------------- /services/apps/application/product/src/products/entities/product.entity.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class Product { 6 | constructor( 7 | private id: string, 8 | private price: number, 9 | private name: string, 10 | private description: string, 11 | ) {} 12 | } 13 | -------------------------------------------------------------------------------- /services/apps/application/product/src/products/products.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { AuthModule } from '@app/auth'; 6 | import { ClientFactoryModule } from '@app/client-factory'; 7 | import { Module } from '@nestjs/common'; 8 | import { ConfigModule } from '@nestjs/config'; 9 | import { 10 | PrometheusModule, 11 | makeCounterProvider, 12 | } from '@willsoto/nestjs-prometheus'; 13 | import { ProductsController } from './products.controller'; 14 | import { ProductsService } from './products.service'; 15 | 16 | @Module({ 17 | imports: [ 18 | AuthModule, 19 | ConfigModule.forRoot({ isGlobal: true }), 20 | ClientFactoryModule, 21 | PrometheusModule.register({ 22 | path: '/products/metrics', 23 | customMetricPrefix: 'saas', 24 | }), 25 | ], 26 | controllers: [ProductsController], 27 | providers: [ 28 | ProductsService, 29 | makeCounterProvider({ 30 | name: 'api_products_requests_total', 31 | help: 'The number of total API Requests to this products service endpoint', 32 | labelNames: ['tenantId', 'method'], 33 | }), 34 | ], 35 | exports: [PrometheusModule], 36 | }) 37 | export class ProductsModule {} 38 | -------------------------------------------------------------------------------- /services/apps/application/product/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../../dist/apps/application/product" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-management/k8s/manifest.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tenant-management 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: tenant-management 10 | template: 11 | metadata: 12 | labels: 13 | app: tenant-management 14 | spec: 15 | containers: 16 | - name: tenant-management 17 | image: public.ecr.aws/o2b5n0j5/eks-saas-tenant-management:latest 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 3001 21 | name: "http" 22 | env: 23 | - name: AWS_REGION 24 | value: "us-west-2" 25 | - name: COGNITO_USER_POOL_ID 26 | value: "us-west-2_aw33YGjZo" 27 | - name: COGNITO_CLIENT_ID 28 | value: "5m7i2urrjv61dou5lmm7ege6mp" 29 | - name: COGNITO_REGION 30 | value: "us-west-2" 31 | - name: TENANT_TABLE_NAME 32 | value: "Tenants-20220725142746" 33 | - name: AUTH_TENANT_TABLE_NAME 34 | value: "AuthInfo-20220725142746" 35 | --- 36 | apiVersion: v1 37 | kind: Service 38 | metadata: 39 | name: tenant-management-service 40 | spec: 41 | selector: 42 | app: tenant-management 43 | ports: 44 | - name: http 45 | protocol: TCP 46 | port: 80 47 | targetPort: 3001 48 | type: NodePort 49 | --- 50 | apiVersion: networking.k8s.io/v1 51 | kind: Ingress 52 | metadata: 53 | name: tenant-management-service-ingress 54 | annotations: 55 | kubernetes.io/ingress.class: "nginx" 56 | nginx.org/mergeable-ingress-type: "minion" 57 | spec: 58 | rules: 59 | - host: a6827acb0d6ef4172b966aa6ba3da833-2faefb68832aa130.elb.us-west-2.amazonaws.com 60 | http: 61 | paths: 62 | - backend: 63 | service: 64 | name: tenant-management-service 65 | port: 66 | number: 80 67 | path: /api/tenants 68 | pathType: Prefix 69 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-management/k8s/partial-template.txt: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tenant-management 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: tenant-management 10 | template: 11 | metadata: 12 | labels: 13 | app: tenant-management 14 | spec: 15 | containers: 16 | - name: tenant-management 17 | image: public.ecr.aws/o2b5n0j5/eks-saas-tenant-management:latest 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 3001 21 | name: "http" 22 | env: 23 | - name: AWS_REGION 24 | value: "$REGION" 25 | - name: COGNITO_USER_POOL_ID 26 | value: "$USERPOOLID" 27 | - name: COGNITO_CLIENT_ID 28 | value: "$APPCLIENTID" 29 | - name: COGNITO_REGION 30 | value: "$REGION" 31 | - name: TENANT_TABLE_NAME 32 | value: "$TENANT_TABLE_NAME" 33 | - name: AUTH_TENANT_TABLE_NAME 34 | value: "$AUTH_INFO_TABLE_NAME" 35 | --- 36 | apiVersion: v1 37 | kind: Service 38 | metadata: 39 | name: tenant-management-service 40 | spec: 41 | selector: 42 | app: tenant-management 43 | ports: 44 | - name: http 45 | protocol: TCP 46 | port: 80 47 | targetPort: 3001 48 | type: NodePort 49 | --- 50 | apiVersion: networking.k8s.io/v1 51 | kind: Ingress 52 | metadata: 53 | name: tenant-management-service-ingress 54 | annotations: 55 | kubernetes.io/ingress.class: "nginx" 56 | nginx.org/mergeable-ingress-type: "minion" 57 | spec: 58 | rules: 59 | - host: $ELBURL 60 | http: 61 | paths: 62 | - backend: 63 | service: 64 | name: tenant-management-service 65 | port: 66 | number: 80 67 | path: /api/tenants 68 | pathType: Prefix 69 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-management/k8s/template.istio.txt: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tenant-management 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: tenant-management 10 | template: 11 | metadata: 12 | labels: 13 | app: tenant-management 14 | spec: 15 | containers: 16 | - name: tenant-management 17 | image: public.ecr.aws/o2b5n0j5/eks-saas-tenant-management:latest 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 3001 21 | name: "http" 22 | env: 23 | - name: AWS_REGION 24 | value: "$REGION" 25 | - name: COGNITO_USER_POOL_ID 26 | value: "$USERPOOLID" 27 | - name: COGNITO_CLIENT_ID 28 | value: "$APPCLIENTID" 29 | - name: COGNITO_REGION 30 | value: "$REGION" 31 | - name: TENANT_TABLE_NAME 32 | value: "$TENANT_TABLE_NAME" 33 | - name: AUTH_TENANT_TABLE_NAME 34 | value: "$AUTH_INFO_TABLE_NAME" 35 | --- 36 | apiVersion: v1 37 | kind: Service 38 | metadata: 39 | name: tenant-management-service 40 | spec: 41 | selector: 42 | app: tenant-management 43 | ports: 44 | - name: http 45 | protocol: TCP 46 | port: 80 47 | targetPort: 3001 48 | type: NodePort -------------------------------------------------------------------------------- /services/apps/shared/tenant-management/k8s/template.txt: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tenant-management 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: tenant-management 10 | template: 11 | metadata: 12 | labels: 13 | app: tenant-management 14 | spec: 15 | containers: 16 | - name: tenant-management 17 | image: public.ecr.aws/o2b5n0j5/eks-saas-tenant-management:latest 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 3001 21 | name: "http" 22 | env: 23 | - name: AWS_REGION 24 | value: "$REGION" 25 | - name: COGNITO_USER_POOL_ID 26 | value: "$USERPOOLID" 27 | - name: COGNITO_CLIENT_ID 28 | value: "$APPCLIENTID" 29 | - name: COGNITO_REGION 30 | value: "$REGION" 31 | - name: TENANT_TABLE_NAME 32 | value: "$TENANT_TABLE_NAME" 33 | - name: AUTH_TENANT_TABLE_NAME 34 | value: "$AUTH_INFO_TABLE_NAME" 35 | --- 36 | apiVersion: v1 37 | kind: Service 38 | metadata: 39 | name: tenant-management-service 40 | spec: 41 | selector: 42 | app: tenant-management 43 | ports: 44 | - name: http 45 | protocol: TCP 46 | port: 80 47 | targetPort: 3001 48 | type: NodePort 49 | --- 50 | apiVersion: networking.k8s.io/v1 51 | kind: Ingress 52 | metadata: 53 | name: tenant-management-service-ingress 54 | annotations: 55 | kubernetes.io/ingress.class: "nginx" 56 | nginx.org/mergeable-ingress-type: "minion" 57 | spec: 58 | rules: 59 | - host: $ELBURL 60 | http: 61 | paths: 62 | - backend: 63 | service: 64 | name: tenant-management-service 65 | port: 66 | number: 80 67 | path: /api/tenants 68 | pathType: Prefix 69 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-management/src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NestFactory } from '@nestjs/core'; 6 | import { TenantsModule } from './tenants/tenants.module'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(TenantsModule); 10 | app.setGlobalPrefix('api'); 11 | app.enableCors({ 12 | allowedHeaders: '*', 13 | origin: '*', 14 | methods: '*', 15 | }); 16 | await app.listen(3001); 17 | } 18 | bootstrap(); 19 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-management/src/tenants/dto/create-tenant.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class CreateTenantDto {} 6 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-management/src/tenants/dto/update-tenant.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { PartialType } from '@nestjs/mapped-types'; 6 | import { CreateTenantDto } from './create-tenant.dto'; 7 | 8 | export class UpdateTenantDto extends PartialType(CreateTenantDto) {} 9 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-management/src/tenants/entities/tenant.entity.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export class Tenant {} 6 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-management/src/tenants/tenants.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Module } from '@nestjs/common'; 6 | import { ConfigModule } from '@nestjs/config'; 7 | 8 | import { TenantsService } from './tenants.service'; 9 | import { TenantsController } from './tenants.controller'; 10 | import { ClientFactoryModule } from 'libs/client-factory/src'; 11 | 12 | @Module({ 13 | imports: [ClientFactoryModule, ConfigModule.forRoot()], 14 | controllers: [TenantsController], 15 | providers: [TenantsService], 16 | }) 17 | export class TenantsModule {} 18 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-management/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-management/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../../dist/apps/shared/tenant-management" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-registration/src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NestFactory } from '@nestjs/core'; 6 | import { RegistrationModule } from './registration/registration.module'; 7 | 8 | async function bootstrap() { 9 | const app = await NestFactory.create(RegistrationModule); 10 | app.setGlobalPrefix('api'); 11 | await app.listen(3000); 12 | } 13 | bootstrap(); 14 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-registration/src/models/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export enum PLAN_TYPE { 6 | Basic = 'basic', 7 | Standard = 'standard', 8 | Premium = 'premium', 9 | } 10 | 11 | export enum USERPOOL_TYPE { 12 | Pooled = 'pooled', 13 | Siloed = 'siloed', 14 | } 15 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-registration/src/registration/constants.ts: -------------------------------------------------------------------------------- 1 | export const USER_SERVICE = 'USER_SERVICE'; 2 | export const USER_SERVICE_PORT = parseInt( 3 | process.env.USER_MANAGEMENT_SERVICE_PORT, 4 | ); 5 | export const USER_SERVICE_HOST = process.env.USER_MANAGEMENT_SERVICE_HOST; 6 | export const CREATE_TENANT_USER = 'createTenantUser'; 7 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-registration/src/registration/dto/create-registration.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { PLAN_TYPE } from '../../models/types'; 6 | 7 | export class CreateRegistrationDto { 8 | email: string; 9 | companyName: string; 10 | plan: PLAN_TYPE; 11 | } 12 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-registration/src/registration/dto/update-registration.dto.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { PartialType } from '@nestjs/mapped-types'; 6 | import { CreateRegistrationDto } from './create-registration.dto'; 7 | 8 | export class UpdateRegistrationDto extends PartialType(CreateRegistrationDto) {} 9 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-registration/src/registration/entities/registration.entity.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { PLAN_TYPE } from '../../models/types'; 6 | 7 | export class Registration { 8 | constructor( 9 | public tenantId: string, 10 | public email: string, 11 | public plan: PLAN_TYPE, 12 | public companyName: string, 13 | ) {} 14 | userPoolType: string; 15 | 16 | public get Path() { 17 | return this.companyName?.replace(/\W/g, '').toLowerCase() || ''; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-registration/src/registration/registration.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Module } from '@nestjs/common'; 6 | import { ConfigModule } from '@nestjs/config'; 7 | import { ClientsModule, Transport } from '@nestjs/microservices'; 8 | import { ClientFactoryModule } from 'libs/client-factory/src'; 9 | 10 | import { IdpService } from '../idp-service/idp.service'; 11 | import { 12 | USER_SERVICE, 13 | USER_SERVICE_HOST, 14 | USER_SERVICE_PORT, 15 | } from './constants'; 16 | import { RegistrationController } from './registration.controller'; 17 | import { RegistrationService } from './registration.service'; 18 | 19 | @Module({ 20 | imports: [ 21 | ClientFactoryModule, 22 | ConfigModule.forRoot(), 23 | ClientsModule.register([ 24 | { 25 | name: USER_SERVICE, 26 | transport: Transport.TCP, 27 | options: { 28 | port: USER_SERVICE_PORT, 29 | host: USER_SERVICE_HOST, 30 | }, 31 | }, 32 | ]), 33 | ], 34 | controllers: [RegistrationController], 35 | providers: [RegistrationService, IdpService], 36 | }) 37 | export class RegistrationModule {} 38 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-registration/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export function getTimeString() { 6 | const date = new Date(); 7 | const yyyy = date.getFullYear().toString(); 8 | const MM = pad(date.getMonth() + 1, 2); 9 | const dd = pad(date.getDate(), 2); 10 | const hh = pad(date.getHours(), 2); 11 | const mm = pad(date.getMinutes(), 2); 12 | const ss = pad(date.getSeconds(), 2); 13 | return yyyy + MM + dd + hh + mm + ss; 14 | } 15 | 16 | function pad(n: number, l: number) { 17 | let str = '' + n; 18 | while (str.length < l) { 19 | str = '0' + str; 20 | } 21 | return str; 22 | } 23 | -------------------------------------------------------------------------------- /services/apps/shared/tenant-registration/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../../dist/apps/shared/tenant-registration" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/apps/shared/user-management/k8s/manifest.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: user-management 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: user-management 10 | template: 11 | metadata: 12 | labels: 13 | app: user-management 14 | spec: 15 | containers: 16 | - name: user-management 17 | image: public.ecr.aws/o2b5n0j5/eks-saas-user:latest 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 3015 21 | env: 22 | - name: AWS_REGION 23 | value: "us-west-2" 24 | - name: COGNITO_USER_POOL_ID 25 | value: "us-west-2_aw33YGjZo" 26 | --- 27 | apiVersion: v1 28 | kind: Service 29 | metadata: 30 | name: user-management 31 | spec: 32 | selector: 33 | app: user-management 34 | ports: 35 | - name: http 36 | protocol: TCP 37 | port: 3015 38 | targetPort: 3015 39 | -------------------------------------------------------------------------------- /services/apps/shared/user-management/k8s/partial-template.txt: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: user-management 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: user-management 10 | template: 11 | metadata: 12 | labels: 13 | app: user-management 14 | spec: 15 | containers: 16 | - name: user-management 17 | image: public.ecr.aws/o2b5n0j5/eks-saas-user:latest 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 3015 21 | env: 22 | - name: AWS_REGION 23 | value: "$REGION" 24 | - name: COGNITO_USER_POOL_ID 25 | value: "$USERPOOLID" 26 | --- 27 | apiVersion: v1 28 | kind: Service 29 | metadata: 30 | name: user-management 31 | spec: 32 | selector: 33 | app: user-management 34 | ports: 35 | - name: http 36 | protocol: TCP 37 | port: 3015 38 | targetPort: 3015 39 | -------------------------------------------------------------------------------- /services/apps/shared/user-management/k8s/template.txt: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: user-management 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: user-management 10 | template: 11 | metadata: 12 | labels: 13 | app: user-management 14 | spec: 15 | containers: 16 | - name: user-management 17 | image: public.ecr.aws/o2b5n0j5/eks-saas-user:latest 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 3015 21 | env: 22 | - name: AWS_REGION 23 | value: "$REGION" 24 | - name: COGNITO_USER_POOL_ID 25 | value: "$USERPOOLID" 26 | --- 27 | apiVersion: v1 28 | kind: Service 29 | metadata: 30 | name: user-management 31 | spec: 32 | selector: 33 | app: user-management 34 | ports: 35 | - name: http 36 | protocol: TCP 37 | port: 3015 38 | targetPort: 3015 39 | -------------------------------------------------------------------------------- /services/apps/shared/user-management/src/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { NestFactory } from '@nestjs/core'; 6 | import { 7 | Transport, 8 | MicroserviceOptions, 9 | TcpOptions, 10 | } from '@nestjs/microservices'; 11 | import { UsersModule } from './users/users.module'; 12 | 13 | async function bootstrap() { 14 | const options: TcpOptions = { 15 | transport: Transport.TCP, 16 | options: { 17 | host: '0.0.0.0', 18 | port: 3015, 19 | }, 20 | }; 21 | const app = await NestFactory.createMicroservice( 22 | UsersModule, 23 | options, 24 | ); 25 | await app.listen(); 26 | } 27 | bootstrap(); 28 | -------------------------------------------------------------------------------- /services/apps/shared/user-management/src/users/dto/create-tenant-user.dto.ts.ts: -------------------------------------------------------------------------------- 1 | export class CreateTenantUserDto { 2 | userPoolId: string; 3 | email: string; 4 | tenantId: string; 5 | companyName: string; 6 | } 7 | -------------------------------------------------------------------------------- /services/apps/shared/user-management/src/users/users.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { MessagePattern, Payload } from '@nestjs/microservices'; 3 | import { UsersService } from './users.service'; 4 | import { CreateTenantUserDto } from './dto/create-tenant-user.dto.ts'; 5 | 6 | @Controller() 7 | export class UsersController { 8 | constructor(private readonly usersService: UsersService) { 9 | console.debug(); 10 | console.debug('Configuration'); 11 | console.debug('-----------------------------------------------------'); 12 | console.debug('PORT=' + process.env.PORT); 13 | console.debug('KUBERNETES_NAMESPACE=' + process.env.KUBERNETES_NAMESPACE); 14 | console.debug('KUBERNETES_POD_NAME=' + process.env.KUBERNETES_POD_NAME); 15 | console.debug('KUBERNETES_NODE_NAME=' + process.env.KUBERNETES_NODE_NAME); 16 | } 17 | 18 | @MessagePattern('createTenantUser') 19 | createTenantUser(@Payload() createTenantUserDto: CreateTenantUserDto) { 20 | return this.usersService.createTenantUser(createTenantUserDto); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /services/apps/shared/user-management/src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersService } from './users.service'; 3 | import { UsersController } from './users.controller'; 4 | 5 | @Module({ 6 | controllers: [UsersController], 7 | providers: [UsersService], 8 | }) 9 | export class UsersModule {} 10 | -------------------------------------------------------------------------------- /services/apps/shared/user-management/src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AdminCreateUserCommand, 3 | CognitoIdentityProviderClient, 4 | } from '@aws-sdk/client-cognito-identity-provider'; 5 | import { Injectable } from '@nestjs/common'; 6 | import { CreateTenantUserDto } from './dto/create-tenant-user.dto.ts'; 7 | 8 | @Injectable() 9 | export class UsersService { 10 | async createTenantUser(createTenantUserDto: CreateTenantUserDto) { 11 | const { userPoolId, email, companyName, tenantId } = createTenantUserDto; 12 | console.log('Adding tenant user', userPoolId, email, tenantId); 13 | const client = new CognitoIdentityProviderClient({}); 14 | const cmd = new AdminCreateUserCommand({ 15 | UserPoolId: userPoolId, 16 | Username: email, 17 | UserAttributes: [ 18 | { Name: 'custom:tenant-id', Value: tenantId }, 19 | { Name: 'custom:company-name', Value: companyName }, 20 | { Name: 'email', Value: email }, 21 | { Name: 'email_verified', Value: 'true' }, 22 | ], 23 | }); 24 | const res = await client.send(cmd); 25 | console.log('Successfully added user:', res.User); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /services/apps/shared/user-management/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "outDir": "../../../dist/apps/shared/user-management" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /services/libs/auth/src/auth-config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Injectable } from '@nestjs/common'; 6 | 7 | @Injectable() 8 | export class AuthConfig { 9 | public userPoolId: string = process.env.COGNITO_USER_POOL_ID; 10 | public clientId: string = process.env.COGNITO_CLIENT_ID; 11 | public region: string = process.env.COGNITO_REGION; 12 | public authority = `https://cognito-idp.${process.env.COGNITO_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`; 13 | } 14 | -------------------------------------------------------------------------------- /services/libs/auth/src/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 6 | 7 | export const TenantCredentials = createParamDecorator( 8 | (data: unknown, ctx: ExecutionContext) => { 9 | const request = ctx.switchToHttp().getRequest(); 10 | return request.user; 11 | }, 12 | ); 13 | -------------------------------------------------------------------------------- /services/libs/auth/src/auth.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { ConfigModule } from '@nestjs/config'; 6 | import { PassportModule } from '@nestjs/passport'; 7 | import { Module } from '@nestjs/common'; 8 | 9 | import { AuthConfig } from './auth-config'; 10 | import { JwtStrategy } from './jwt.strategy'; 11 | 12 | @Module({ 13 | imports: [ 14 | ConfigModule.forRoot({ isGlobal: true }), 15 | PassportModule.register({ defaultStrategy: 'jwt' }), 16 | ], 17 | providers: [JwtStrategy, AuthConfig], 18 | exports: [JwtStrategy], 19 | }) 20 | export class AuthModule {} 21 | -------------------------------------------------------------------------------- /services/libs/auth/src/credential-vendor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { STSClient, AssumeRoleCommand } from '@aws-sdk/client-sts'; 6 | import * as Mustache from 'mustache'; 7 | import * as policies from './policies.json'; 8 | 9 | export enum PolicyType { 10 | DynamoDBLeadingKey = 'DYNAMOLEADINGKEY', 11 | } 12 | 13 | export type CredentialConfig = { 14 | policyType: PolicyType; 15 | attributes: ClientAttributes; 16 | duration?: number; 17 | roleSessionName?: string; 18 | }; 19 | 20 | export type ClientAttributes = { 21 | [key: string]: any; 22 | }; 23 | 24 | export class CredentialVendor { 25 | constructor(private tenantId: string) {} 26 | 27 | async getCredentials(config: CredentialConfig): Promise { 28 | let policy: string; 29 | switch (config.policyType) { 30 | case PolicyType.DynamoDBLeadingKey: 31 | const template = JSON.stringify(policies.dynamodbLeadingKey); 32 | const vals = { 33 | ...config.attributes, 34 | tenant: this.tenantId, 35 | }; 36 | policy = Mustache.render(template, vals); 37 | console.log('POLICY:', policy); 38 | default: 39 | break; 40 | } 41 | const sts = new STSClient({ region: process.env.AWS_REGION }); 42 | const cmd = new AssumeRoleCommand({ 43 | DurationSeconds: config.duration || 900, 44 | Policy: policy, 45 | RoleArn: process.env.IAM_ROLE_ARN, 46 | RoleSessionName: config.roleSessionName || this.tenantId, 47 | }); 48 | const response = await sts.send(cmd); 49 | console.log('Successfully assumed role: ', process.env.IAM_ROLE_ARN); 50 | return response.Credentials; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /services/libs/auth/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export * from './auth.module'; 6 | -------------------------------------------------------------------------------- /services/libs/auth/src/jwt-auth.guard.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Injectable } from '@nestjs/common'; 6 | import { AuthGuard } from '@nestjs/passport'; 7 | 8 | @Injectable() 9 | export class JwtAuthGuard extends AuthGuard('jwt') {} 10 | -------------------------------------------------------------------------------- /services/libs/auth/src/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { ExtractJwt, Strategy } from 'passport-jwt'; 6 | import { PassportStrategy } from '@nestjs/passport'; 7 | import { Injectable } from '@nestjs/common'; 8 | import { passportJwtSecret } from 'jwks-rsa'; 9 | import { AuthConfig } from './auth-config'; 10 | 11 | @Injectable() 12 | export class JwtStrategy extends PassportStrategy(Strategy) { 13 | constructor(private authConfig: AuthConfig) { 14 | super({ 15 | secretOrKeyProvider: passportJwtSecret({ 16 | cache: true, 17 | rateLimit: true, 18 | jwksRequestsPerMinute: 5, 19 | jwksUri: `${authConfig.authority}/.well-known/jwks.json`, 20 | }), 21 | 22 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 23 | audience: authConfig.clientId, 24 | issuer: authConfig.authority, 25 | algorithms: ['RS256'], 26 | }); 27 | } 28 | 29 | async validate(payload: any) { 30 | const match = payload.iss.match(/([a-z\d\_\-]+)(\/*|)$/gi); 31 | return { 32 | userId: payload.sub, 33 | username: payload['cognito:username'], 34 | tenantId: payload['custom:tenant-id'], 35 | email: payload.email, 36 | userPoolId: match && match[0], 37 | appClientId: payload.aud, 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /services/libs/auth/src/policies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dynamodbLeadingKey": { 3 | "Version": "2012-10-17", 4 | "Statement": [ 5 | { 6 | "Effect": "Allow", 7 | "Action": ["dynamodb:*"], 8 | "Resource": ["arn:aws:dynamodb:*:*:table/{{table}}"], 9 | "Condition": { 10 | "ForAllValues:StringEquals": { 11 | "dynamodb:LeadingKeys": ["{{tenant}}"] 12 | } 13 | } 14 | } 15 | ] 16 | }, 17 | "s3": { 18 | "Version": "2012-10-17", 19 | "Statement": [ 20 | { 21 | "Effect": "Allow", 22 | "Action": ["dynamodb:*"], 23 | "Resource": ["arn:aws:dynamodb:*:*:table/{{table}}"], 24 | "Condition": { 25 | "ForAllValues:StringEquals": { 26 | "dynamodb:LeadingKeys": ["{{tenant}}"] 27 | } 28 | } 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /services/libs/auth/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/auth", 6 | "resolveJsonModule": true 7 | }, 8 | "include": ["src/**/*"], 9 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /services/libs/client-factory/src/client-factory.module.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { Module } from '@nestjs/common'; 6 | import { ClientFactoryService } from './client-factory.service'; 7 | 8 | @Module({ 9 | providers: [ClientFactoryService], 10 | exports: [ClientFactoryService], 11 | }) 12 | export class ClientFactoryModule {} 13 | -------------------------------------------------------------------------------- /services/libs/client-factory/src/client-factory.service.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | import { 6 | CredentialConfig, 7 | CredentialVendor, 8 | PolicyType, 9 | } from '@app/auth/credential-vendor'; 10 | import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; 11 | import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; 12 | import { Injectable } from '@nestjs/common'; 13 | 14 | @Injectable() 15 | export class ClientFactoryService { 16 | public get client(): DynamoDBDocumentClient { 17 | console.log('REGION:', process.env.AWS_REGION); 18 | const client = DynamoDBDocumentClient.from( 19 | new DynamoDBClient({ region: process.env.AWS_REGION }), 20 | ); 21 | return client; 22 | } 23 | 24 | public async getClient( 25 | tenantId: string, 26 | credentialConfig?: CredentialConfig, 27 | ) { 28 | const credentialVendor = new CredentialVendor(tenantId); 29 | const creds = await credentialVendor.getCredentials( 30 | credentialConfig || { 31 | policyType: PolicyType.DynamoDBLeadingKey, 32 | attributes: { 33 | tenant: tenantId, 34 | }, 35 | }, 36 | ); 37 | return DynamoDBDocumentClient.from( 38 | new DynamoDBClient({ 39 | credentials: { 40 | accessKeyId: creds.AccessKeyId, 41 | secretAccessKey: creds.SecretAccessKey, 42 | sessionToken: creds.SessionToken, 43 | expiration: creds.Expiration, 44 | }, 45 | }), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /services/libs/client-factory/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | export * from './client-factory.module'; 6 | export * from './client-factory.service'; 7 | -------------------------------------------------------------------------------- /services/libs/client-factory/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/client-factory", 6 | "resolveJsonModule": true 7 | }, 8 | "include": ["src/**/*"], 9 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /services/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "resolveJsonModule": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false, 21 | "paths": { 22 | "@app/auth": ["libs/auth/src"], 23 | "@app/auth/*": ["libs/auth/src/*"], 24 | "@app/client-factory": ["libs/client-factory/src"], 25 | "@app/client-factory/*": ["libs/client-factory/src/*"] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tenant-artifacts/deploy-pooled.sh: -------------------------------------------------------------------------------- 1 | STACKS=$(aws cloudformation describe-stacks) 2 | USERPOOLID=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="PooledTenantUserPoolId") | .OutputValue') 3 | APPCLIENTID=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="PooledTenantAppClientId") | .OutputValue') 4 | REGION=$(aws configure get region) 5 | AWS_ACCOUNT_ID=$(aws sts get-caller-identity --region $REGION |jq -r ".Account") 6 | ELBURL=$(kubectl get svc -l istio=ingress -n istio-ingress -o json | jq -r '.items[0].status.loadBalancer.ingress[0].hostname') 7 | IAM_ROLE_ARN=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="RoleUsedByTVM") | .OutputValue') 8 | PRODUCTTABLE=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="ProductTable") | .OutputValue') 9 | ORDERTABLE=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="OrderTable") | .OutputValue') 10 | 11 | TMPFILE=`mktemp /tmp/$0.XXXXXX` || exit 1 12 | cat <> $TMPFILE 13 | region: $REGION 14 | cognito: 15 | userPoolId: $USERPOOLID 16 | clientId: $APPCLIENTID 17 | loadBalancerAddress: $ELBURL 18 | app: 19 | tenantPath: app 20 | iamRoleArn: $IAM_ROLE_ARN 21 | 22 | product: 23 | image: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/aws-workshop/eks-saas-product 24 | productTable: $PRODUCTTABLE 25 | port: 80 26 | targetPort: 3005 27 | 28 | order: 29 | image: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/aws-workshop/eks-saas-order:v1 30 | orderTable: $ORDERTABLE 31 | port: 80 32 | targetPort: 3010 33 | 34 | application: 35 | image: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/aws-workshop/eks-saas-application 36 | port: 80 37 | targetPort: 80 38 | EOF 39 | 40 | cat $TMPFILE 41 | 42 | helm install -f $TMPFILE pooled ./helm/standard-tier-chart -n pooled -------------------------------------------------------------------------------- /tenant-artifacts/helm/basic-tier-chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/basic-tier-chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: basic-tier 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/basic-tier-chart/templates/application.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | config.json: | 4 | { 5 | "apiUrl": "http://{{.Values.loadBalancerAddress}}", 6 | "stage": "develop" 7 | } 8 | kind: ConfigMap 9 | metadata: 10 | name: app-config 11 | --- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | name: application 16 | labels: 17 | tenant: {{.Values.app.tenantPath}} 18 | spec: 19 | replicas: 1 20 | selector: 21 | matchLabels: 22 | app: application 23 | template: 24 | metadata: 25 | labels: 26 | app: application 27 | spec: 28 | containers: 29 | - name: application 30 | image: {{.Values.app.application.image}} 31 | imagePullPolicy: Always 32 | ports: 33 | - containerPort: 80 34 | name: "http" 35 | volumeMounts: 36 | - name: config-volume 37 | mountPath: /usr/share/nginx/html/app/assets/config 38 | volumes: 39 | - name: config-volume 40 | configMap: 41 | name: app-config 42 | --- 43 | apiVersion: v1 44 | kind: Service 45 | metadata: 46 | name: application-service 47 | spec: 48 | selector: 49 | app: application 50 | ports: 51 | - name: http 52 | protocol: TCP 53 | port: {{.Values.app.application.port}} 54 | targetPort: {{.Values.app.application.targetPort}} 55 | type: NodePort 56 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/basic-tier-chart/templates/destinationrule.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: DestinationRule 3 | metadata: 4 | name: order-service-destination-rule 5 | spec: 6 | host: order-service 7 | trafficPolicy: 8 | loadBalancer: 9 | simple: RANDOM 10 | subsets: 11 | - name: v1 12 | labels: 13 | version: v1 14 | - name: v2 15 | labels: 16 | version: v2 -------------------------------------------------------------------------------- /tenant-artifacts/helm/basic-tier-chart/values.yaml: -------------------------------------------------------------------------------- 1 | region: $REGION 2 | cognito: 3 | userPoolId: $USERPOOLID 4 | clientId: $APPCLIENTID 5 | loadBalancerAddress: $ELBURL 6 | 7 | admin: 8 | image: public.ecr.aws/o2b5n0j5/eks-saas-admin:latest 9 | port: 80 10 | targetPort: 80 11 | 12 | app: 13 | tenantPath: app 14 | iamRoleArn: $IAM_ROLE_ARN 15 | 16 | product: 17 | image: public.ecr.aws/o2b5n0j5/eks-saas-product:v1.1 18 | productTable: $PRODUCTTABLE 19 | port: 80 20 | targetPort: 3005 21 | 22 | order: 23 | image: public.ecr.aws/o2b5n0j5/eks-saas-order:v1.1 24 | orderTable: $ORDERTABLE 25 | port: 80 26 | targetPort: 3010 27 | 28 | application: 29 | image: public.ecr.aws/o2b5n0j5/eks-saas-application:latest 30 | port: 80 31 | targetPort: 80 32 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/core-services/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/core-services/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: core-services 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/core-services/templates/admin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | config.json: | 4 | { 5 | "apiUrl": "//{{ .Values.loadBalancerAddress}}", 6 | "stage": "develop", 7 | "amplifyConfig": { 8 | "aws_project_region": "{{.Values.region}}", 9 | "aws_cognito_region": "{{.Values.region}}", 10 | "aws_user_pools_id": "{{.Values.cognito.userPoolId}}", 11 | "aws_user_pools_web_client_id": "{{.Values.cognito.clientId}}" 12 | } 13 | } 14 | kind: ConfigMap 15 | metadata: 16 | name: admin-config 17 | namespace: default 18 | --- 19 | apiVersion: apps/v1 20 | kind: Deployment 21 | metadata: 22 | name: admin-application 23 | spec: 24 | replicas: 1 25 | selector: 26 | matchLabels: 27 | app: admin-application 28 | template: 29 | metadata: 30 | labels: 31 | app: admin-application 32 | spec: 33 | containers: 34 | - name: admin-application 35 | image: {{ .Values.admin.image }} 36 | imagePullPolicy: Always 37 | ports: 38 | - containerPort: 80 39 | name: "http" 40 | volumeMounts: 41 | - name: config-volume 42 | mountPath: /usr/share/nginx/html/admin/assets/config 43 | volumes: 44 | - name: config-volume 45 | configMap: 46 | name: admin-config 47 | --- 48 | apiVersion: v1 49 | kind: Service 50 | metadata: 51 | name: admin-application-service 52 | spec: 53 | selector: 54 | app: admin-application 55 | ports: 56 | - name: http 57 | protocol: TCP 58 | port: {{ .Values.admin.port }} 59 | targetPort: {{ .Values.admin.targetPort }} 60 | type: NodePort 61 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/core-services/templates/istio-gateway.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: Gateway 3 | metadata: 4 | name: eks-saas-workshop-gateway 5 | namespace: default 6 | spec: 7 | # The selector matches the ingress gateway pod labels. 8 | # If you installed Istio using Helm following the standard documentation, this would be "istio=ingress" 9 | selector: 10 | istio: ingress 11 | servers: 12 | - port: 13 | number: 80 14 | name: http 15 | protocol: HTTP 16 | hosts: 17 | - "*" 18 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/core-services/templates/tenant-management.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tenant-management 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: tenant-management 10 | template: 11 | metadata: 12 | labels: 13 | app: tenant-management 14 | spec: 15 | containers: 16 | - name: tenant-management 17 | image: {{.Values.core.tenantManagement.image}} 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 3001 21 | name: "http" 22 | env: 23 | - name: AWS_REGION 24 | value: {{.Values.region}} 25 | - name: COGNITO_USER_POOL_ID 26 | value: {{.Values.cognito.userPoolId}} 27 | - name: COGNITO_CLIENT_ID 28 | value: {{.Values.cognito.clientId}} 29 | - name: COGNITO_REGION 30 | value: {{.Values.region}} 31 | - name: TENANT_TABLE_NAME 32 | value: {{.Values.core.tenantTableName}} 33 | - name: AUTH_TENANT_TABLE_NAME 34 | value: {{.Values.core.authInfoTableName}} 35 | --- 36 | apiVersion: v1 37 | kind: Service 38 | metadata: 39 | name: tenant-management-service 40 | spec: 41 | selector: 42 | app: tenant-management 43 | ports: 44 | - name: http 45 | protocol: TCP 46 | port: {{.Values.core.tenantManagement.port}} 47 | targetPort: {{.Values.core.tenantManagement.targetPort}} 48 | type: NodePort 49 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/core-services/templates/tenant-registration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: tenant-registration 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: tenant-registration 10 | template: 11 | metadata: 12 | labels: 13 | app: tenant-registration 14 | spec: 15 | containers: 16 | - name: tenant-registration 17 | image: {{.Values.core.tenantRegistration.image}} 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: 21 | {{.Values.core.tenantRegistration.targetPort}} 22 | name: "http" 23 | env: 24 | - name: SERVICE_ADDRESS 25 | value: {{.Values.loadBalancerAddress}} 26 | - name: AWS_REGION 27 | value: {{.Values.region}} 28 | - name: COGNITO_USER_POOL_ID 29 | value: {{.Values.cognito.userPoolId}} 30 | - name: COGNITO_CLIENT_ID 31 | value: {{.Values.cognito.clientId}} 32 | - name: COGNITO_REGION 33 | value: {{.Values.region}} 34 | - name: TENANT_TABLE_NAME 35 | value: {{.Values.core.tenantTableName}} 36 | - name: AUTH_TENANT_TABLE_NAME 37 | value: {{.Values.core.authInfoTableName}} 38 | - name: TENANT_STACK_MAPPING_TABLE_NAME 39 | value: {{.Values.core.tenantStackMappingTable}} 40 | --- 41 | apiVersion: v1 42 | kind: Service 43 | metadata: 44 | name: tenant-registration-service 45 | spec: 46 | selector: 47 | app: tenant-registration 48 | ports: 49 | - name: http 50 | protocol: TCP 51 | port: {{.Values.core.tenantRegistration.port}} 52 | targetPort: {{.Values.core.tenantRegistration.targetPort}} 53 | type: NodePort 54 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/core-services/templates/user-management.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: user-management 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: user-management 10 | template: 11 | metadata: 12 | labels: 13 | app: user-management 14 | spec: 15 | containers: 16 | - name: user-management 17 | image: {{.Values.core.userManagement.image}} 18 | imagePullPolicy: Always 19 | ports: 20 | - containerPort: {{.Values.core.userManagement.targetPort}} 21 | env: 22 | - name: AWS_REGION 23 | value: "{{ .Values.region }}" 24 | - name: COGNITO_USER_POOL_ID 25 | value: "{{ .Values.cognito.userPoolId }}" 26 | --- 27 | apiVersion: v1 28 | kind: Service 29 | metadata: 30 | name: user-management 31 | spec: 32 | selector: 33 | app: user-management 34 | ports: 35 | - name: http 36 | protocol: TCP 37 | port: {{.Values.core.userManagement.targetPort}} 38 | targetPort: {{.Values.core.userManagement.targetPort}} 39 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/core-services/templates/virtual-services.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: VirtualService 3 | metadata: 4 | name: core-services-routes 5 | spec: 6 | hosts: 7 | - "{{ .Values.loadBalancerAddress }}" 8 | gateways: 9 | - default/eks-saas-workshop-gateway 10 | http: 11 | - match: 12 | - uri: 13 | prefix: /api/registration 14 | route: 15 | - destination: 16 | port: 17 | number: 80 18 | host: tenant-registration-service 19 | - match: 20 | - uri: 21 | prefix: /api/tenants 22 | route: 23 | - destination: 24 | port: 25 | number: 80 26 | host: tenant-management-service 27 | - match: 28 | - uri: 29 | prefix: /admin 30 | route: 31 | - destination: 32 | port: 33 | number: 80 34 | host: admin-application-service 35 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/core-services/values.yaml: -------------------------------------------------------------------------------- 1 | region: us-west-2 2 | cognito: 3 | userPoolId: us-west-2_HcYvTut6D 4 | clientId: 24ibamo1oupv0csoo9p4qjdd5r 5 | loadBalancerAddress: af3bbbaa776ee474298937b49c6b3bb6-02a0f405e5885970.elb.us-west-2.amazonaws.com 6 | 7 | admin: 8 | image: public.ecr.aws/o2b5n0j5/eks-saas-admin:latest 9 | port: 80 10 | targetPort: 80 11 | 12 | core: 13 | tenantTableName: Tenants 14 | authInfoTableName: AuthInfo 15 | tenantStackMappingTable: EKS-SaaS-Tenant-Stack-Mapping 16 | 17 | tenantManagement: 18 | image: public.ecr.aws/o2b5n0j5/eks-saas-tenant-management:v1.1 19 | port: 80 20 | targetPort: 3001 21 | 22 | tenantRegistration: 23 | image: public.ecr.aws/o2b5n0j5/eks-saas-tenant-registration:v1.1 24 | name: tenant-registration 25 | port: 80 26 | targetPort: 3000 27 | 28 | userManagement: 29 | image: public.ecr.aws/o2b5n0j5/eks-saas-user:v1.1 30 | name: user-management 31 | port: 80 32 | targetPort: 3015 33 | 34 | app: 35 | tenantPath: app 36 | iamRoleArn: arn:aws:iam::995597444569:role/root-stack-eksstackNested-DynamicAssumeRole644B86FF-avCE006bAUGL 37 | 38 | product: 39 | image: public.ecr.aws/o2b5n0j5/eks-saas-product:v1.1 40 | productTable: Products-Pooled 41 | port: 80 42 | targetPort: 3005 43 | 44 | order: 45 | image: public.ecr.aws/o2b5n0j5/eks-saas-order:v1.1 46 | orderTable: Orders-Pooled 47 | port: 80 48 | targetPort: 3010 49 | 50 | application: 51 | image: public.ecr.aws/o2b5n0j5/eks-saas-application:latest 52 | port: 80 53 | targetPort: 80 54 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/premium-tier-chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/premium-tier-chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: premium-tier-chart 3 | description: A Helm chart for Premium tier tenant deployments 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/premium-tier-chart/templates/application.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | config.json: | 4 | { 5 | "apiUrl": "http://{{.Values.loadBalancerAddress}}", 6 | "stage": "develop" 7 | } 8 | kind: ConfigMap 9 | metadata: 10 | name: app-config 11 | --- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | name: application 16 | labels: 17 | tenant: {{.Values.app.tenantPath}} 18 | spec: 19 | replicas: 1 20 | selector: 21 | matchLabels: 22 | app: application 23 | template: 24 | metadata: 25 | labels: 26 | app: application 27 | context: tenant-artifacts 28 | tier: premium 29 | tenant: {{.Values.app.tenantPath}} 30 | spec: 31 | containers: 32 | - name: application 33 | image: {{.Values.app.application.image}} 34 | imagePullPolicy: Always 35 | ports: 36 | - containerPort: 80 37 | name: "http" 38 | volumeMounts: 39 | - name: config-volume 40 | mountPath: /usr/share/nginx/html/app/assets/config 41 | volumes: 42 | - name: config-volume 43 | configMap: 44 | name: app-config 45 | tolerations: 46 | - key: node-pool 47 | value: "{{ .Values.app.tenantPath }}" 48 | effect: "NoSchedule" 49 | nodeSelector: 50 | node-pool: {{ .Values.app.tenantPath }} 51 | --- 52 | apiVersion: v1 53 | kind: Service 54 | metadata: 55 | name: application-service 56 | spec: 57 | selector: 58 | app: application 59 | ports: 60 | - name: http 61 | protocol: TCP 62 | port: {{.Values.app.application.port}} 63 | targetPort: {{.Values.app.application.targetPort}} 64 | type: NodePort 65 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/premium-tier-chart/templates/provisioner.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: karpenter.sh/v1alpha5 2 | kind: Provisioner 3 | metadata: 4 | name: {{.Values.app.tenantPath}} 5 | spec: 6 | # References cloud provider-specific custom resource, see your cloud provider specific documentation 7 | providerRef: 8 | name: {{.Values.app.tenantPath}} 9 | 10 | taints: 11 | - key: node-pool 12 | value: {{.Values.app.tenantPath}} 13 | effect: NoSchedule 14 | labels: 15 | node-pool: {{.Values.app.tenantPath}} 16 | 17 | limits: 18 | resources: 19 | cpu: "100" 20 | 21 | ttlSecondsAfterEmpty: 30 22 | --- 23 | apiVersion: karpenter.k8s.aws/v1alpha1 24 | kind: AWSNodeTemplate 25 | metadata: 26 | name: {{.Values.app.tenantPath}} 27 | spec: 28 | subnetSelector: 29 | "aws:cloudformation:stack-name" : {{ .Values.eksStackName }} 30 | securityGroupSelector: 31 | "aws:eks:cluster-name" : "eksworkshop-eksctl" 32 | tags: 33 | "karpenter.sh/discovery": "eksworkshop-eksctl" -------------------------------------------------------------------------------- /tenant-artifacts/helm/premium-tier-chart/templates/virtual-services.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.istio.io/v1alpha3 3 | kind: VirtualService 4 | metadata: 5 | name: app-routes 6 | spec: 7 | hosts: 8 | - "{{ .Values.loadBalancerAddress }}" 9 | gateways: 10 | - default/eks-saas-workshop-gateway 11 | http: 12 | - match: 13 | - uri: 14 | prefix: /{{.Values.app.tenantPath}}/products/metrics 15 | route: 16 | - destination: 17 | port: 18 | number: 80 19 | host: product-service 20 | - match: 21 | - uri: 22 | prefix: /{{.Values.app.tenantPath}}/api/products 23 | route: 24 | - destination: 25 | port: 26 | number: 80 27 | host: product-service 28 | - match: 29 | - uri: 30 | prefix: /{{.Values.app.tenantPath}}/orders/metrics 31 | route: 32 | - destination: 33 | port: 34 | number: 80 35 | host: order-service 36 | - match: 37 | - uri: 38 | prefix: /{{.Values.app.tenantPath}}/api/orders 39 | route: 40 | - destination: 41 | port: 42 | number: 80 43 | host: order-service 44 | - match: 45 | - uri: 46 | prefix: /{{.Values.app.tenantPath}} 47 | route: 48 | - destination: 49 | port: 50 | number: 80 51 | host: application-service 52 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/premium-tier-chart/values.yaml: -------------------------------------------------------------------------------- 1 | region: us-east-1 2 | cognito: 3 | userPoolId: us-east-1_yW37ZuSOe 4 | clientId: 671rnjuaj4r9hvudi7s3rvc3u2 5 | loadBalancerAddress: a0723e432a9274d2cbcda9969bf9b912-16197809.us-west-2.elb.amazonaws.com 6 | 7 | admin: 8 | image: public.ecr.aws/o2b5n0j5/eks-saas-admin:latest 9 | port: 80 10 | targetPort: 80 11 | 12 | core: 13 | tenantTableName: Tenants-20230924144359 14 | authInfoTableName: AuthInfo-20230924144359 15 | tenantStackMappingTable: EKS-SaaS-Tenant-Stack-Mapping 16 | 17 | tenantManagement: 18 | image: public.ecr.aws/o2b5n0j5/eks-saas-tenant-management:latest 19 | port: 80 20 | targetPort: 3001 21 | 22 | tenantRegistration: 23 | image: public.ecr.aws/t5r4o8x0/eks-saas-tenant-registration:latest 24 | name: tenant-registration 25 | port: 80 26 | targetPort: 3000 27 | 28 | userManagement: 29 | image: public.ecr.aws/o2b5n0j5/eks-saas-user:latest 30 | name: user-management 31 | port: 80 32 | targetPort: 3015 33 | 34 | app: 35 | tenantPath: app 36 | iamRoleArn: arn:aws:iam::832499941258:role/ClusterStack-DynamicAssumeRole644B86FF-9T8H3NZCLKHS 37 | 38 | product: 39 | image: public.ecr.aws/o2b5n0j5/eks-saas-product:latest 40 | productTable: Products-Pooled-20230924144359 41 | port: 80 42 | targetPort: 3005 43 | 44 | order: 45 | image: public.ecr.aws/o2b5n0j5/eks-saas-order:latest 46 | orderTable: Orders-Pooled-20230924144359 47 | port: 80 48 | targetPort: 3010 49 | 50 | application: 51 | image: public.ecr.aws/o2b5n0j5/eks-saas-application:latest 52 | port: 80 53 | targetPort: 80 54 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/standard-tier-chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/standard-tier-chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: standard-tier-chart 3 | description: A Helm chart for Standard tier tenant deployments 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/standard-tier-chart/templates/application.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | config.json: | 4 | { 5 | "apiUrl": "http://{{.Values.loadBalancerAddress}}", 6 | "stage": "develop" 7 | } 8 | kind: ConfigMap 9 | metadata: 10 | name: app-config 11 | --- 12 | apiVersion: apps/v1 13 | kind: Deployment 14 | metadata: 15 | name: application 16 | labels: 17 | tenant: {{.Values.app.tenantPath}} 18 | spec: 19 | replicas: 1 20 | selector: 21 | matchLabels: 22 | app: application 23 | template: 24 | metadata: 25 | labels: 26 | app: application 27 | context: tenant-artifacts 28 | tier: standard 29 | tenant: {{.Values.app.tenantPath}} 30 | spec: 31 | containers: 32 | - name: application 33 | image: {{.Values.app.application.image}} 34 | imagePullPolicy: Always 35 | ports: 36 | - containerPort: 80 37 | name: "http" 38 | volumeMounts: 39 | - name: config-volume 40 | mountPath: /usr/share/nginx/html/app/assets/config 41 | volumes: 42 | - name: config-volume 43 | configMap: 44 | name: app-config 45 | --- 46 | apiVersion: v1 47 | kind: Service 48 | metadata: 49 | name: application-service 50 | spec: 51 | selector: 52 | app: application 53 | ports: 54 | - name: http 55 | protocol: TCP 56 | port: {{.Values.app.application.port}} 57 | targetPort: {{.Values.app.application.targetPort}} 58 | type: NodePort 59 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/standard-tier-chart/templates/virtual-services.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.istio.io/v1alpha3 3 | kind: VirtualService 4 | metadata: 5 | name: app-routes 6 | spec: 7 | hosts: 8 | - "{{ .Values.loadBalancerAddress }}" 9 | gateways: 10 | - default/eks-saas-workshop-gateway 11 | http: 12 | - match: 13 | - uri: 14 | prefix: /{{.Values.app.tenantPath}}/products/metrics 15 | route: 16 | - destination: 17 | port: 18 | number: 80 19 | host: product-service 20 | - match: 21 | - uri: 22 | prefix: /{{.Values.app.tenantPath}}/api/products 23 | route: 24 | - destination: 25 | port: 26 | number: 80 27 | host: product-service 28 | - match: 29 | - uri: 30 | prefix: /{{.Values.app.tenantPath}}/orders/metrics 31 | route: 32 | - destination: 33 | port: 34 | number: 80 35 | host: order-service 36 | - match: 37 | - uri: 38 | prefix: /{{.Values.app.tenantPath}}/api/orders 39 | route: 40 | - destination: 41 | port: 42 | number: 80 43 | host: order-service 44 | - match: 45 | - uri: 46 | prefix: /{{.Values.app.tenantPath}} 47 | route: 48 | - destination: 49 | port: 50 | number: 80 51 | host: application-service 52 | -------------------------------------------------------------------------------- /tenant-artifacts/helm/standard-tier-chart/values.yaml: -------------------------------------------------------------------------------- 1 | region: us-east-1 2 | cognito: 3 | userPoolId: us-east-1_yW37ZuSOe 4 | clientId: 671rnjuaj4r9hvudi7s3rvc3u2 5 | loadBalancerAddress: a0723e432a9274d2cbcda9969bf9b912-16197809.us-west-2.elb.amazonaws.com 6 | 7 | admin: 8 | image: public.ecr.aws/o2b5n0j5/eks-saas-admin:latest 9 | port: 80 10 | targetPort: 80 11 | 12 | core: 13 | tenantTableName: Tenants-20230924144359 14 | authInfoTableName: AuthInfo-20230924144359 15 | tenantStackMappingTable: EKS-SaaS-Tenant-Stack-Mapping 16 | 17 | tenantManagement: 18 | image: public.ecr.aws/o2b5n0j5/eks-saas-tenant-management:latest 19 | port: 80 20 | targetPort: 3001 21 | 22 | tenantRegistration: 23 | image: public.ecr.aws/t5r4o8x0/eks-saas-tenant-registration:latest 24 | name: tenant-registration 25 | port: 80 26 | targetPort: 3000 27 | 28 | userManagement: 29 | image: public.ecr.aws/o2b5n0j5/eks-saas-user:latest 30 | name: user-management 31 | port: 80 32 | targetPort: 3015 33 | 34 | app: 35 | tenantPath: app 36 | iamRoleArn: arn:aws:iam::832499941258:role/ClusterStack-DynamicAssumeRole644B86FF-9T8H3NZCLKHS 37 | 38 | product: 39 | image: public.ecr.aws/o2b5n0j5/eks-saas-product:latest 40 | productTable: Products-Pooled-20230924144359 41 | port: 80 42 | targetPort: 3005 43 | 44 | order: 45 | image: public.ecr.aws/o2b5n0j5/eks-saas-order:latest 46 | orderTable: Orders-Pooled-20230924144359 47 | port: 80 48 | targetPort: 3010 49 | 50 | application: 51 | image: public.ecr.aws/o2b5n0j5/eks-saas-application:latest 52 | port: 80 53 | targetPort: 80 54 | -------------------------------------------------------------------------------- /tenant-artifacts/set-core-values.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | export STACKS=$(aws cloudformation describe-stacks) 6 | export USERPOOLID=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="AdminUserPoolId") | .OutputValue') 7 | export APPCLIENTID=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="AdminAppClientId") | .OutputValue') 8 | export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --region $AWS_REGION |jq -r ".Account") 9 | export ELBURL=$(kubectl get svc -l istio=ingress -n istio-ingress -o json | jq -r '.items[0].status.loadBalancer.ingress[0].hostname') 10 | export TENANT_TABLE_NAME=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="TenantTable") | .OutputValue') 11 | export AUTH_INFO_TABLE_NAME=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="AuthInfoTable") | .OutputValue') 12 | export TENANT_STACK_MAPPING_TABLE_NAME=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="TenantStackMappingTable") | .OutputValue') 13 | export PRODUCTTABLE=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="ProductTable") | .OutputValue') 14 | export ORDERTABLE=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="OrderTable") | .OutputValue') 15 | export IAM_ROLE_ARN=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="RoleUsedByTVM") | .OutputValue') 16 | 17 | 18 | envsubst < template.txt > helm/core-services/values.yaml 19 | -------------------------------------------------------------------------------- /tenant-artifacts/set-values.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | export STACKS=$(aws cloudformation describe-stacks) 6 | export USERPOOLID=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="PooledTenantUserPoolId") | .OutputValue') 7 | export APPCLIENTID=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="PooledTenantAppClientId") | .OutputValue') 8 | export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --region $AWS_REGION|jq -r ".Account") 9 | export ELBURL=$(kubectl get svc -l istio=ingress -n istio-ingress -o json | jq -r '.items[0].status.loadBalancer.ingress[0].hostname') 10 | export IAM_ROLE_ARN=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="RoleUsedByTVM") | .OutputValue') 11 | export PRODUCTTABLE=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="ProductTable") | .OutputValue') 12 | export ORDERTABLE=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="OrderTable") | .OutputValue') 13 | export EKSSTACKNAME=$(echo $STACKS | jq -r '.Stacks[]?.Outputs[]? | select(.OutputKey=="EksStackRenderedName") | .OutputValue') 14 | 15 | 16 | envsubst < template.txt > values.yaml 17 | -------------------------------------------------------------------------------- /tenant-artifacts/template.txt: -------------------------------------------------------------------------------- 1 | region: $AWS_REGION 2 | eksStackName: $EKSSTACKNAME 3 | cognito: 4 | userPoolId: $USERPOOLID 5 | clientId: $APPCLIENTID 6 | loadBalancerAddress: $ELBURL 7 | v1v2: 8 | enabled: false 9 | 10 | admin: 11 | image: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/aws-workshop/eks-saas-admin 12 | port: 80 13 | targetPort: 80 14 | 15 | core: 16 | tenantTableName: $TENANT_TABLE_NAME 17 | authInfoTableName: $AUTH_INFO_TABLE_NAME 18 | tenantStackMappingTable: $TENANT_STACK_MAPPING_TABLE_NAME 19 | 20 | tenantManagement: 21 | image: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/aws-workshop/eks-saas-tenant-management 22 | port: 80 23 | targetPort: 3001 24 | 25 | tenantRegistration: 26 | image: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/aws-workshop/eks-saas-tenant-registration 27 | name: tenant-registration 28 | port: 80 29 | targetPort: 3000 30 | 31 | userManagement: 32 | image: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/aws-workshop/eks-saas-user 33 | name: user-management 34 | port: 80 35 | targetPort: 3015 36 | 37 | app: 38 | tenantPath: app 39 | iamRoleArn: $IAM_ROLE_ARN 40 | 41 | product: 42 | image: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/aws-workshop/eks-saas-product 43 | productTable: $PRODUCTTABLE 44 | port: 80 45 | targetPort: 3005 46 | 47 | order: 48 | image: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/aws-workshop/eks-saas-order:v1 49 | imagev2: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/aws-workshop/eks-saas-order:v2 50 | orderTable: $ORDERTABLE 51 | port: 80 52 | targetPort: 3010 53 | 54 | application: 55 | image: $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/aws-workshop/eks-saas-application 56 | port: 80 57 | targetPort: 80 58 | -------------------------------------------------------------------------------- /tenant-artifacts/update-provisioning-status.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | : "${TENANTSTACKTABLE:=$1}" 3 | : "${TENANTPATH:=$2}" 4 | 5 | aws dynamodb update-item \ 6 | --table-name $TENANTSTACKTABLE \ 7 | --key '{"TenantName":{"S":"'$TENANTPATH'"}}' \ 8 | --update-expression "SET #D=:d" \ 9 | --expression-attribute-names '{"#D":"DeploymentStatus"}' \ 10 | --expression-attribute-values '{":d":{"S":"Provisioned"}}' --------------------------------------------------------------------------------