From 78892d7ab238088505b5020f602277c90101505c Mon Sep 17 00:00:00 2001 From: Tom Richards Date: Mon, 27 Feb 2023 09:22:17 +0000 Subject: [PATCH] add a separate code bucket to segregate code static sites --- README.md | 9 +- action.yaml | 14 +- cdk/__snapshots__/infra.test.ts.snap | 217 ++++++++++++++++----------- cdk/infra.ts | 22 ++- cdk/static-site.ts | 1 - index.js | 12 ++ index.ts | 14 ++ service/main.go | 13 +- 8 files changed, 198 insertions(+), 104 deletions(-) diff --git a/README.md b/README.md index cff8cc7..961875a 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,20 @@ jobs: ## Inputs ### **app** `string` (required): - The app name. Used for the Riffraff deployment name and also to tag AWS resources. Typically this would be the first part of your domain name - e.g. 'example' for 'example.gutools.co.uk'. -### **domain** `string` (required): +### **domain** `string` (required): The domain should be a Guardian-owned domain. For internal tools, `[app].gutools.co.uk` is recommended but check it is free first! -### **artifact** `string` (optional - default='artifact') +### **codeDomain** `string` (optional): +The domain should be a Guardian-owned domain. For internal tools, +`[name].code.dev-gutools.co.uk` is recommended but check it is free first! +Only use this option if your project really needs a lower envionment. +### **artifact** `string` (optional - default='artifact') Name of the artifact containing the static resources. Should be uploaded in an earlier workflow step. diff --git a/action.yaml b/action.yaml index 5dd3d3e..c799783 100644 --- a/action.yaml +++ b/action.yaml @@ -7,6 +7,9 @@ inputs: domain: description: A Guardian-owned domain. [name].gutools.co.uk is recommended. required: true + codeDomain: + description: OPTIONAL Guardian-owned domain for CODE environment. [name].code.dev-gutools.co.uk is recommended. + required: false artifact: description: 'Name of artifact containing the static site. Should be uploaded in an earlier workflow step.' required: false @@ -31,6 +34,7 @@ runs: env: INPUT_APP: ${{ inputs.app }} INPUT_DOMAIN: ${{ inputs.domain }} + INPUT_CODE_DOMAIN: ${{ inputs.codeDomain }} INPUT_ARTIFACT: ${{ inputs.artifact }} INPUT_DRYRUN: ${{ inputs.dryRun}} INPUT_ACTIONS_RUNTIME_TOKEN: ${ github.token } @@ -64,19 +68,25 @@ runs: - eu-west-1 allowedStages: - PROD + ${{ inputs.codeDomain && '- CODE' || '' }} deployments: cfn: type: cloud-formation app: ${{ inputs.app }} parameters: - templatePath: cfn.json + templatePath: + templateStagePaths: + PROD: cfn.json + ${{ inputs.codeDomain && 'CODE: cfn-CODE.json' || '' }} static-site-assets: type: aws-s3 app: ${{ inputs.domain }} # A hack to prefix uploads with the domain. parameters: cacheControl: private publicReadAcl: false - bucket: deploy-infra-actions-static-site-i-staticd8c87b36-jyufgyb0llkj # TODO replace with SSM param once possible. + bucketSsmKeyStageParam: + PROD: /INFRA/deploy/actions-static-site-infra/bucket + CODE: /INFRA/deploy/actions-static-site-infra/codeBucket prefixApp: true # See comment on `app` above. prefixStack: false prefixStage: false diff --git a/cdk/__snapshots__/infra.test.ts.snap b/cdk/__snapshots__/infra.test.ts.snap index ec9ec76..b696280 100644 --- a/cdk/__snapshots__/infra.test.ts.snap +++ b/cdk/__snapshots__/infra.test.ts.snap @@ -203,6 +203,11 @@ Environment=\\"BUCKET=", "Ref": "staticD8C87B36", }, "\\" +Environment=\\"CODE_BUCKET=", + Object { + "Ref": "codestaticB41DF3D7", + }, + "\\" Environment=\\"PORT=9000\\" ExecStart=/app @@ -652,6 +657,36 @@ systemctl start app }, ], }, + Object { + "Action": Array [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*", + ], + "Effect": "Allow", + "Resource": Array [ + Object { + "Fn::GetAtt": Array [ + "codestaticB41DF3D7", + "Arn", + ], + }, + Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "codestaticB41DF3D7", + "Arn", + ], + }, + "/*", + ], + ], + }, + ], + }, ], "Version": "2012-10-17", }, @@ -1112,6 +1147,99 @@ systemctl start app }, "Type": "AWS::EC2::SecurityGroup", }, + "codestaticB41DF3D7": Object { + "DeletionPolicy": "Retain", + "Properties": Object { + "Tags": Array [ + Object { + "Key": "gu:cdk:version", + "Value": "48.5.1", + }, + Object { + "Key": "gu:repo", + "Value": "guardian/actions-static-site", + }, + Object { + "Key": "Stack", + "Value": "stack", + }, + Object { + "Key": "Stage", + "Value": "INFRA", + }, + ], + "WebsiteConfiguration": Object { + "IndexDocument": "index.html", + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + "codestaticPolicy56B5E7C1": Object { + "Properties": Object { + "Bucket": Object { + "Ref": "codestaticB41DF3D7", + }, + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "s3:PutObject", + "Effect": "Allow", + "Principal": Object { + "AWS": Array [ + Object { + "Fn::Join": Array [ + "", + Array [ + "arn:", + Object { + "Ref": "AWS::Partition", + }, + ":iam::000000000016:root", + ], + ], + }, + "arn:aws:iam::000000000016:role/galaxies-data-refresher-lambda-role-CODE", + ], + }, + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + Object { + "Fn::GetAtt": Array [ + "codestaticB41DF3D7", + "Arn", + ], + }, + "/galaxies.code.dev-gutools.co.uk/data/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::S3::BucketPolicy", + }, + "codestaticsitebucketCDD8A031": Object { + "Properties": Object { + "Description": "Bucket for CODE static sites.", + "Name": "/INFRA/stack/app/codeBucket", + "Tags": Object { + "Stack": "stack", + "Stage": "INFRA", + "gu:cdk:version": "48.5.1", + "gu:repo": "guardian/actions-static-site", + }, + "Type": "String", + "Value": Object { + "Ref": "codestaticB41DF3D7", + }, + }, + "Type": "AWS::SSM::Parameter", + }, "ldpaccess567AC006": Object { "Properties": Object { "GroupDescription": "static-site-INFRA/ldp-access", @@ -1202,95 +1330,6 @@ systemctl start app ], }, }, - Object { - "Action": "s3:ListBucket", - "Condition": Object { - "StringLike": Object { - "s3:prefix": Array [ - "galaxies.gutools.co.uk/data/*", - ], - }, - }, - "Effect": "Allow", - "Principal": Object { - "AWS": "arn:aws:iam::000000000016:role/galaxies-data-refresher-lambda-role-PROD", - }, - "Resource": Object { - "Fn::GetAtt": Array [ - "staticD8C87B36", - "Arn", - ], - }, - }, - Object { - "Action": "s3:PutObject", - "Effect": "Allow", - "Principal": Object { - "AWS": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::000000000016:root", - ], - ], - }, - "arn:aws:iam::000000000016:role/galaxies-data-refresher-lambda-role-CODE", - ], - }, - "Resource": Object { - "Fn::Join": Array [ - "", - Array [ - Object { - "Fn::GetAtt": Array [ - "staticD8C87B36", - "Arn", - ], - }, - "/galaxies.code.dev-gutools.co.uk/data/*", - ], - ], - }, - }, - Object { - "Action": "s3:ListBucket", - "Condition": Object { - "StringLike": Object { - "s3:prefix": Array [ - "galaxies.code.dev-gutools.co.uk/data/*", - ], - }, - }, - "Effect": "Allow", - "Principal": Object { - "AWS": Array [ - Object { - "Fn::Join": Array [ - "", - Array [ - "arn:", - Object { - "Ref": "AWS::Partition", - }, - ":iam::000000000016:root", - ], - ], - }, - "arn:aws:iam::000000000016:role/galaxies-data-refresher-lambda-role-CODE", - ], - }, - "Resource": Object { - "Fn::GetAtt": Array [ - "staticD8C87B36", - "Arn", - ], - }, - }, ], "Version": "2012-10-17", }, diff --git a/cdk/infra.ts b/cdk/infra.ts index 93c5a41..b5b0cdf 100644 --- a/cdk/infra.ts +++ b/cdk/infra.ts @@ -47,7 +47,10 @@ export class Infra extends GuStack { constructor(scope: App, id: string, props: InfraProps) { super(scope, id, props); - const bucket = new Bucket(this, "static", { + const prodBucket = new Bucket(this, "static", { + websiteIndexDocument: 'index.html', + }) + const codeBucket = new Bucket(this, "code-static", { websiteIndexDocument: 'index.html', }) @@ -62,7 +65,8 @@ cat << EOF > /etc/systemd/system/${app}.service Description=Static Site service [Service] -Environment="BUCKET=${bucket.bucketName}" +Environment="BUCKET=${prodBucket.bucketName}" +Environment="CODE_BUCKET=${codeBucket.bucketName}" Environment="PORT=${port}" ExecStart=/${app} @@ -106,7 +110,8 @@ systemctl start ${app} ec2.loadBalancer.addSecurityGroup(sg) - bucket.grantRead(ec2.autoScalingGroup) + prodBucket.grantRead(ec2.autoScalingGroup) + codeBucket.grantRead(ec2.autoScalingGroup) // Google Auth stuff... @@ -155,7 +160,12 @@ systemctl start ${app} new StringParameter(this, 'static-site-bucket', { description: 'Bucket for static sites.', parameterName: `${configPrefix}/bucket`, - stringValue: bucket.bucketName, + stringValue: prodBucket.bucketName, + }); + new StringParameter(this, 'code-static-site-bucket', { + description: 'Bucket for CODE static sites.', + parameterName: `${configPrefix}/codeBucket`, + stringValue: codeBucket.bucketName, }); // Used by static site Cloudformations to attach certs. @@ -175,12 +185,14 @@ systemctl start ${app} // https://github.com/guardian/galaxies Object.values({ PROD: { + bucket: prodBucket, prefix: "galaxies.gutools.co.uk/data/*", principals: [new ArnPrincipal( `arn:aws:iam::${GuardianAwsAccounts.DeveloperPlayground}:role/galaxies-data-refresher-lambda-role-PROD` )] }, CODE: { + bucket: codeBucket, prefix: "galaxies.code.dev-gutools.co.uk/data/*", principals: [ new AccountPrincipal(GuardianAwsAccounts.DeveloperPlayground), // for local development @@ -188,7 +200,7 @@ systemctl start ${app} `arn:aws:iam::${GuardianAwsAccounts.DeveloperPlayground}:role/galaxies-data-refresher-lambda-role-CODE` )] } - }).forEach(({ principals, prefix }) => { + }).forEach(({ bucket, principals, prefix }) => { bucket.addToResourcePolicy( new PolicyStatement({ resources: [bucket.arnForObjects(prefix)], diff --git a/cdk/static-site.ts b/cdk/static-site.ts index e154189..c0ce3dc 100644 --- a/cdk/static-site.ts +++ b/cdk/static-site.ts @@ -8,7 +8,6 @@ import { import { GuCname } from "@guardian/cdk/lib/constructs/dns/"; import type { App} from "aws-cdk-lib"; import { Duration } from "aws-cdk-lib"; -import { Certificate } from "aws-cdk-lib/aws-certificatemanager"; import { CfnListenerCertificate, } from "aws-cdk-lib/aws-elasticloadbalancingv2"; diff --git a/index.js b/index.js index a710561..8c21964 100644 --- a/index.js +++ b/index.js @@ -908983,6 +908983,7 @@ var StaticSite = class extends import_core.GuStack { var main = () => { const app = core.getInput("app", { required: true }); const domain = core.getInput("domain", { required: true }); + const codeDomain = core.getInput("codeDomain", { required: false }); const stack = "deploy"; core.info("Inputs are: " + JSON.stringify({ app, stack, domain })); const cdkApp = new import_aws_cdk_lib2.App(); @@ -908994,6 +908995,17 @@ var main = () => { }); const cfn = import_assertions.Template.fromStack(cdkStack).toJSON(); fs.writeFileSync("cfn.json", JSON.stringify(cfn, void 0, 2)); + if (codeDomain) { + const cdkAppCODE = new import_aws_cdk_lib2.App(); + const cdkStackCODE = new StaticSite(cdkAppCODE, "static-site-code", { + app, + stack, + stage: "CODE", + domainName: codeDomain + }); + const cfnCODE = import_assertions.Template.fromStack(cdkStackCODE).toJSON(); + fs.writeFileSync("cfn-CODE.json", JSON.stringify(cfnCODE, void 0, 2)); + } }; try { if (require.main === module) diff --git a/index.ts b/index.ts index eaefb37..d5ec8a0 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,7 @@ import { StaticSite } from "./cdk/static-site"; export const main = (): void => { const app = core.getInput("app", { required: true }); const domain = core.getInput("domain", { required: true }); + const codeDomain = core.getInput("codeDomain", { required: false }); const stack = "deploy"; @@ -25,6 +26,19 @@ export const main = (): void => { const cfn = Template.fromStack(cdkStack).toJSON(); fs.writeFileSync("cfn.json", JSON.stringify(cfn, undefined, 2)); + + if (codeDomain) { + const cdkAppCODE = new App(); + const cdkStackCODE = new StaticSite(cdkAppCODE, "static-site-code", { + app, + stack, + stage: "CODE", + domainName: codeDomain, + }); + + const cfnCODE = Template.fromStack(cdkStackCODE).toJSON(); + fs.writeFileSync("cfn-CODE.json", JSON.stringify(cfnCODE, undefined, 2)); + } }; try { diff --git a/service/main.go b/service/main.go index df9c018..399d50c 100644 --- a/service/main.go +++ b/service/main.go @@ -15,7 +15,7 @@ import ( ) type Config struct { - Port, Bucket string + Port, Bucket, CodeBucket string // Override when local for easier testing. RequireAuth bool @@ -43,6 +43,7 @@ func optional(key, fallback string) string { func getConfig() Config { return Config{ Bucket: required("BUCKET"), + CodeBucket: required("CODE_BUCKET"), Port: optional("PORT", "3333"), RequireAuth: optional("REQUIRE_AUTH", "true") != "false", Profile: optional("PROFILE", ""), @@ -52,13 +53,14 @@ func getConfig() Config { func main() { config := getConfig() store := s3.New(config.Bucket, config.Profile) + codeStore := s3.New(config.CodeBucket, config.Profile) http.HandleFunc("/healthcheck", middleware.WithRequestLog(http.HandlerFunc(ok))) if config.RequireAuth { - http.Handle("/", middleware.WithRequestLog(middleware.WithAuth(middleware.WithDomainPrefix(storeServer(store))))) + http.Handle("/", middleware.WithRequestLog(middleware.WithAuth(middleware.WithDomainPrefix(storeServer(store, codeStore))))) } else { - http.Handle("/", middleware.WithRequestLog(middleware.WithDomainPrefix(storeServer(store)))) + http.Handle("/", middleware.WithRequestLog(middleware.WithDomainPrefix(storeServer(store, codeStore)))) } log.Printf("Server starting on http://localhost:%s.", config.Port) @@ -70,10 +72,13 @@ func ok(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "OK") } -func storeServer(store store.Store) http.HandlerFunc { +func storeServer(store store.Store, fallbackStore store.Store) http.HandlerFunc { return func(resp http.ResponseWriter, req *http.Request) { key := req.URL.Path got, err := store.Get(key) + if err != nil { + got, err = fallbackStore.Get(key) + } if err != nil { log.Printf("unable to fetch from store for path %s and host %s: %v", req.URL.Path, req.Host, err) statusNotFound(resp)