diff --git a/app/graphql/types/item_bwf_xml_type.rb b/app/graphql/types/item_bwf_csv_type.rb similarity index 75% rename from app/graphql/types/item_bwf_xml_type.rb rename to app/graphql/types/item_bwf_csv_type.rb index c7539840..95489391 100644 --- a/app/graphql/types/item_bwf_xml_type.rb +++ b/app/graphql/types/item_bwf_csv_type.rb @@ -1,8 +1,8 @@ -class Types::ItemBwfXmlType < Types::BaseObject +class Types::ItemBwfCsvType < Types::BaseObject field :full_identifier, String, null: false field :collection_identifier, String, null: false field :item_identifier, String, null: false - field :xml, String, null: false + field :csv, String, null: false field :created_at, GraphQL::Types::ISO8601DateTime field :updated_at, GraphQL::Types::ISO8601DateTime end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 6f74a9a3..972578c5 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -27,10 +27,11 @@ def item(full_identifier:) collection.items.find_by(identifier: item_identifier) end - field :item_bwf_xml, ItemBwfXmlType, 'Get the BWF XML for an item' do + field :item_bwf_csv, ItemBwfCsvType, 'Get the BWF XML for an item' do argument :full_identifier, ID + argument :filename, String end - def item_bwf_xml(full_identifier:) + def item_bwf_csv(full_identifier:, filename:) raise(GraphQL::ExecutionError, 'Not authorised') unless context[:admin_authenticated] collection_identifier, item_identifier = full_identifier.split('-') @@ -40,16 +41,39 @@ def item_bwf_xml(full_identifier:) item = collection.items.find_by(identifier: item_identifier) raise(GraphQL::ExecutionError, 'Not found') unless item - warden = Warden::Proxy.new({}, Warden::Manager.new({})).tap do |i| - i.set_user(context[:current_user], scope: :user) + desc = [ + '# Notes', + '', + "Reference: https://catalog.paradisec.org.au/repository/#{collection.identifier}/#{item.identifier}", + ] + + unless item.subject_languages.empty? + desc << "Language: #{item.subject_languages.first.name}\" #{item.subject_languages.first.code}" + end + + desc << "Country: #{item.countries.first.code}" unless item.countries.empty? + desc << "Description: #{item.description}" + + bwf = { + 'FileName' => filename, + 'Description' => desc.join('\n').truncate(256), + 'Originator' => item.collector_name, + 'OriginationDate' => item.originated_on, + 'BextVersion' => 1, + 'CodingHistory' => 'A=PCM,F=96000,W=24,M=stereo,T=Paragest Pipeline' + # 'TBA' => @item.ingest_notes + } + + csv = CSV.generate(headers: true) do |c| + c << bwf.keys + c << bwf.values end - item_renderer = ItemsController.renderer.new('warden' => warden) { full_identifier: item.full_identifier, collection_identifier: collection.identifier, item_identifier: item.identifier, - xml: item_renderer.render('items/show_bwf', formats: [:xml], assigns: { item: }), + csv:, created_at: item.created_at, updated_at: item.updated_at } diff --git a/app/views/items/show_bwf.xml.haml b/app/views/items/show_bwf.xml.haml deleted file mode 100644 index b5e11e11..00000000 --- a/app/views/items/show_bwf.xml.haml +++ /dev/null @@ -1,19 +0,0 @@ -%Core - %Description - = "# Notes" - = "Reference: https://catalog.paradisec.org.au/repository/#{@item.collection.identifier}/#{@item.identifier}" - = "" - = "Description: #{@item.description}." - = "" - - - unless @item.subject_languages.empty? - = "Language: \"#{@item.subject_languages.first.name}\" #{@item.subject_languages.first.code};" - - - unless @item.countries.empty? - = "Country: #{@item.countries.first.code};" - - %Originator= @item.collector_name - %OriginationDate= @item.originated_on - %BextVersion 1 - %CodingHistory A=PCM,F=96000,W=24,M=stereo,T=Paragest Pipeline - =# %COMMENT= @item.ingest_notes diff --git a/cdk/lib/app-stack.ts b/cdk/lib/app-stack.ts index 7db1474f..c745a4b8 100644 --- a/cdk/lib/app-stack.ts +++ b/cdk/lib/app-stack.ts @@ -1,24 +1,29 @@ -import * as cdk from 'aws-cdk-lib'; -import { Construct } from 'constructs'; +import * as cdk from "aws-cdk-lib"; +import { Construct } from "constructs"; -import * as autoscaling from 'aws-cdk-lib/aws-autoscaling'; -import * as backup from 'aws-cdk-lib/aws-backup'; -import * as ec2 from 'aws-cdk-lib/aws-ec2'; -import * as ecs from 'aws-cdk-lib/aws-ecs'; -import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as rds from 'aws-cdk-lib/aws-rds'; -import * as route53 from 'aws-cdk-lib/aws-route53'; -import * as ses from 'aws-cdk-lib/aws-ses'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; -import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; +import * as autoscaling from "aws-cdk-lib/aws-autoscaling"; +import * as backup from "aws-cdk-lib/aws-backup"; +import * as ec2 from "aws-cdk-lib/aws-ec2"; +import * as ecs from "aws-cdk-lib/aws-ecs"; +import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as rds from "aws-cdk-lib/aws-rds"; +import * as route53 from "aws-cdk-lib/aws-route53"; +import * as ses from "aws-cdk-lib/aws-ses"; +import * as ssm from "aws-cdk-lib/aws-ssm"; +import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; -import { NagSuppressions } from 'cdk-nag'; +import { NagSuppressions } from "cdk-nag"; -import { AppProps } from './types'; +import { AppProps } from "./types"; export class AppStack extends cdk.Stack { - constructor(scope: Construct, id: string, appProps: AppProps, props?: cdk.StackProps) { + constructor( + scope: Construct, + id: string, + appProps: AppProps, + props?: cdk.StackProps, + ) { super(scope, id, props); const { @@ -38,24 +43,48 @@ export class AppStack extends cdk.Stack { // Network // //////////////////////// - const vpc = ec2.Vpc.fromLookup(this, 'VPC', { vpcId: ssm.StringParameter.valueFromLookup(this, '/usyd/resources/vpc-id') }); + const vpc = ec2.Vpc.fromLookup(this, "VPC", { + vpcId: ssm.StringParameter.valueFromLookup( + this, + "/usyd/resources/vpc-id", + ), + }); - const dataSubnetIds = ['a', 'b', 'c'].map((az) => ssm.StringParameter.valueForStringParameter(this, `/usyd/resources/subnets/isolated/apse2${az}-id`)); - const dataSubnets = dataSubnetIds.map((subnetId, index) => ec2.Subnet.fromSubnetAttributes(this, `DataSubnet${index}`, { subnetId })); + const dataSubnetIds = ["a", "b", "c"].map((az) => + ssm.StringParameter.valueForStringParameter( + this, + `/usyd/resources/subnets/isolated/apse2${az}-id`, + ), + ); + const dataSubnets = dataSubnetIds.map((subnetId, index) => + ec2.Subnet.fromSubnetAttributes(this, `DataSubnet${index}`, { subnetId }), + ); - const appSubnetIds = ['a', 'b', 'c'].map((az) => ssm.StringParameter.valueForStringParameter(this, `/usyd/resources/subnets/public/apse2${az}-id`)); - const appSubnets = appSubnetIds.map((subnetId, index) => ec2.Subnet.fromSubnetAttributes(this, `AppSubnet${index}`, { subnetId })); + const appSubnetIds = ["a", "b", "c"].map((az) => + ssm.StringParameter.valueForStringParameter( + this, + `/usyd/resources/subnets/public/apse2${az}-id`, + ), + ); + const appSubnets = appSubnetIds.map((subnetId, index) => + ec2.Subnet.fromSubnetAttributes(this, `AppSubnet${index}`, { subnetId }), + ); // //////////////////////// // Database // //////////////////////// - const db = new rds.DatabaseInstance(this, 'RdsInstance', { - engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0 }), - instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE4_GRAVITON, ec2.InstanceSize.MICRO), + const db = new rds.DatabaseInstance(this, "RdsInstance", { + engine: rds.DatabaseInstanceEngine.mysql({ + version: rds.MysqlEngineVersion.VER_8_0, + }), + instanceType: ec2.InstanceType.of( + ec2.InstanceClass.BURSTABLE4_GRAVITON, + ec2.InstanceSize.MICRO, + ), // storageEncrypted: true, // NOTE: It defaults to true, but SonarQube doesn't seem to know that - credentials: rds.Credentials.fromGeneratedSecret('nabu'), - databaseName: 'nabu', + credentials: rds.Credentials.fromGeneratedSecret("nabu"), + databaseName: "nabu", vpc, vpcSubnets: { subnets: dataSubnets, @@ -65,10 +94,10 @@ export class AppStack extends cdk.Stack { NagSuppressions.addResourceSuppressions( db, [ - { id: 'AwsSolutions-RDS3', reason: 'Single AZ app, HA not needed' }, - { id: 'AwsSolutions-RDS11', reason: 'Standard port is fine' }, - { id: 'AwsSolutions-SMG4', reason: 'Rails doesn\'t support rotation' }, - { id: 'AwsSolutions-RDS2', reason: 'FIXME: We should have encryption' }, // FIXME: We should really fix this + { id: "AwsSolutions-RDS3", reason: "Single AZ app, HA not needed" }, + { id: "AwsSolutions-RDS11", reason: "Standard port is fine" }, + { id: "AwsSolutions-SMG4", reason: "Rails doesn't support rotation" }, + { id: "AwsSolutions-RDS2", reason: "FIXME: We should have encryption" }, // FIXME: We should really fix this ], true, ); @@ -77,23 +106,23 @@ export class AppStack extends cdk.Stack { // ECS Cluster // //////////////////////// - const cluster = new ecs.Cluster(this, 'Cluster', { + const cluster = new ecs.Cluster(this, "Cluster", { clusterName: appName, vpc, containerInsights: true, }); cluster.addDefaultCloudMapNamespace({ - name: 'nabu', + name: "nabu", useForServiceConnect: true, }); - const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'EcsASG', { + const autoScalingGroup = new autoscaling.AutoScalingGroup(this, "EcsASG", { vpc, vpcSubnets: { subnets: appSubnets, }, - instanceType: new ec2.InstanceType('m6a.xlarge'), + instanceType: new ec2.InstanceType("m6a.xlarge"), machineImage: ecs.EcsOptimizedImage.amazonLinux2(), minCapacity: 1, @@ -101,65 +130,91 @@ export class AppStack extends cdk.Stack { // keyName: 'nabu', }); - NagSuppressions.addResourceSuppressions( - autoScalingGroup, - [ - { id: 'AwsSolutions-EC26', reason: 'EBS coume already encrypted due to AMI defaults' }, - { id: 'AwsSolutions-AS3', reason: 'We can live without the other notifications' }, - ], - ); + NagSuppressions.addResourceSuppressions(autoScalingGroup, [ + { + id: "AwsSolutions-EC26", + reason: "EBS coume already encrypted due to AMI defaults", + }, + { + id: "AwsSolutions-AS3", + reason: "We can live without the other notifications", + }, + ]); // needed by service connect - autoScalingGroup.addToRolePolicy(new iam.PolicyStatement({ - actions: ['ecs:Poll'], - resources: ['*'], - })); + autoScalingGroup.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["ecs:Poll"], + resources: ["*"], + }), + ); - const capacityProvider = new ecs.AsgCapacityProvider(this, 'EcsAsgCapacityProvider', { - autoScalingGroup, - }); + const capacityProvider = new ecs.AsgCapacityProvider( + this, + "EcsAsgCapacityProvider", + { + autoScalingGroup, + }, + ); cluster.addAsgCapacityProvider(capacityProvider); // //////////////////////// // Search // //////////////////////// - const searchTaskDefinition = new ecs.Ec2TaskDefinition(this, 'SearchTaskDefinition', { - volumes: [{ - name: 'solr-data', - dockerVolumeConfiguration: { - scope: ecs.Scope.SHARED, - autoprovision: true, - driver: 'local', - }, - }], - }); - const searchContainer = searchTaskDefinition.addContainer('SearchContainer', { - memoryLimitMiB: 1536, - image: ecs.ContainerImage.fromAsset('..', { file: 'docker/search.Dockerfile' }), - portMappings: [{ name: 'search', containerPort: 8983 }], - logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'SearchService' }), - ulimits: [ - { name: ecs.UlimitName.NOFILE, softLimit: 65536, hardLimit: 65536 * 2 }, - ], - }); + const searchTaskDefinition = new ecs.Ec2TaskDefinition( + this, + "SearchTaskDefinition", + { + volumes: [ + { + name: "solr-data", + dockerVolumeConfiguration: { + scope: ecs.Scope.SHARED, + autoprovision: true, + driver: "local", + }, + }, + ], + }, + ); + const searchContainer = searchTaskDefinition.addContainer( + "SearchContainer", + { + memoryLimitMiB: 1536, + image: ecs.ContainerImage.fromAsset("..", { + file: "docker/search.Dockerfile", + }), + portMappings: [{ name: "search", containerPort: 8983 }], + logging: ecs.LogDrivers.awsLogs({ streamPrefix: "SearchService" }), + ulimits: [ + { + name: ecs.UlimitName.NOFILE, + softLimit: 65536, + hardLimit: 65536 * 2, + }, + ], + }, + ); searchContainer.addMountPoints({ containerPath: `/var/solr/mnt/${railsEnv}`, readOnly: false, - sourceVolume: 'solr-data', + sourceVolume: "solr-data", }); - new ecs.Ec2Service(this, 'SearchService', { - serviceName: 'search', + new ecs.Ec2Service(this, "SearchService", { + serviceName: "search", cluster, taskDefinition: searchTaskDefinition, enableExecuteCommand: true, serviceConnectConfiguration: { logDriver: ecs.LogDrivers.awsLogs({ - streamPrefix: 'sc-traffic', + streamPrefix: "sc-traffic", }), - services: [{ - portMappingName: 'search', - }], + services: [ + { + portMappingName: "search", + }, + ], }, }); @@ -167,16 +222,20 @@ export class AppStack extends cdk.Stack { // Proxyist // //////////////////////// - const proxyistTaskDefinition = new ecs.Ec2TaskDefinition(this, 'ProxyistTaskDefinition'); - NagSuppressions.addResourceSuppressions( - proxyistTaskDefinition, - [{ id: 'AwsSolutions-ECS2', reason: 'We are fine with env variables' }], + const proxyistTaskDefinition = new ecs.Ec2TaskDefinition( + this, + "ProxyistTaskDefinition", ); - proxyistTaskDefinition.addContainer('ProxyistContainer', { + NagSuppressions.addResourceSuppressions(proxyistTaskDefinition, [ + { id: "AwsSolutions-ECS2", reason: "We are fine with env variables" }, + ]); + proxyistTaskDefinition.addContainer("ProxyistContainer", { memoryLimitMiB: 256, - image: ecs.ContainerImage.fromAsset('..', { file: 'docker/proxyist.Dockerfile' }), - portMappings: [{ name: 'proxyist', containerPort: 3000 }], - logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'ProxyistService' }), + image: ecs.ContainerImage.fromAsset("..", { + file: "docker/proxyist.Dockerfile", + }), + portMappings: [{ name: "proxyist", containerPort: 3000 }], + logging: ecs.LogDrivers.awsLogs({ streamPrefix: "ProxyistService" }), environment: { AWS_REGION: region, BUCKET_NAME: catalogBucket.bucketName, @@ -184,18 +243,20 @@ export class AppStack extends cdk.Stack { }); catalogBucket.grantReadWrite(proxyistTaskDefinition.taskRole); - new ecs.Ec2Service(this, 'ProxyistService', { - serviceName: 'proxyist', + new ecs.Ec2Service(this, "ProxyistService", { + serviceName: "proxyist", cluster, taskDefinition: proxyistTaskDefinition, enableExecuteCommand: true, serviceConnectConfiguration: { logDriver: ecs.LogDrivers.awsLogs({ - streamPrefix: 'sc-traffic', + streamPrefix: "sc-traffic", }), - services: [{ - portMappingName: 'proxyist', - }], + services: [ + { + portMappingName: "proxyist", + }, + ], }, }); @@ -203,24 +264,28 @@ export class AppStack extends cdk.Stack { // Viewer // //////////////////////// - const viewerTaskDefinition = new ecs.Ec2TaskDefinition(this, 'ViewerTaskDefinition'); - NagSuppressions.addResourceSuppressions( - viewerTaskDefinition, - [{ id: 'AwsSolutions-ECS2', reason: 'We are fine with env variables' }], + const viewerTaskDefinition = new ecs.Ec2TaskDefinition( + this, + "ViewerTaskDefinition", ); - viewerTaskDefinition.addContainer('ViewerContainer', { + NagSuppressions.addResourceSuppressions(viewerTaskDefinition, [ + { id: "AwsSolutions-ECS2", reason: "We are fine with env variables" }, + ]); + viewerTaskDefinition.addContainer("ViewerContainer", { memoryLimitMiB: 128, - image: ecs.ContainerImage.fromAsset('..', { file: 'docker/viewer.Dockerfile' }), - portMappings: [{ name: 'viewer', containerPort: 80 }], - logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'ViewerService' }), + image: ecs.ContainerImage.fromAsset("..", { + file: "docker/viewer.Dockerfile", + }), + portMappings: [{ name: "viewer", containerPort: 80 }], + logging: ecs.LogDrivers.awsLogs({ streamPrefix: "ViewerService" }), environment: { AWS_REGION: region, BUCKET_NAME: catalogBucket.bucketName, }, }); - const viewerService = new ecs.Ec2Service(this, 'ViewerService', { - serviceName: 'viewer', + const viewerService = new ecs.Ec2Service(this, "ViewerService", { + serviceName: "viewer", cluster, taskDefinition: viewerTaskDefinition, enableExecuteCommand: true, @@ -229,128 +294,178 @@ export class AppStack extends cdk.Stack { // ////////////////////// // Secrets // //////////////////////// - const appSecrets = new secretsmanager.Secret(this, 'AppSecrets', { + const appSecrets = new secretsmanager.Secret(this, "AppSecrets", { secretObjectValue: { - recaptcha_site_key: cdk.SecretValue.unsafePlainText('secret'), - recaptcha_secret_key: cdk.SecretValue.unsafePlainText('secret'), - sentry_api_token: cdk.SecretValue.unsafePlainText('secret'), - secret_key_base: cdk.SecretValue.unsafePlainText('secret'), - datacite_user: cdk.SecretValue.unsafePlainText('secret'), - datacite_pass: cdk.SecretValue.unsafePlainText('secret'), + recaptcha_site_key: cdk.SecretValue.unsafePlainText("secret"), + recaptcha_secret_key: cdk.SecretValue.unsafePlainText("secret"), + sentry_api_token: cdk.SecretValue.unsafePlainText("secret"), + secret_key_base: cdk.SecretValue.unsafePlainText("secret"), + datacite_user: cdk.SecretValue.unsafePlainText("secret"), + datacite_pass: cdk.SecretValue.unsafePlainText("secret"), }, }); - NagSuppressions.addResourceSuppressions( - appSecrets, - [{ id: 'AwsSolutions-SMG4', reason: 'No auto rotation needed' }], - ); + NagSuppressions.addResourceSuppressions(appSecrets, [ + { id: "AwsSolutions-SMG4", reason: "No auto rotation needed" }, + ]); // //////////////////////// // App // //////////////////////// - const appImage = ecs.ContainerImage.fromAsset('..', { file: 'Dockerfile' }); + const appImage = ecs.ContainerImage.fromAsset("..", { file: "Dockerfile" }); const commonAppImageOptions: ecs.ContainerDefinitionOptions = { image: appImage, environment: { - RAILS_SERVE_STATIC_FILES: 'true', // TODO: do we need nginx in production?? + RAILS_SERVE_STATIC_FILES: "true", // TODO: do we need nginx in production?? RAILS_ENV: railsEnv, SOLR_URL: `http://search.nabu:8983/solr/${railsEnv}`, - PROXYIST_URL: 'http://proxyist.nabu:3000', - SENTRY_DSN: 'https://aa8f28b06df84f358949b927e85a924e@o4504801902985216.ingest.sentry.io/4504801910980608', - DOI_PREFIX: '10.26278', - DATACITE_BASE_URL: 'https://mds.datacite.org', + PROXYIST_URL: "http://proxyist.nabu:3000", + SENTRY_DSN: + "https://aa8f28b06df84f358949b927e85a924e@o4504801902985216.ingest.sentry.io/4504801910980608", + DOI_PREFIX: "10.26278", + DATACITE_BASE_URL: "https://mds.datacite.org", AWS_REGION: region, }, secrets: { - SECRET_KEY_BASE: ecs.Secret.fromSecretsManager(appSecrets, 'secret_key_base'), - NABU_DATABASE_PASSWORD: ecs.Secret.fromSecretsManager(db.secret!, 'password'), - NABU_DATABASE_HOSTNAME: ecs.Secret.fromSecretsManager(db.secret!, 'host'), - RECAPTCHA_SITE_KEY: ecs.Secret.fromSecretsManager(appSecrets, 'recaptcha_site_key'), - RECAPTCHA_SECRET_KEY: ecs.Secret.fromSecretsManager(appSecrets, 'recaptcha_secret_key'), - SENTRY_API_TOKEN: ecs.Secret.fromSecretsManager(appSecrets, 'sentry_api_token'), - DATACITE_USER: ecs.Secret.fromSecretsManager(appSecrets, 'datacite_user'), - DATACITE_PASS: ecs.Secret.fromSecretsManager(appSecrets, 'datacite_pass'), + SECRET_KEY_BASE: ecs.Secret.fromSecretsManager( + appSecrets, + "secret_key_base", + ), + NABU_DATABASE_PASSWORD: ecs.Secret.fromSecretsManager( + db.secret!, + "password", + ), + NABU_DATABASE_HOSTNAME: ecs.Secret.fromSecretsManager( + db.secret!, + "host", + ), + RECAPTCHA_SITE_KEY: ecs.Secret.fromSecretsManager( + appSecrets, + "recaptcha_site_key", + ), + RECAPTCHA_SECRET_KEY: ecs.Secret.fromSecretsManager( + appSecrets, + "recaptcha_secret_key", + ), + SENTRY_API_TOKEN: ecs.Secret.fromSecretsManager( + appSecrets, + "sentry_api_token", + ), + DATACITE_USER: ecs.Secret.fromSecretsManager( + appSecrets, + "datacite_user", + ), + DATACITE_PASS: ecs.Secret.fromSecretsManager( + appSecrets, + "datacite_pass", + ), }, }; - const appTaskDefinition = new ecs.Ec2TaskDefinition(this, 'AppTaskDefinition'); - NagSuppressions.addResourceSuppressions( - appTaskDefinition, - [{ id: 'AwsSolutions-ECS2', reason: 'We are fine with env variables' }], + const appTaskDefinition = new ecs.Ec2TaskDefinition( + this, + "AppTaskDefinition", ); - appTaskDefinition.addContainer('AppContainer', { + NagSuppressions.addResourceSuppressions(appTaskDefinition, [ + { id: "AwsSolutions-ECS2", reason: "We are fine with env variables" }, + ]); + appTaskDefinition.addContainer("AppContainer", { ...commonAppImageOptions, // NOTE: This is huge due to being able to show all 30000 items on the one page - memoryLimitMiB: 2048, + memoryLimitMiB: 4096, portMappings: [{ containerPort: 3000 }], - logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'AppService' }), + logging: ecs.LogDrivers.awsLogs({ streamPrefix: "AppService" }), }); - appTaskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ - actions: ['ses:SendRawEmail'], - resources: ['*'], - })); + appTaskDefinition.addToTaskRolePolicy( + new iam.PolicyStatement({ + actions: ["ses:SendRawEmail"], + resources: ["*"], + }), + ); - const appService = new ecs.Ec2Service(this, 'AppService', { - serviceName: 'app', + const appService = new ecs.Ec2Service(this, "AppService", { + serviceName: "app", cluster, taskDefinition: appTaskDefinition, enableExecuteCommand: true, }); appService.enableServiceConnect(); - db.connections.allowDefaultPortFrom(autoScalingGroup, 'Allow from ECS service'); - const loadBalancer = elbv2.ApplicationLoadBalancer.fromLookup(this, 'AppAlb', { - loadBalancerArn: ssm.StringParameter.valueFromLookup(this, '/usyd/resources/application-load-balancer/application/arn'), - }); - loadBalancer.connections.allowTo(autoScalingGroup, ec2.Port.allTcp(), 'Allow from LB to ECS service'); + db.connections.allowDefaultPortFrom( + autoScalingGroup, + "Allow from ECS service", + ); + const loadBalancer = elbv2.ApplicationLoadBalancer.fromLookup( + this, + "AppAlb", + { + loadBalancerArn: ssm.StringParameter.valueFromLookup( + this, + "/usyd/resources/application-load-balancer/application/arn", + ), + }, + ); + loadBalancer.connections.allowTo( + autoScalingGroup, + ec2.Port.allTcp(), + "Allow from LB to ECS service", + ); // //////////////////////// // Jobs // //////////////////////// - const jobsTaskDefinition = new ecs.Ec2TaskDefinition(this, 'JobsTaskDefinition'); - NagSuppressions.addResourceSuppressions( - jobsTaskDefinition, - [{ id: 'AwsSolutions-ECS2', reason: 'We are fine with env variables' }], + const jobsTaskDefinition = new ecs.Ec2TaskDefinition( + this, + "JobsTaskDefinition", ); - jobsTaskDefinition.addContainer('JobsContainer', { + NagSuppressions.addResourceSuppressions(jobsTaskDefinition, [ + { id: "AwsSolutions-ECS2", reason: "We are fine with env variables" }, + ]); + jobsTaskDefinition.addContainer("JobsContainer", { ...commonAppImageOptions, - memoryLimitMiB: 512, - logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'JobsService' }), - command: ['bin/delayed_job', 'run'], + memoryLimitMiB: 1024, + logging: ecs.LogDrivers.awsLogs({ streamPrefix: "JobsService" }), + command: ["bin/delayed_job", "run"], }); - jobsTaskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ - actions: ['ses:SendRawEmail'], - resources: ['*'], - })); + jobsTaskDefinition.addToTaskRolePolicy( + new iam.PolicyStatement({ + actions: ["ses:SendRawEmail"], + resources: ["*"], + }), + ); - const jobsService = new ecs.Ec2Service(this, 'JobsService', { - serviceName: 'jobs', + const jobsService = new ecs.Ec2Service(this, "JobsService", { + serviceName: "jobs", cluster, taskDefinition: jobsTaskDefinition, enableExecuteCommand: true, }); jobsService.enableServiceConnect(); - if (env === 'prod') { - const cronTaskDefinition = new ecs.Ec2TaskDefinition(this, 'CronTaskDefinition'); - NagSuppressions.addResourceSuppressions( - cronTaskDefinition, - [{ id: 'AwsSolutions-ECS2', reason: 'We are fine with env variables' }], + if (env === "prod") { + const cronTaskDefinition = new ecs.Ec2TaskDefinition( + this, + "CronTaskDefinition", ); - cronTaskDefinition.addContainer('CronContainer', { + NagSuppressions.addResourceSuppressions(cronTaskDefinition, [ + { id: "AwsSolutions-ECS2", reason: "We are fine with env variables" }, + ]); + cronTaskDefinition.addContainer("CronContainer", { ...commonAppImageOptions, memoryLimitMiB: 512, - logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'CronService' }), - command: ['bundle', 'exec', 'cron-worker/cron.rb'], + logging: ecs.LogDrivers.awsLogs({ streamPrefix: "CronService" }), + command: ["bundle", "exec", "cron-worker/cron.rb"], }); - cronTaskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ - actions: ['ses:SendEmail'], - resources: ['*'], - })); + cronTaskDefinition.addToTaskRolePolicy( + new iam.PolicyStatement({ + actions: ["ses:SendEmail"], + resources: ["*"], + }), + ); - const cronService = new ecs.Ec2Service(this, 'CronService', { - serviceName: 'cron', + const cronService = new ecs.Ec2Service(this, "CronService", { + serviceName: "cron", cluster, taskDefinition: cronTaskDefinition, enableExecuteCommand: true, @@ -362,41 +477,64 @@ export class AppStack extends cdk.Stack { // Application Load Balancer // //////////////////////// - const sslListener = elbv2.ApplicationListener.fromLookup(this, 'AlbSslListener', { - loadBalancerArn: ssm.StringParameter.valueFromLookup(this, '/usyd/resources/application-load-balancer/application/arn'), - listenerProtocol: elbv2.ApplicationProtocol.HTTPS, - }); - if (env === 'prod') { - sslListener.addCertificates('TempCatalogCert', [elbv2.ListenerCertificate.fromArn(tempCertificate.certificateArn)]); + const sslListener = elbv2.ApplicationListener.fromLookup( + this, + "AlbSslListener", + { + loadBalancerArn: ssm.StringParameter.valueFromLookup( + this, + "/usyd/resources/application-load-balancer/application/arn", + ), + listenerProtocol: elbv2.ApplicationProtocol.HTTPS, + }, + ); + if (env === "prod") { + sslListener.addCertificates("TempCatalogCert", [ + elbv2.ListenerCertificate.fromArn(tempCertificate.certificateArn), + ]); } - const appTargetGroup = new elbv2.ApplicationTargetGroup(this, 'AppTargetGroup', { - targets: [appService], - vpc, - protocol: elbv2.ApplicationProtocol.HTTP, - deregistrationDelay: cdk.Duration.seconds(30), - }); + const appTargetGroup = new elbv2.ApplicationTargetGroup( + this, + "AppTargetGroup", + { + targets: [appService], + vpc, + protocol: elbv2.ApplicationProtocol.HTTP, + deregistrationDelay: cdk.Duration.seconds(30), + }, + ); - sslListener.addTargetGroups('AlbTargetGroups', { + sslListener.addTargetGroups("AlbTargetGroups", { targetGroups: [appTargetGroup], priority: 10, conditions: [ - elbv2.ListenerCondition.hostHeaders(['catalog.paradisec.org.au', `catalog.${zoneName}`]), + elbv2.ListenerCondition.hostHeaders([ + "catalog.paradisec.org.au", + `catalog.${zoneName}`, + ]), ], }); - const viewerTargetGroup = new elbv2.ApplicationTargetGroup(this, 'ViewerTargetGroup', { - targets: [viewerService], - vpc, - protocol: elbv2.ApplicationProtocol.HTTP, - }); + const viewerTargetGroup = new elbv2.ApplicationTargetGroup( + this, + "ViewerTargetGroup", + { + targets: [viewerService], + vpc, + protocol: elbv2.ApplicationProtocol.HTTP, + }, + ); - sslListener.addTargetGroups('ViewerTargetGroups', { + sslListener.addTargetGroups("ViewerTargetGroups", { targetGroups: [viewerTargetGroup], priority: 5, conditions: [ - elbv2.ListenerCondition.hostHeaders(['catalog.paradisec.org.au', `catalog.${zoneName}`]), - elbv2.ListenerCondition.pathPatterns(['/viewer/*']), + elbv2.ListenerCondition.hostHeaders([ + "catalog.paradisec.org.au", + `catalog.${zoneName}`, + ]), + elbv2.ListenerCondition.pathPatterns(["/viewer/*"]), ], }); @@ -404,8 +542,8 @@ export class AppStack extends cdk.Stack { // DNS // //////////////////////// - new route53.CnameRecord(this, 'CatalogRecord', { - recordName: 'catalog', + new route53.CnameRecord(this, "CatalogRecord", { + recordName: "catalog", zone, domainName: cloudflare, }); @@ -415,18 +553,18 @@ export class AppStack extends cdk.Stack { // //////////////////////// // From - new ses.EmailIdentity(this, 'AdminSesIdentity', { - identity: ses.Identity.email('admin@paradisec.org.au'), + new ses.EmailIdentity(this, "AdminSesIdentity", { + identity: ses.Identity.email("admin@paradisec.org.au"), }); - if (env === 'stage') { + if (env === "stage") { // To const testers = [ - 'johnf@inodes.org', - 'jodie.kell@sydney.edu.au', - 'julia.miller@anu.edu.au', - 'enwardy@hotmail.com', - 'thien@unimelb.edu.au', + "johnf@inodes.org", + "jodie.kell@sydney.edu.au", + "julia.miller@anu.edu.au", + "enwardy@hotmail.com", + "thien@unimelb.edu.au", ]; testers.forEach((email) => { new ses.EmailIdentity(this, `TesterSesIdentity-${email}`, { @@ -439,19 +577,28 @@ export class AppStack extends cdk.Stack { // Backups // //////////////////////// - const plan = backup.BackupPlan.dailyMonthly1YearRetention(this, 'BackupPlan'); + const plan = backup.BackupPlan.dailyMonthly1YearRetention( + this, + "BackupPlan", + ); - plan.addSelection('BackupSelection', { - resources: [ - backup.BackupResource.fromRdsDatabaseInstance(db), - ], + plan.addSelection("BackupSelection", { + resources: [backup.BackupResource.fromRdsDatabaseInstance(db)], }); NagSuppressions.addResourceSuppressions( plan, - [{ id: 'AwsSolutions-IAM4', reason: 'Managed Policy is fine', appliesTo: ['Policy::arn::iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup'] }], + [ + { + id: "AwsSolutions-IAM4", + reason: "Managed Policy is fine", + appliesTo: [ + "Policy::arn::iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup", + ], + }, + ], true, ); - cdk.Tags.of(this).add('uni:billing:application', 'para'); + cdk.Tags.of(this).add("uni:billing:application", "para"); } }