diff --git a/.gitignore b/.gitignore index 0ee0957d16..b7b4e9df36 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .dccache node_modules .vscode +setup_upgrade.sh \ No newline at end of file diff --git a/.jenkins/projectBuilder.Jenkinsfile b/.jenkins/projectBuilder.Jenkinsfile new file mode 100644 index 0000000000..d3222f258f --- /dev/null +++ b/.jenkins/projectBuilder.Jenkinsfile @@ -0,0 +1,85 @@ +projectBuilderV5 ( + buildAgent:[ + image: "467155500999.dkr.ecr.us-east-1.amazonaws.com/jenkins-agent:default", + cpu: 2048, + memory: 4096 + ], + + projects: [ + "upgrade-service":[ + artifactType: "ecr", + projectDir: "backend", + runInProjectDir: true, + versioning: "calendar", + appInfrastructure: [ + [file: "cloudformation/backend/app-infrastructure.yml"] + ], + s3Context: [ + glob: "backend/**/*,types/**/*,*.json" + ], + fileFilter: [ + include: ["types/.*","cloudformation/backend/app-infrastructure.yml"] + ], + dockerConfig: [ + dockerFile: "backend/cl.Dockerfile", + requiresCodeArtifactToken: true, + ] + ], + "upgrade":[ + artifactType: 'codeartifact', + projectDir: 'frontend', + runInProjectDir: true, + artifactDir: 'dist/upgrade', + versioning: 'calendar', + oneArtifactPerEnvironment: true, + buildScripts: [ + [ + script: 'npm ci --no-audit', + githubCheck: '${projectName} npm ci --no-audit', + log: '${projectName}-npm-ci.log' + ], + [ + script: 'npm run build:project', + log: '${projectName}-build.log', + githubCheck: '${projectName}-build' + ] + ], + envVars: [ + API_BASE_URL: '@vault(secret/configs/upgrade/${environment}/API_BASE_URL)', + BASE_HREF_PREFIX: '@vault(secret/configs/upgrade/${environment}/BASE_HREF_PREFIX)', + GOOGLE_CLIENT_ID: '@vault(secret/configs/upgrade/${environment}/GOOGLE_CLIENT_ID)', + ], + ] + ], + deployments: [ + UpgradeService: [ + projects: ["upgrade-service"], + automated: [ + [ + type: "defaultBranch", + environment: "qa" + ] + ], + jobs: [ + [ + job: "Upgrade-Service-Deploy", + type: "bluegreen" + ] + ] + ], + Upgrade: [ + projects: ["upgrade"], + automated: [ + [ + type: "defaultBranch", + environment: "qa" + ] + ], + jobs: [ + [ + job: "Upgrade-Frontend-Deploy" + ] + ] + ], + ] +) diff --git a/backend/cl.Dockerfile b/backend/cl.Dockerfile new file mode 100644 index 0000000000..5b437b4369 --- /dev/null +++ b/backend/cl.Dockerfile @@ -0,0 +1,30 @@ +ARG IMAGE_REPO + +FROM ${IMAGE_REPO}node:18-alpine3.16 AS build +WORKDIR /usr/src/app +COPY . . +RUN ls +RUN npm ci --no-audit +WORKDIR /usr/src/app/types +RUN npm ci --no-audit +RUN cp -R . ../backend/packages/Upgrade/types +# ARG CODEARTIFACT_AUTH_TOKEN +# ARG CODEARTIFACT_REGISTRY="//cli-467155500999.d.codeartifact.us-east-1.amazonaws.com/npm/cli-npm-artifacts/" +# RUN npm config set '${CODEARTIFACT_REGISTRY}:_authToken=${CODEARTIFACT_AUTH_TOKEN}' +WORKDIR /usr/src/app/backend/packages/Upgrade +RUN npm ci --no-audit + +WORKDIR /usr/src/app/backend/ +RUN npm ci --no-audit +RUN ["npm", "run", "build:upgrade"] + +FROM ${IMAGE_REPO}node:18-alpine3.16 + +ENV NEW_RELIC_NO_CONFIG_FILE=true +ENV NR_NATIVE_METRICS_NO_BUILD=true +ENV NODE_OPTIONS=--max_old_space_size=4096 + +WORKDIR /usr/src/app +COPY --from=build /usr/src/app/backend ./ +EXPOSE 3030 +CMD ["npm", "run", "--silent", "production:upgrade"] \ No newline at end of file diff --git a/cloudformation/backend/app-infrastructure.yml b/cloudformation/backend/app-infrastructure.yml new file mode 100644 index 0000000000..a69d6139ae --- /dev/null +++ b/cloudformation/backend/app-infrastructure.yml @@ -0,0 +1,322 @@ +AWSTemplateFormatVersion: 2010-09-09 +Description: Upgrade Service Blue/Green Deployment +Parameters: + appName: + Description: Name of the application (used for naming resources) + Type: String + Default: upgrade-service + environment: + Description: Name of the environment being deployed to. + Type: String + Default: qa + AllowedValues: + - qa + - staging + - prod + cluster: + Description: Cluster to deploy to (green or blue) + Type: String + Default: blue + AllowedValues: + - blue + - green + version: + Description: Version of the service managed by this CF Stack + Type: String + servicePort: + Description: Port that the application is listening on in the container + Type: String + Default: 3030 + sharedEcsResourcesPrefix: + Description: Prefix for the CloudFormation stack that contains shared resources for ECS. + Type: String + Default: shared-ecs-resources + sharedLoggingPrefix: + Description: Prefix for the CloudFormation stack that contains S3 logging buckets + Type: String + Default: shared-logging-infrastructure + sharedNetworkingPrefix: + Description: Prefix for the CloudFormation stack that contains shared networking resources + Type: String + Default: shared-networking-infrastructure + sharedResourcesPrefix: + Description: Prefix of the shared network and ECS resources to use + Type: String + Default: upgrade-service-shared-infrastructure +Conditions: + IsProd: !Equals + - !Ref environment + - prod +Resources: + ecsTaskDef: + Type: AWS::ECS::TaskDefinition + UpdateReplacePolicy: Retain + Properties: + ContainerDefinitions: + - Name: log_router + Image: 467155500999.dkr.ecr.us-east-1.amazonaws.com/splunk/fluentd-hec:latest + Essential: 'true' + MemoryReservation: 64 + ReadonlyRootFilesystem: true + Environment: + - Name: HEC_HOST + Value: '{{resolve:ssm:HEC_HOST}}' + - Name: SPLUNK_INDEX + Value: '{{resolve:ssm:SPLUNK_INDEX}}' + - Name: VERSION + Value: !Ref version + Secrets: + - Name: HEC_TOKEN + ValueFrom: !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/HEC_TOKEN + FirelensConfiguration: + Type: fluentd + Options: + config-file-type: file + config-file-value: /fluentd_configs/default-fluent.conf + LogConfiguration: + LogDriver: awslogs + Options: + awslogs-group: + Fn::ImportValue: !Sub ${sharedLoggingPrefix}-${environment}-cloudwatchLogGroup + awslogs-region: !Ref AWS::Region + awslogs-stream-prefix: !Ref appName + MountPoints: + - ContainerPath: /tmp + SourceVolume: log_router_tmp + - ContainerPath: /fluentd_configs + SourceVolume: fluentd_configs + ReadOnly: true + - Name: !Ref appName + Essential: 'true' + Image: !Sub 467155500999.dkr.ecr.us-east-1.amazonaws.com/microservices/${appName}:${version} + Environment: + - Name: ADMIN_USERS + Value: '{{resolve:ssm:UPGRADE_ADMIN_USERS}}' + - Name: APP_BANNER + Value: '{{resolve:ssm:UPGRADE_APP_BANNER}}' + - Name: APP_DEMO + Value: '{{resolve:ssm:UPGRADE_APP_DEMO}}' + - Name: APP_HOST + Value: '{{resolve:ssm:UPGRADE_APP_HOST}}' + - Name: APP_NAME + Value: '{{resolve:ssm:UPGRADE_APP_NAME}}' + - Name: APP_PORT + Value: '{{resolve:ssm:UPGRADE_APP_PORT}}' + - Name: APP_ROUTE_PREFIX + Value: '{{resolve:ssm:UPGRADE_APP_ROUTE_PREFIX}}' + - Name: APP_SCHEMA + Value: '{{resolve:ssm:UPGRADE_APP_SCHEMA}}' + - Name: AUTH_CHECK + Value: '{{resolve:ssm:UPGRADE_AUTH_CHECK}}' + - Name: AWS_REGION + Value: !Ref AWS::Region + - Name: CACHING_ENABLED + Value: '{{resolve:ssm:UPGRADE_CACHING_ENABLED}}' + - Name: CACHING_TTL + Value: '{{resolve:ssm:UPGRADE_CACHING_TTL}}' + - Name: CONTROLLERS + Value: '{{resolve:ssm:UPGRADE_CONTROLLERS}}' + - Name: DOMAIN_NAME + Value: '{{resolve:ssm:DOMAIN}}' + - Name: EMAIL_BUCKET + Value: '{{resolve:ssm:UPGRADE_EMAIL_BUCKET}}' + - Name: EMAIL_EXPIRE_AFTER_SECONDS + Value: '{{resolve:ssm:UPGRADE_EMAIL_EXPIRE_AFTER_SECONDS}}' + - Name: EMAIL_FROM + Value: '{{resolve:ssm:UPGRADE_EMAIL_FROM}}' + - Name: HOST_URL + Value: '{{resolve:ssm:UPGRADE_HOST_URL}}' + - Name: INTERCEPTORS + Value: '{{resolve:ssm:UPGRADE_INTERCEPTORS}}' + - Name: LOG_LEVEL + Value: '{{resolve:ssm:UPGRADE_LOG_LEVEL}}' + - Name: LOG_OUTPUT + Value: '{{resolve:ssm:UPGRADE_LOG_OUTPUT}}' + - Name: NEW_RELIC_APP_NAME + Value: '{{resolve:ssm:UPGRADE_NEW_RELIC_APP_NAME}}' + - Name: RDS_DB_NAME + Value: '{{resolve:ssm:UPGRADE_RDS_DB_NAME}}' + - Name: RDS_HOSTNAME + Value: '{{resolve:ssm:UPGRADE_RDS_HOSTNAME}}' + - Name: RDS_HOSTNAME_REPLICAS + Value: '{{resolve:ssm:UPGRADE_RDS_HOSTNAME_REPLICAS}}' + - Name: RDS_PORT + Value: '{{resolve:ssm:UPGRADE_RDS_PORT}}' + - Name: SCHEDULER_STEP_FUNCTION + Value: '{{resolve:ssm:UPGRADE_SCHEDULER_STEP_FUNCTION}}' + - Name: SWAGGER_API + Value: '{{resolve:ssm:UPGRADE_SWAGGER_API}}' + - Name: SWAGGER_ENABLED + Value: '{{resolve:ssm:UPGRADE_SWAGGER_ENABLED}}' + - Name: SWAGGER_FILE + Value: '{{resolve:ssm:UPGRADE_SWAGGER_FILE}}' + - Name: SWAGGER_JSON + Value: '{{resolve:ssm:UPGRADE_SWAGGER_JSON}}' + - Name: SWAGGER_ROUTE + Value: '{{resolve:ssm:UPGRADE_SWAGGER_ROUTE}}' + - Name: TYPEORM_CONNECTION + Value: '{{resolve:ssm:UPGRADE_TYPEORM_CONNECTION}}' + - Name: TYPEORM_ENTITIES + Value: '{{resolve:ssm:UPGRADE_TYPEORM_ENTITIES}}' + - Name: TYPEORM_ENTITIES_DIR + Value: '{{resolve:ssm:UPGRADE_TYPEORM_ENTITIES_DIR}}' + - Name: TYPEORM_FACTORY + Value: '{{resolve:ssm:UPGRADE_TYPEORM_FACTORY}}' + - Name: TYPEORM_LOGGER + Value: '{{resolve:ssm:UPGRADE_TYPEORM_LOGGER}}' + - Name: TYPEORM_LOGGING + Value: '{{resolve:ssm:UPGRADE_TYPEORM_LOGGING}}' + - Name: TYPEORM_MAX_QUERY_EXECUTION_TIME + Value: '{{resolve:ssm:UPGRADE_TYPEORM_MAX_QUERY_EXECUTION_TIME}}' + - Name: TYPEORM_MIGRATIONS + Value: '{{resolve:ssm:UPGRADE_TYPEORM_MIGRATIONS}}' + - Name: TYPEORM_MIGRATIONS_DIR + Value: '{{resolve:ssm:UPGRADE_TYPEORM_MIGRATIONS_DIR}}' + - Name: TYPEORM_SEED + Value: '{{resolve:ssm:UPGRADE_TYPEORM_SEED}}' + - Name: TYPEORM_SYNCHRONIZE + Value: '{{resolve:ssm:UPGRADE_TYPEORM_SYNCHRONIZE}}' + - Name: USE_NEW_RELIC + Value: '{{resolve:ssm:UPGRADE_USE_NEW_RELIC}}' + - Name: CONTEXT_METADATA + Value: '{{resolve:ssm:UPGRADE_CONTEXT_METADATA}}' + - Name: METRICS + Value: '{{resolve:ssm:UPGRADE_METRICS}}' + Secrets: + - Name: NEW_RELIC_LICENSE_KEY + ValueFrom: !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/NEW_RELIC_LICENSE_KEY + - Name: CLIENT_API_KEY + ValueFrom: !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/UPGRADE_CLIENT_API_KEY + - Name: CLIENT_API_SECRET + ValueFrom: !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/UPGRADE_CLIENT_API_SECRET + - Name: GOOGLE_CLIENT_ID + ValueFrom: !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/UPGRADE_GOOGLE_CLIENT_ID + - Name: RDS_PASSWORD + ValueFrom: !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/UPGRADE_DB_PASSWORD + - Name: RDS_USERNAME + ValueFrom: !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/UPGRADE_RDS_USERNAME + - Name: SWAGGER_PASSWORD + ValueFrom: !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/UPGRADE_SWAGGER_PASSWORD + - Name: SWAGGER_USERNAME + ValueFrom: !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/UPGRADE_SWAGGER_USERNAME + - Name: TOKEN_SECRET_KEY + ValueFrom: !Sub arn:aws:ssm:us-east-1:${AWS::AccountId}:parameter/UPGRADE_TOKEN_SECRET_KEY + LogConfiguration: + LogDriver: awsfirelens + MemoryReservation: 256 + ReadonlyRootFilesystem: false + PortMappings: + - ContainerPort: !Ref servicePort + Cpu: 1024 + ExecutionRoleArn: + Fn::ImportValue: !Sub ${sharedEcsResourcesPrefix}-${environment}-ecsExecutionRoleArn + Family: !Sub ${appName}-${environment}-${cluster} + Memory: 2048 + NetworkMode: awsvpc + RequiresCompatibilities: + - FARGATE + RuntimePlatform: + CpuArchitecture: X86_64 + OperatingSystemFamily: LINUX + Volumes: + - Name: log_router_tmp + - Name: fluentd_configs + EFSVolumeConfiguration: + TransitEncryption: ENABLED + FilesystemId: + Fn::ImportValue: !Sub ${sharedEcsResourcesPrefix}-${environment}-fluentdConfigFilesystemId + ecsService: + Type: AWS::ECS::Service + DependsOn: alblistenerrule + Properties: + ServiceName: !Sub ${appName}-${environment}-${cluster} + Cluster: + Fn::ImportValue: !Sub ${sharedResourcesPrefix}-${environment}-ecsClusterName + DesiredCount: !If [IsProd, 2, 1] + EnableECSManagedTags: 'true' + HealthCheckGracePeriodSeconds: 60 + LoadBalancers: + - ContainerName: !Ref appName + ContainerPort: !Ref servicePort + TargetGroupArn: !Ref ecsTargetGroup + DeploymentConfiguration: + DeploymentCircuitBreaker: + Enable: 'true' + Rollback: 'true' + NetworkConfiguration: + AwsvpcConfiguration: + AssignPublicIp: DISABLED + SecurityGroups: + - Fn::ImportValue: !Sub ${sharedResourcesPrefix}-${environment}-ecsSecurityGroupName + - Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-vpcEndpointSecurityGroupId + Subnets: + - Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-privatesubnet1 + - Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-privatesubnet2 + - Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-privatesubnet3 + PropagateTags: TASK_DEFINITION + TaskDefinition: !Ref ecsTaskDef + ecsScaling: + Type: AWS::ApplicationAutoScaling::ScalingPolicy + DependsOn: ecsScalingTarget + Properties: + PolicyName: !Sub ${appName}-${environment}-${cluster} + PolicyType: TargetTrackingScaling + ResourceId: !Join [ '/', [ 'service', Fn::ImportValue: !Sub '${sharedResourcesPrefix}-${environment}-ecsClusterName', !GetAtt 'ecsService.Name' ]] + ScalableDimension: ecs:service:DesiredCount + ServiceNamespace: ecs + TargetTrackingScalingPolicyConfiguration: + TargetValue: 90.0 + ScaleInCooldown: 300 + ScaleOutCooldown: 300 + PredefinedMetricSpecification: + PredefinedMetricType: ECSServiceAverageCPUUtilization + ecsScalingTarget: + Type: AWS::ApplicationAutoScaling::ScalableTarget + DependsOn: ecsService + Properties: + MaxCapacity: !If [IsProd, 16, 2] + MinCapacity: !If [IsProd, 2, 1] + ResourceId: !Join [ '/', [ 'service', Fn::ImportValue: !Sub '${sharedResourcesPrefix}-${environment}-ecsClusterName', !GetAtt 'ecsService.Name' ]] + RoleARN: !Sub arn:aws:iam::${AWS::AccountId}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService + ScalableDimension: ecs:service:DesiredCount + ServiceNamespace: ecs + ecsTargetGroup: + Type: AWS::ElasticLoadBalancingV2::TargetGroup + Properties: + Name: !Sub ${appName}-${environment}-${cluster} + HealthCheckEnabled: 'true' + HealthCheckIntervalSeconds: 30 + HealthCheckPath: /upgrade-service/api/ + HealthCheckPort: traffic-port + HealthCheckProtocol: HTTP + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 2 + Port: !Ref servicePort + Matcher: + HttpCode: '200' + Protocol: HTTP + ProtocolVersion: HTTP1 + TargetGroupAttributes: + - Key: load_balancing.algorithm.type + Value: least_outstanding_requests + - Key: deregistration_delay.timeout_seconds + Value: 30 + TargetType: ip + UnhealthyThresholdCount: 2 + VpcId: + Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-vpcid + alblistenerrule: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + DependsOn: ecsTargetGroup + Properties: + Actions: + - TargetGroupArn: !Ref ecsTargetGroup + Type: forward + Conditions: + - Field: path-pattern + PathPatternConfig: + Values: + - /upgrade-service* + ListenerArn: + Fn::ImportValue: !Sub ${sharedResourcesPrefix}-${environment}-albListenerArn-${cluster} + Priority: 1 \ No newline at end of file diff --git a/cloudformation/backend/shared-infrastructure.yml b/cloudformation/backend/shared-infrastructure.yml new file mode 100644 index 0000000000..79e2d0629c --- /dev/null +++ b/cloudformation/backend/shared-infrastructure.yml @@ -0,0 +1,210 @@ +# This template is manually deployed to three environments: + +AWSTemplateFormatVersion: 2010-09-09 +Description: Upgrade Service Shared Infrastructure +Parameters: + appName: + Description: Name of the application (used for naming resources) + Type: String + Default: upgrade-service + environment: + Description: Name of the environment being deployed to. + Type: String + Default: qa + AllowedValues: + - qa + - staging + - prod + servicePort: + Description: Port that the application is listening on in the container + Type: String + Default: 3030 + sharedCertificatesPrefix: + Description: Prefix for the CloudFormation stack that contains shared SSL certificates + Type: String + Default: shared-certificates-infrastructure + sharedLoggingPrefix: + Description: Prefix for the CloudFormation stack that contains S3 logging buckets + Type: String + Default: shared-logging-infrastructure + sharedNetworkingPrefix: + Description: Prefix for the CloudFormation stack that contains shared networking resources + Type: String + Default: shared-networking-infrastructure +Conditions: + IsProd: !Equals + - !Ref environment + - prod +Resources: + ecsServiceSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: !Sub ${appName}-${environment}-sg + GroupDescription: !Sub Security group for ${appName} ECS containers and ALB + VpcId: + Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-vpcid + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: !Ref servicePort + ToPort: !Ref servicePort + SourceSecurityGroupId: !Ref albSecurityGroup + albSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: !Sub ${appName}-${environment}-alb-sg + GroupDescription: !Sub Security group for green/blue load balancers for ${appName} + VpcId: + Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-vpcid + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + albGreen: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Name: !Sub ${appName}-alb-green + IpAddressType: ipv4 + LoadBalancerAttributes: + - Key: access_logs.s3.enabled + Value: 'true' + - Key: access_logs.s3.bucket + Value: !ImportValue + Fn::Sub: ${sharedLoggingPrefix}-${environment}-s3LogBucketName + - Key: access_logs.s3.prefix + Value: !Sub ${appName}-alb + Subnets: + - Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-publicsubnet1 + - Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-publicsubnet2 + - Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-publicsubnet3 + SecurityGroups: + - !Ref albSecurityGroup + albListenerGreen: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + Certificates: + - CertificateArn: + Fn::ImportValue: !Sub ${sharedCertificatesPrefix}-${environment}-microservicesCertificateArn + DefaultActions: + - FixedResponseConfig: + ContentType: text/plain + MessageBody: Route Not found + StatusCode: 404 + Order: 500 + Type: fixed-response + LoadBalancerArn: !Ref albGreen + Port: 443 + Protocol: HTTPS + albListenerCertificatesGreen: + Type: AWS::ElasticLoadBalancingV2::ListenerCertificate + Properties: + Certificates: + - CertificateArn: + Fn::ImportValue: !Sub ${sharedCertificatesPrefix}-${environment}-certificateArn + ListenerArn: !Ref albListenerGreen + albBlue: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + Properties: + Name: !Sub ${appName}-alb-blue + IpAddressType: ipv4 + LoadBalancerAttributes: + - Key: access_logs.s3.enabled + Value: 'true' + - Key: access_logs.s3.bucket + Value: + Fn::ImportValue: !Sub ${sharedLoggingPrefix}-${environment}-s3LogBucketName + - Key: access_logs.s3.prefix + Value: !Sub ${appName}-alb + Subnets: + - Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-publicsubnet1 + - Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-publicsubnet2 + - Fn::ImportValue: !Sub ${sharedNetworkingPrefix}-${environment}-publicsubnet3 + SecurityGroups: + - !Ref albSecurityGroup + albListenerBlue: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + Certificates: + - CertificateArn: + Fn::ImportValue: !Sub ${sharedCertificatesPrefix}-${environment}-microservicesCertificateArn + DefaultActions: + - FixedResponseConfig: + ContentType: text/plain + MessageBody: Route Not found + StatusCode: 404 + Order: 500 + Type: fixed-response + LoadBalancerArn: !Ref albBlue + Port: 443 + Protocol: HTTPS + albListenerCertificatesBlue: + Type: AWS::ElasticLoadBalancingV2::ListenerCertificate + Properties: + Certificates: + - CertificateArn: + Fn::ImportValue: !Sub ${sharedCertificatesPrefix}-${environment}-certificateArn + ListenerArn: !Ref albListenerBlue + # Production gets one non-spot task for every 10 spot tasks + # QA/Staging are all spot + ecsCluster: + Type: AWS::ECS::Cluster + Properties: + ClusterName: !Sub ${appName}-${environment} + CapacityProviders: + - FARGATE + - FARGATE_SPOT + DefaultCapacityProviderStrategy: + - !If + - IsProd + - CapacityProvider: FARGATE + Base: 1 + Weight: 1 + - !Ref AWS::NoValue + - CapacityProvider: FARGATE_SPOT + Weight: !If [IsProd, 10, 1] +Outputs: + ecsSecurityGroupName: + Description: Security group name for ECS tasks + Value: !Ref ecsServiceSecurityGroup + Export: + Name: !Sub ${AWS::StackName}-ecsSecurityGroupName + ecsClusterName: + Description: Name of the ECS cluster + Value: !Ref ecsCluster + Export: + Name: !Sub ${AWS::StackName}-ecsClusterName + servicePort: + Description: Port that the application is listening on in the container + Value: !Ref servicePort + Export: + Name: !Sub ${AWS::StackName}-servicePort + albListenerArnBlue: + Description: Blue Application Load Balancer Listener Arn + Value: !Ref albListenerBlue + Export: + Name: !Sub ${AWS::StackName}-albListenerArn-blue + albListenerArnGreen: + Description: Green Application Load Balancer Listener Arn + Value: !Ref albListenerGreen + Export: + Name: !Sub ${AWS::StackName}-albListenerArn-green + albDnsBlue: + Description: Blue Application Load Balancer DNS name + Value: !GetAtt albBlue.DNSName + Export: + Name: !Sub ${AWS::StackName}-albDns-blue + albDnsGreen: + Description: Green Application Load Balancer DNS name + Value: !GetAtt albGreen.DNSName + Export: + Name: !Sub ${AWS::StackName}-albDns-green + albHostedZoneBlue: + Description: The ID of the hosted zone associated with the blue load balancer. + Value: !GetAtt albBlue.CanonicalHostedZoneID + Export: + Name: !Sub ${AWS::StackName}-albHostedZone-blue + albHostedZoneGreen: + Description: The ID of the hosted zone associated with the green load balancer. + Value: !GetAtt albGreen.CanonicalHostedZoneID + Export: + Name: !Sub ${AWS::StackName}-albHostedZone-green \ No newline at end of file diff --git a/frontend/angular.json b/frontend/angular.json index ac05d889fa..af74811303 100755 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -39,7 +39,48 @@ "namedChunks": true }, "configurations": { - "production": { + "beanstalk": { + "fileReplacements": [ + { + "replace": "projects/upgrade/src/environments/environment.ts", + "with": "projects/upgrade/src/environments/environment.beanstalk.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "budgets": [ + { + "type": "bundle", + "name": "polyfills", + "baseline": "150kb", + "maximumWarning": "50kb", + "maximumError": "100kb" + }, + { + "type": "bundle", + "name": "styles", + "baseline": "1mb", + "maximumWarning": "2mb", + "maximumError": "100kb" + }, + { + "type": "bundle", + "name": "main", + "baseline": "2mb", + "maximumWarning": "3mb", + "maximumError": "200kb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, + "prod": { "fileReplacements": [ { "replace": "projects/upgrade/src/environments/environment.ts", @@ -162,6 +203,47 @@ } ] }, + "qa": { + "fileReplacements": [ + { + "replace": "projects/upgrade/src/environments/environment.ts", + "with": "projects/upgrade/src/environments/environment.qa.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "budgets": [ + { + "type": "bundle", + "name": "polyfills", + "baseline": "150kb", + "maximumWarning": "50kb", + "maximumError": "100kb" + }, + { + "type": "bundle", + "name": "styles", + "baseline": "1mb", + "maximumWarning": "2mb", + "maximumError": "100kb" + }, + { + "type": "bundle", + "name": "main", + "baseline": "2mb", + "maximumWarning": "3mb", + "maximumError": "200kb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ] + }, "staging": { "fileReplacements": [ { @@ -253,8 +335,17 @@ "buildTarget": "upgrade:build" }, "configurations": { - "production": { - "buildTarget": "upgrade:build:production" + "prod": { + "buildTarget": "upgrade:build:prod" + }, + "staging": { + "buildTarget": "upgrade:build:staging" + }, + "qa": { + "buildTarget": "upgrade:build:qa" + }, + "beanstalk": { + "buildTarget": "upgrade:build:beanstalk" } } }, @@ -271,8 +362,17 @@ "devServerTarget": "upgrade:serve" }, "configurations": { - "production": { - "devServerTarget": "upgrade:serve:production" + "prod": { + "devServerTarget": "upgrade:serve:prod" + }, + "staging": { + "devServerTarget": "upgrade:serve:staging" + }, + "qa": { + "devServerTarget": "upgrade:serve:qa" + }, + "beanstalk": { + "devServerTarget": "upgrade:serve:beanstalk" } } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 966fafcac0..7959f538f0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -63,6 +63,7 @@ "jest-environment-jsdom": "^29.5.0", "jest-preset-angular": "^13.1.4", "npm-run-all": "^4.1.5", + "replace-in-file": "^6.1.0", "rimraf": "^3.0.2", "standard-version": "^9.5.0", "ts-jest": "^29.0.8", @@ -16370,6 +16371,93 @@ "jsesc": "bin/jsesc" } }, + "node_modules/replace-in-file": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.5.tgz", + "integrity": "sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "glob": "^7.2.0", + "yargs": "^17.2.1" + }, + "bin": { + "replace-in-file": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/replace-in-file/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/replace-in-file/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/replace-in-file/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/replace-in-file/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/replace-in-file/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/replace-in-file/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5421a3c188..d3effa9853 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,8 @@ "prebuild": "npm run test", "build": "ng build", "prebuild:prod": "npm run test", - "build:prod": "ng build --configuration production", + "build:project": "node set_build_variables.js && ng build --configuration ${ENV:-develop} --base-href /upgrade/", + "build:prod": "ng build --configuration beanstalk", "build:demo": "ng build --configuration demo --base-href /upgrade/", "build:dev": "ng build -c=development", "clean": "rimraf node_modules && rm package-lock.json && npm update && npm i", @@ -104,6 +105,7 @@ "ts-jest": "^29.0.8", "ts-node": "~10.8.0", "typescript": "~5.3.3", - "webpack-bundle-analyzer": "^4.6.1" + "webpack-bundle-analyzer": "^4.6.1", + "replace-in-file": "^6.1.0" } } diff --git a/frontend/projects/upgrade/src/app/app-routing.module.ts b/frontend/projects/upgrade/src/app/app-routing.module.ts index ed325b7593..6eb4c5e0f0 100755 --- a/frontend/projects/upgrade/src/app/app-routing.module.ts +++ b/frontend/projects/upgrade/src/app/app-routing.module.ts @@ -28,7 +28,7 @@ const routes: Routes = [ // useHash supports github.io demo page, remove in your app imports: [ RouterModule.forRoot(routes, { - useHash: false, + useHash: true, scrollPositionRestoration: 'enabled', }), ], diff --git a/frontend/projects/upgrade/src/environments/environment.beanstalk.prod.ts b/frontend/projects/upgrade/src/environments/environment.beanstalk.prod.ts new file mode 100644 index 0000000000..29009dd846 --- /dev/null +++ b/frontend/projects/upgrade/src/environments/environment.beanstalk.prod.ts @@ -0,0 +1,66 @@ +export const environment = { + appName: 'UpGrade', + envName: 'PROD', + apiBaseUrl: '', + production: true, + test: false, + baseHrefPrefix: '', + googleClientId: '', + domainName: '', + pollingEnabled: true, + pollingInterval: 10 * 1000, + pollingLimit: 600, + featureFlagNavToggle: false, + withinSubjectExperimentSupportToggle: false, + errorLogsToggle: false, + metricAnalyticsExperimentDisplayToggle: false, + api: { + getAllExperiments: '/experiments/paginated', + createNewExperiments: '/experiments', + validateExperiment: '/experiments/validation', + importExperiment: '/experiments/import', + exportExperiment: '/experiments/export', + updateExperiments: '/experiments', + experimentContext: '/experiments/context', + getExperimentById: '/experiments/single', + getAllAuditLogs: '/audit', + getAllErrorLogs: '/error', + experimentsStats: '/stats/enrollment', + experimentDetailStat: '/stats/enrollment/detail', + generateCsv: '/stats/csv', + experimentGraphInfo: '/stats/enrollment/date', + deleteExperiment: '/experiments', + updateExperimentState: '/experiments/state', + users: '/users', + loginUser: '/login/user', // Used to create a new user after login if doesn't exist in DB + getAllUsers: '/users/paginated', + userDetails: '/users/details', + previewUsers: '/previewUsers', + stratification: '/stratification', + getAllPreviewUsers: '/previewUsers/paginated', + previewUsersAssignCondition: '/previewUsers/assign', + allPartitions: '/experiments/partitions', + allExperimentNames: '/experiments/names', + featureFlag: '/flags', + updateFlagStatus: '/flags/status', + updateFilterMode: '/flags/filterMode', + getPaginatedFlags: '/flags/paginated', + exportFlagsDesign: '/flags/export', + emailFlagData: '/flags/mail', + addFlagInclusionList: '/flags/inclusionList', + addFlagExclusionList: '/flags/exclusionList', + setting: '/setting', + metrics: '/metric', + metricsSave: '/metric/save', + queryResult: '/query/analyse', + getVersion: '/version', + contextMetaData: '/experiments/contextMetaData', + segments: '/segments', + validateSegments: '/segments/validation', + importSegments: '/segments/import', + exportSegments: '/segments/export/json', + exportSegment: '/segments/export', + exportSegmentCSV: '/segments/export/csv', + getGroupAssignmentStatus: '/experiments/getGroupAssignmentStatus', + }, +}; diff --git a/frontend/projects/upgrade/src/environments/environment.prod.ts b/frontend/projects/upgrade/src/environments/environment.prod.ts index 29009dd846..8fa627102e 100755 --- a/frontend/projects/upgrade/src/environments/environment.prod.ts +++ b/frontend/projects/upgrade/src/environments/environment.prod.ts @@ -1,11 +1,11 @@ export const environment = { appName: 'UpGrade', envName: 'PROD', - apiBaseUrl: '', + apiBaseUrl: '%API_BASE_URL%', production: true, test: false, - baseHrefPrefix: '', - googleClientId: '', + baseHrefPrefix: '%BASE_HREF_PREFIX%', + googleClientId: '%GOOGLE_CLIENT_ID%', domainName: '', pollingEnabled: true, pollingInterval: 10 * 1000, diff --git a/frontend/projects/upgrade/src/environments/environment.qa.ts b/frontend/projects/upgrade/src/environments/environment.qa.ts new file mode 100644 index 0000000000..943d26cd55 --- /dev/null +++ b/frontend/projects/upgrade/src/environments/environment.qa.ts @@ -0,0 +1,66 @@ +export const environment = { + appName: 'UpGrade', + envName: 'qa', + apiBaseUrl: '%API_BASE_URL%', + production: true, + test: false, + baseHrefPrefix: '%BASE_HREF_PREFIX%', + googleClientId: '%GOOGLE_CLIENT_ID%', + domainName: '', + pollingEnabled: true, + pollingInterval: 10 * 1000, + pollingLimit: 600, + featureFlagNavToggle: true, + withinSubjectExperimentSupportToggle: true, + errorLogsToggle: false, + metricAnalyticsExperimentDisplayToggle: false, + api: { + getAllExperiments: '/experiments/paginated', + createNewExperiments: '/experiments', + validateExperiment: '/experiments/validation', + importExperiment: '/experiments/import', + exportExperiment: '/experiments/export', + updateExperiments: '/experiments', + experimentContext: '/experiments/context', + getExperimentById: '/experiments/single', + getAllAuditLogs: '/audit', + getAllErrorLogs: '/error', + experimentsStats: '/stats/enrollment', + experimentDetailStat: '/stats/enrollment/detail', + generateCsv: '/stats/csv', + experimentGraphInfo: '/stats/enrollment/date', + deleteExperiment: '/experiments', + updateExperimentState: '/experiments/state', + users: '/users', + loginUser: '/login/user', // Used to create a new user after login if doesn't exist in DB + getAllUsers: '/users/paginated', + userDetails: '/users/details', + previewUsers: '/previewUsers', + stratification: '/stratification', + getAllPreviewUsers: '/previewUsers/paginated', + previewUsersAssignCondition: '/previewUsers/assign', + allPartitions: '/experiments/partitions', + allExperimentNames: '/experiments/names', + featureFlag: '/flags', + updateFlagStatus: '/flags/status', + updateFilterMode: '/flags/filterMode', + getPaginatedFlags: '/flags/paginated', + exportFlagsDesign: '/flags/export', + emailFlagData: '/flags/mail', + addFlagInclusionList: '/flags/inclusionList', + addFlagExclusionList: '/flags/exclusionList', + setting: '/setting', + metrics: '/metric', + metricsSave: '/metric/save', + queryResult: '/query/analyse', + getVersion: '/version', + contextMetaData: '/experiments/contextMetaData', + segments: '/segments', + validateSegments: '/segments/validation', + importSegments: '/segments/import', + exportSegments: '/segments/export/json', + exportSegment: '/segments/export', + exportSegmentCSV: '/segments/export/csv', + getGroupAssignmentStatus: '/experiments/getGroupAssignmentStatus', + }, +}; diff --git a/frontend/projects/upgrade/src/environments/environment.staging.ts b/frontend/projects/upgrade/src/environments/environment.staging.ts index da8828a159..88daee831f 100644 --- a/frontend/projects/upgrade/src/environments/environment.staging.ts +++ b/frontend/projects/upgrade/src/environments/environment.staging.ts @@ -1,11 +1,11 @@ export const environment = { appName: 'UpGrade', envName: 'staging', - apiBaseUrl: 'http://staging-upgrade-experiment-app.eba-gp6psjut.us-east-1.elasticbeanstalk.com/api', + apiBaseUrl: '%API_BASE_URL%', production: true, test: false, - baseHrefPrefix: '', - googleClientId: '135765367152-pq4jhd3gra10jda9l6bpnmu9gqt48tup.apps.googleusercontent.com', + baseHrefPrefix: '%BASE_HREF_PREFIX%', + googleClientId: '%GOOGLE_CLIENT_ID%', domainName: '', pollingEnabled: true, pollingInterval: 10 * 1000, diff --git a/frontend/set_build_variables.js b/frontend/set_build_variables.js new file mode 100644 index 0000000000..fd532b664a --- /dev/null +++ b/frontend/set_build_variables.js @@ -0,0 +1,34 @@ +var envVars = require("./settings.env.js"); +replace = require("replace-in-file"); + +var replacements = []; +var envKeys = Object.keys(envVars); + +for (var i in envKeys) { + var replacement = {}; + replacement["search"] = new RegExp("%" + envKeys[i] + "%", "g"); + replacement["replace"] = envVars[envKeys[i]]; + + replacements.push(replacement); +} + +console.log( + "Beginning pre build environment string replacements in environments.*.ts files" +); +for (var i in replacements) { + try { + const changedFiles = replace.sync({ + files: [ + 'projects/upgrade/src/environments/environment.prod.ts', + 'projects/upgrade/src/environments/environment.qa.ts', + 'projects/upgrade/src/environments/environment.staging.ts', + ], + from: replacements[i].search, + to: replacements[i].replace, + allowEmptyPaths: false, + }); + } catch (error) { + console.error("Error occurred:", error); + } +} +console.log("Pre build environment string replacements complete."); \ No newline at end of file diff --git a/frontend/settings.env.js b/frontend/settings.env.js new file mode 100644 index 0000000000..1243c3b088 --- /dev/null +++ b/frontend/settings.env.js @@ -0,0 +1,5 @@ +module.exports = { + API_BASE_URL: process.env.API_BASE_URL || ' ', + BASE_HREF_PREFIX: process.env.BASE_HREF_PREFIX || ' ', + GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID || ' ', +};