diff --git a/README.md b/README.md index d16dfe7..fbf24bc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -### Serverless Release Dashboard +### Moot - A Serverless Release Dashboard AWS Serverless solution deployed with Terraform which implements a single-page-application dashboard. This dashboard creates releases that are intended to trigger continuous integration (CI) production deploy pipelines. All that is needed to kick off a release is a version number. @@ -19,11 +19,11 @@ This solution utilises the following services: #### Repositories View -![alt text](https://github.com/seanturner026/serverless-release-dashboard/blob/main/assets/repositories.png?raw=true) +![alt text](https://github.com/seanturner026/moot/blob/main/assets/repositories.png?raw=true) #### Add Repository View -![alt text](https://github.com/seanturner026/serverless-release-dashboard/blob/main/assets/repositories-add.png?raw=true) +![alt text](https://github.com/seanturner026/moot/blob/main/assets/repositories-add.png?raw=true) #### Users View -![alt text](https://github.com/seanturner026/serverless-release-dashboard/blob/main/assets/users.png?raw=true) +![alt text](https://github.com/seanturner026/moot/blob/main/assets/users.png?raw=true) diff --git a/cmd/auth/login.go b/cmd/auth/login.go index 330ebe5..c7fee97 100644 --- a/cmd/auth/login.go +++ b/cmd/auth/login.go @@ -9,7 +9,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" - "github.com/seanturner026/serverless-release-dashboard/internal/util" + "github.com/seanturner026/moot/internal/util" log "github.com/sirupsen/logrus" ) diff --git a/cmd/auth/main.go b/cmd/auth/main.go index c31fa4e..e68edd4 100644 --- a/cmd/auth/main.go +++ b/cmd/auth/main.go @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" - "github.com/seanturner026/serverless-release-dashboard/internal/util" + "github.com/seanturner026/moot/internal/util" log "github.com/sirupsen/logrus" ) diff --git a/cmd/auth/reset_password.go b/cmd/auth/reset_password.go index 501c826..da998ef 100644 --- a/cmd/auth/reset_password.go +++ b/cmd/auth/reset_password.go @@ -8,7 +8,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" - "github.com/seanturner026/serverless-release-dashboard/internal/util" + "github.com/seanturner026/moot/internal/util" log "github.com/sirupsen/logrus" ) diff --git a/cmd/post_confirmation/main.go b/cmd/post_confirmation/main.go deleted file mode 100644 index ad883fb..0000000 --- a/cmd/post_confirmation/main.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/aws/aws-lambda-go/events" - "github.com/aws/aws-lambda-go/lambda" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" - "github.com/seanturner026/serverless-release-dashboard/internal/util" - log "github.com/sirupsen/logrus" -) - -type application struct { - Config configuration -} - -type configuration struct { - DB dynamodbiface.DynamoDBAPI - TableName string - UserPoolID string -} - -func (app application) handler(event events.CognitoEventUserPoolsPostConfirmation) (events.APIGatewayV2HTTPResponse, error) { - headers := map[string]string{"Content-Type": "application/json"} - - log.Info(fmt.Sprintf("%+v", event)) - - // e := releaseEvent{} - // err := json.Unmarshal([]byte(event), &e) - // if err != nil { - // log.Error(fmt.Sprintf("%v", err)) - // } - - // token, err := app.AWS.getProviderToken(e) - // if err != nil { - // message := fmt.Sprintf("Unable to release %s version %s, please double check the %s token", e.RepoName, e.ReleaseVersion, e.RepoProvider) - // statusCode := 400 - // return util.GenerateResponseBody(message, statusCode, nil, headers, []string{}), nil - // } - - // var message string - // var statusCode int - // if event.RawPath == "/releases/create/github" { - // log.Info(fmt.Sprintf("handling request on %v", event.RawPath)) - // message, statusCode = app.releasesGithubHandler(e, token) - - // } else if event.RawPath == "/releases/create/gitlab" { - // log.Info(fmt.Sprintf("handling request on %v", event.RawPath)) - // message, statusCode = app.releasesGitlabHandler(e, token) - - // } else { - // log.Error(fmt.Sprintf("path %v does not exist", event.RawPath)) - // return util.GenerateResponseBody(fmt.Sprintf("Path does not exist %v", event.RawPath), 404, nil, headers, []string{}), nil - // } - - // if app.Config.SlackWebhookURL != "" { - // err = util.PostToSlack(app.Config.SlackWebhookURL, fmt.Sprintf( - // "Starting release for %v version %v...\n\n%v", - // e.RepoName, - // e.ReleaseVersion, - // e.ReleaseBody, - // )) - // if err != nil { - // message := fmt.Sprintf("Released %v version %v successfully, unable to send slack notification and update latest version in backend", e.RepoName, e.ReleaseVersion) - // statusCode := 200 - // return util.GenerateResponseBody(message, statusCode, nil, headers, []string{}), nil - // } - // } - - // err = app.AWS.updateCurrentVersion(e) - // if err != nil { - // message := fmt.Sprintf("Released %v version %v successfully, unable to update latest version in backend", e.RepoName, e.ReleaseVersion) - // statusCode := 200 - // return util.GenerateResponseBody(message, statusCode, err, headers, []string{}), nil - // } - - return util.GenerateResponseBody("message", 200, nil, headers, []string{}), nil - // return util.GenerateResponseBody(message, statusCode, err, headers, []string{}), nil -} - -func main() { - log.SetFormatter(&log.JSONFormatter{}) - - config := configuration{ - TableName: os.Getenv("TABLE_NAME"), - UserPoolID: os.Getenv("USER_POOL_ID"), - DB: dynamodb.New(session.Must(session.NewSession())), - } - - app := application{Config: config} - - lambda.Start(app.handler) -} diff --git a/cmd/releases/main.go b/cmd/releases/main.go index de17e72..60b5c49 100644 --- a/cmd/releases/main.go +++ b/cmd/releases/main.go @@ -14,7 +14,7 @@ import ( "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" "github.com/aws/aws-sdk-go/service/ssm" "github.com/aws/aws-sdk-go/service/ssm/ssmiface" - "github.com/seanturner026/serverless-release-dashboard/internal/util" + "github.com/seanturner026/moot/internal/util" log "github.com/sirupsen/logrus" ) @@ -46,16 +46,17 @@ type awsController struct { } type configuration struct { + DashboardName string SlackWebhookURL string } -func (app awsController) getProviderToken(e releaseEvent) (string, error) { +func (app application) getProviderToken(e releaseEvent) (string, error) { input := &ssm.GetParameterInput{ - Name: aws.String(fmt.Sprintf("/dev_release_dashboard/%s_token", e.RepoProvider)), + Name: aws.String(fmt.Sprintf("/%s/%s_token", app.Config.DashboardName, e.RepoProvider)), WithDecryption: aws.Bool(true), } - resp, err := app.SSM.GetParameter(input) + resp, err := app.AWS.SSM.GetParameter(input) if err != nil { if aerr, ok := err.(awserr.Error); ok { log.Error(fmt.Sprintf("%v", aerr.Error())) @@ -111,7 +112,7 @@ func (app application) handler(event events.APIGatewayV2HTTPRequest) (events.API log.Error(fmt.Sprintf("%v", err)) } - token, err := app.AWS.getProviderToken(e) + token, err := app.getProviderToken(e) if err != nil { message := fmt.Sprintf("Unable to release %s version %s, please double check the %s token", e.RepoName, e.ReleaseVersion, e.RepoProvider) statusCode := 400 @@ -167,6 +168,7 @@ func main() { SSM: ssm.New(session.Must(session.NewSession())), }, Config: configuration{ + DashboardName: os.Getenv("DASHBOARD_NAME"), SlackWebhookURL: os.Getenv("SLACK_WEBHOOK_URL"), }, } diff --git a/cmd/repositories/create.go b/cmd/repositories/create.go index 8d46256..a011d11 100644 --- a/cmd/repositories/create.go +++ b/cmd/repositories/create.go @@ -27,13 +27,13 @@ type createRepoEvent struct { GitlabProjectID string `dynamodbav:"GitlabProjectID,omitempty" json:"gitlab_repo_id,omitempty"` } -func (app awsController) getProviderToken(e createRepoEvent) (string, error) { +func (app application) getProviderToken(e createRepoEvent) (string, error) { input := &ssm.GetParameterInput{ - Name: aws.String(fmt.Sprintf("/dev_release_dashboard/%s_token", e.RepoProvider)), + Name: aws.String(fmt.Sprintf("/%s/%s_token", app.Config.DashboardName, e.RepoProvider)), WithDecryption: aws.Bool(true), } - resp, err := app.SSM.GetParameter(input) + resp, err := app.AWS.SSM.GetParameter(input) if err != nil { if aerr, ok := err.(awserr.Error); ok { log.Error(fmt.Sprintf("%v", aerr.Error())) @@ -87,7 +87,7 @@ func (app awsController) writeRepoToDB(e createRepoEvent, itemInput map[string]* } return err } - log.Info(fmt.Sprintf("wrote ID %s successfully", e.RepoName)) + log.Info(fmt.Sprintf("wroterepository %s successfully", e.RepoName)) return nil } @@ -98,9 +98,9 @@ func (app application) repositoriesCreateHandler(event events.APIGatewayV2HTTPRe log.Error(fmt.Sprintf("%v", err)) } e.PK = "repo" - token, err := app.AWS.getProviderToken(e) + token, err := app.getProviderToken(e) if err != nil { - message := fmt.Sprintf("Unable to onboard %s, please double check that the token has read/write access to this repository", e.RepoName) + message := fmt.Sprintf("Unable to onboard %s, please double check that a token has been provided for %s", e.RepoName, e.RepoProvider) statusCode := 400 return message, statusCode } diff --git a/cmd/repositories/main.go b/cmd/repositories/main.go index b629302..224ba20 100644 --- a/cmd/repositories/main.go +++ b/cmd/repositories/main.go @@ -13,15 +13,16 @@ import ( "github.com/aws/aws-sdk-go/service/ssm" "github.com/aws/aws-sdk-go/service/ssm/ssmiface" "github.com/google/go-github/github" - util "github.com/seanturner026/serverless-release-dashboard/internal/util" + util "github.com/seanturner026/moot/internal/util" log "github.com/sirupsen/logrus" "github.com/xanzy/go-gitlab" ) type application struct { - AWS awsController - GH githubController - GL gitlabController + AWS awsController + GH githubController + GL gitlabController + Config configuration } type awsController struct { @@ -41,6 +42,10 @@ type gitlabController struct { Client *gitlab.Client } +type configuration struct { + DashboardName string +} + type repository struct { RepoName string `json:"repo_name,omitempty"` RepoProvider string `dynamodbav:"SK" json:"repo_provider,omitempty"` @@ -85,6 +90,9 @@ func main() { DB: dynamodb.New(session.Must(session.NewSession())), SSM: ssm.New(session.Must(session.NewSession())), }, + Config: configuration{ + DashboardName: os.Getenv("DASHBOARD_NAME"), + }, } lambda.Start(app.handler) diff --git a/cmd/users/main.go b/cmd/users/main.go index a2bf90f..930d17d 100644 --- a/cmd/users/main.go +++ b/cmd/users/main.go @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go/service/cognitoidentityprovider/cognitoidentityprovideriface" "github.com/aws/aws-sdk-go/service/dynamodb" "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" - "github.com/seanturner026/serverless-release-dashboard/internal/util" + "github.com/seanturner026/moot/internal/util" log "github.com/sirupsen/logrus" ) diff --git a/deployments/terraform/.terraform.lock.hcl b/deployments/terraform/.terraform.lock.hcl index aff272c..80c6ea3 100644 --- a/deployments/terraform/.terraform.lock.hcl +++ b/deployments/terraform/.terraform.lock.hcl @@ -2,70 +2,74 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/archive" { - version = "2.0.0" + version = "2.2.0" hashes = [ - "h1:eOUi4EO4QTgPuz+gmD8xy2LTTxNfyc9txyc9PDARth8=", - "zh:1c3300a6686481ed0b3ce456949805b5c55f901b77108543046f12118a102914", - "zh:2d44bcc8a5ea3f2c2ff54ca4c4b273cdc3500ad8bb6929eec6722bb9d10a9714", - "zh:4be17c4e06a74ca30a3c9e88446e453ad16c493b2ddef872a6580dbfe618a1b4", - "zh:50c216a6c672b51d67ece37ce69c121acbcd5c8d24b899e876c61bc09989f69f", - "zh:90d6ad51fecab8d2f086aab9ed45020f1a9d3627dbc95edda11af8e3f87082d9", - "zh:d147427135330b4940a85a0f552e2c1f83a0d1ccdcc82c36a2d311b7f85dbd38", - "zh:dd4b8c0b113e37fd83ceab0f8a203ccfa39caaca5551c70a94fa61e900902932", - "zh:e30ef64a2ed0c2a6e8d1e29b8da8b05b4a303e256478a4b410af81c81cbcbc23", - "zh:e5a81379810d3e1f380b7145d0ecee4a69ab4e80184f77b66cd4411b86aca6a8", - "zh:f9a0a2a72721e7e047e83053173b3a68a7d53e2a22719cc2d9234dbee46c466c", + "h1:2K5LQkuWRS2YN1/YoNaHn9MAzjuTX8Gaqy6i8Mbfv8Y=", + "zh:06bd875932288f235c16e2237142b493c2c2b6aba0e82e8c85068332a8d2a29e", + "zh:0c681b481372afcaefddacc7ccdf1d3bb3a0c0d4678a526bc8b02d0c331479bc", + "zh:100fc5b3fc01ea463533d7bbfb01cb7113947a969a4ec12e27f5b2be49884d6c", + "zh:55c0d7ddddbd0a46d57c51fcfa9b91f14eed081a45101dbfc7fd9d2278aa1403", + "zh:73a5dd68379119167934c48afa1101b09abad2deb436cd5c446733e705869d6b", + "zh:841fc4ac6dc3479981330974d44ad2341deada8a5ff9e3b1b4510702dfbdbed9", + "zh:91be62c9b41edb137f7f835491183628d484e9d6efa82fcb75cfa538c92791c5", + "zh:acd5f442bd88d67eb948b18dc2ed421c6c3faee62d3a12200e442bfff0aa7d8b", + "zh:ad5720da5524641ad718a565694821be5f61f68f1c3c5d2cfa24426b8e774bef", + "zh:e63f12ea938520b3f83634fc29da28d92eed5cfbc5cc8ca08281a6a9c36cca65", + "zh:f6542918faa115df46474a36aabb4c3899650bea036b5f8a5e296be6f8f25767", ] } provider "registry.terraform.io/hashicorp/aws" { - version = "3.28.0" + version = "3.42.0" + constraints = ">= 3.37.0" hashes = [ - "h1:0cCqlVoOAj4YOi61kVpqoxu1bdAmB67z6uZf+lsHJOw=", - "zh:1fee7fce319be5bea7df2e95f28a78a04e15c18bad5eb56dcc0ecc324c97f4b8", - "zh:2383ff31ef7411f7d4bef1ee288f0f79bec41cf220ac94c2b31f6a702b26f984", - "zh:2f450372a8aa7d32f62524159a5930e0251ba34f491d66f00239452a6d575921", - "zh:379d4fdc16a2245b50959f5bfcb24c71fb74b292b6cf9c2d267b6ce94dddd208", - "zh:9fd1078759edd79548ec52c6853668a69f22803c92c0ac202f5c43c1ace63ac0", - "zh:aef544e720ce79f97875cc4ef5dd163922e9f47a496e663d0a272e881d2dd32e", - "zh:e2f28ba5bde0403f3273e80860a80ab5e63420e0142c0e8e283b651b750a8ffe", - "zh:ebc859186fcdd4700cc7091a8ecf4e06cc6d2eceadaeadda0d0e49efc6456325", - "zh:ee7bced0660945206c6226de35ae465b52e406b12e9ff1075186af37962caa6f", - "zh:f33063481894f951ff1e76b94a8311041a4bd3f1f1f01d1a8580d6c893e13c2c", + "h1:C6/yDp6BhuDFx0qdkBuJj/OWUJpAoraHTJaU6ac38Rw=", + "zh:126c856a6eedddd8571f161a826a407ba5655a37a6241393560a96b8c4beca1a", + "zh:1a4868e6ac734b5fc2e79a4a889d176286b66664aad709435aa6acee5871d5b0", + "zh:40fed7637ab8ddeb93bef06aded35d970f0628025b97459ae805463e8aa0a58a", + "zh:68def3c0a5a1aac1db6372c51daef858b707f03052626d3427ac24cba6f2014d", + "zh:6db7ec9c8d1803a0b6f40a664aa892e0f8894562de83061fa7ac1bc51ff5e7e5", + "zh:7058abaad595930b3f97dc04e45c112b2dbf37d098372a849081f7081da2fb52", + "zh:8c25adb15a19da301c478aa1f4a4d8647cabdf8e5dae8331d4490f80ea718c26", + "zh:8e129b847401e39fcbc54817726dab877f36b7f00ff5ed76f7b43470abe99ff9", + "zh:d268bb267a2d6b39df7ddee8efa7c1ef7a15cf335dfa5f2e64c9dae9b623a1b8", + "zh:d6eeb3614a0ab50f8e9ab5666ae5754ea668ce327310e5b21b7f04a18d7611a8", + "zh:f5d3c58055dff6e38562b75d3edc908cb2f1e45c6914f6b00f4773359ce49324", ] } -provider "registry.terraform.io/hashicorp/null" { - version = "3.0.0" +provider "registry.terraform.io/hashicorp/external" { + version = "2.1.0" hashes = [ - "h1:V1tzrSG6t3e7zWvUwRbGbhsWU2Jd/anrJpOl9XM+R/8=", - "zh:05fb7eab469324c97e9b73a61d2ece6f91de4e9b493e573bfeda0f2077bc3a4c", - "zh:1688aa91885a395c4ae67636d411475d0b831e422e005dcf02eedacaafac3bb4", - "zh:24a0b1292e3a474f57c483a7a4512d797e041bc9c2fbaac42fe12e86a7fb5a3c", - "zh:2fc951bd0d1b9b23427acc93be09b6909d72871e464088171da60fbee4fdde03", - "zh:6db825759425599a326385a68acc6be2d9ba0d7d6ef587191d0cdc6daef9ac63", - "zh:85985763d02618993c32c294072cc6ec51f1692b803cb506fcfedca9d40eaec9", - "zh:a53186599c57058be1509f904da512342cfdc5d808efdaf02dec15f0f3cb039a", - "zh:c2e07b49b6efa676bdc7b00c06333ea1792a983a5720f9e2233db27323d2707c", - "zh:cdc8fe1096103cf5374751e2e8408ec4abd2eb67d5a1c5151fe2c7ecfd525bef", - "zh:dbdef21df0c012b0d08776f3d4f34eb0f2f229adfde07ff252a119e52c0f65b7", + "h1:LTl5CGW8wiIEe16AC4MtXN/95xWWNDbap70zJsBTk0w=", + "zh:0d83ffb72fbd08986378204a7373d8c43b127049096eaf2765bfdd6b00ad9853", + "zh:7577d6edc67b1e8c2cf62fe6501192df1231d74125d90e51d570d586d95269c5", + "zh:9c669ded5d5affa4b2544952c4b6588dfed55260147d24ced02dca3a2829f328", + "zh:a404d46f2831f90633947ab5d57e19dbfe35b3704104ba6ec80bcf50b058acfd", + "zh:ae1caea1c936d459ceadf287bb5c5bd67b5e2a7819df6f5c4114b7305df7f822", + "zh:afb4f805477694a4b9dde86b268d2c0821711c8aab1c6088f5f992228c4c06fb", + "zh:b993b4a1de8a462643e78f4786789e44ce5064b332fee1cb0d6250ed085561b8", + "zh:c84b2c13fa3ea2c0aa7291243006d560ce480a5591294b9001ce3742fc9c5791", + "zh:c8966f69b7eccccb771704fd5335923692eccc9e0e90cb95d14538fe2e92a3b8", + "zh:d5fe68850d449b811e633a300b114d0617df6d450305e8251643b4d143dc855b", + "zh:ddebfd1e674ba336df09b1f27bbaa0e036c25b7a7087dc8081443f6e5954028b", ] } -provider "registry.terraform.io/hashicorp/random" { +provider "registry.terraform.io/hashicorp/null" { version = "3.1.0" hashes = [ - "h1:rKYu5ZUbXwrLG1w81k7H3nce/Ys6yAxXhWcbtk36HjY=", - "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", - "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", - "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", - "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", - "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", - "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", - "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", - "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", - "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", - "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", - "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", + "h1:xhbHC6in3nQryvTQBWKxebi3inG5OCgHgc4fRxL0ymc=", + "zh:02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2", + "zh:53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515", + "zh:5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521", + "zh:9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2", + "zh:a6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e", + "zh:a8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53", + "zh:c797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d", + "zh:cecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8", + "zh:e1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70", + "zh:fc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b", + "zh:fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e", ] } diff --git a/deployments/terraform/assets/cognito.go b/deployments/terraform/assets/cognito.go new file mode 100644 index 0000000..62e2373 --- /dev/null +++ b/deployments/terraform/assets/cognito.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/cognitoidentityprovider" + "github.com/urfave/cli/v2" +) + +func getAdminUserID(adminUserEmail, userPoolID string) string { + input := &cognitoidentityprovider.ListUsersInput{ + AttributesToGet: []*string{aws.String("email")}, + Filter: aws.String(fmt.Sprintf("email = \"%s\"", adminUserEmail)), + UserPoolId: aws.String(userPoolID), + } + + client := cognitoidentityprovider.New(session.Must(session.NewSession(&aws.Config{Region: aws.String("us-east-1")}))) + resp, err := client.ListUsers(input) + if err != nil { + if aerr, ok := err.(awserr.Error); ok { + fmt.Printf("%v\n", aerr.Error()) + } else { + fmt.Printf("%v\n", err.Error()) + } + } + return *resp.Users[0].Username +} + +func printUserID(adminUserID string) { + + type externalDataResponse struct { + UserID string `json:"user_id"` + } + + response := &externalDataResponse{} + response.UserID = adminUserID + responseJSON, err := json.Marshal(response) + if err != nil { + fmt.Println(err) + } + _, err = os.Stdout.Write(responseJSON) + if err != nil { + fmt.Println(err) + } +} + +func main() { + app := &cli.App{ + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "a", + Aliases: []string{"admin-user-email"}, + Usage: "Email address of the Dashboard Admin.", + Required: true, + }, + &cli.StringFlag{ + Name: "u", + Aliases: []string{"user-pool-id"}, + Usage: "ID of the User Pool.", + Required: true, + }, + }, + Action: func(c *cli.Context) error { + adminUserEmail := c.String("a") + userPoolID := c.String("u") + adminUserID := getAdminUserID(adminUserEmail, userPoolID) + printUserID(adminUserID) + return nil + }, + } + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} diff --git a/deployments/terraform/assets/cognito_invite_template.html b/deployments/terraform/assets/cognito_invite_template.html index dc5c876..24e1bc3 100644 --- a/deployments/terraform/assets/cognito_invite_template.html +++ b/deployments/terraform/assets/cognito_invite_template.html @@ -1,7 +1,7 @@ -

You have been given access to the Serverless Release Dashboard.

+

You have been given access to Moot, a Serverless Release Dashboard.

Please see below for temporary credentials:

username: {username}

password: {####}

diff --git a/deployments/terraform/assets/dynamodb_put_item_input.json b/deployments/terraform/assets/dynamodb_put_item_input.json index 3fc5a8b..08175e6 100644 --- a/deployments/terraform/assets/dynamodb_put_item_input.json +++ b/deployments/terraform/assets/dynamodb_put_item_input.json @@ -6,6 +6,6 @@ "S": "${admin_user_email}" }, "ID": { - "S": "${uud}" + "S": "${uuid}" } } diff --git a/deployments/terraform/data.tf b/deployments/terraform/data.tf index d1e8d05..a4d6de4 100644 --- a/deployments/terraform/data.tf +++ b/deployments/terraform/data.tf @@ -1,20 +1,30 @@ data "aws_caller_identity" "current" {} data "aws_region" "current" {} -data "null_data_source" "wait_for_lambda_build" { - for_each = local.lambdas +data "aws_route53_zone" "this" { + count = var.hosted_zone_name != "" ? 1 : 0 - inputs = { - lambda_build_id = null_resource.lambda_build[each.key].id - source = "${local.path}/bin/${each.key}" - } + name = var.hosted_zone_name + private_zone = false +} + +data "external" "admin_user_id" { + count = var.admin_user_email != "" && !var.enable_delete_admin_user ? 1 : 0 + depends_on = [null_resource.create_admin_user[0]] + + program = [ + "go", "run", "${path.module}/assets/cognito.go", + "--admin-user-email", var.admin_user_email, + "--user-pool-id", aws_cognito_user_pool.this.id, + ] } data "archive_file" "this" { - for_each = local.lambdas + for_each = local.lambdas + depends_on = [null_resource.lambda_build] type = "zip" - source_file = data.null_data_source.wait_for_lambda_build[each.key].outputs["source"] + source_file = "${local.path}/bin/${each.key}" output_path = "${local.path}/archive/${each.key}.zip" } @@ -46,11 +56,11 @@ data "aws_iam_policy_document" "policy" { data "aws_iam_policy_document" "s3" { statement { actions = ["s3:GetObject"] - resources = ["arn:aws:s3:::release-dashboard-${data.aws_caller_identity.current.account_id}/*"] + resources = ["arn:aws:s3:::${replace(var.name, "_", "-")}-${data.aws_caller_identity.current.account_id}/*"] principals { type = "AWS" - identifiers = module.cloudfront.this_cloudfront_origin_access_identity_iam_arns + identifiers = module.cloudfront.cloudfront_origin_access_identity_iam_arns } } } diff --git a/deployments/terraform/examples/complete/.terraform.lock.hcl b/deployments/terraform/examples/complete/.terraform.lock.hcl new file mode 100644 index 0000000..377156c --- /dev/null +++ b/deployments/terraform/examples/complete/.terraform.lock.hcl @@ -0,0 +1,74 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/archive" { + version = "2.2.0" + hashes = [ + "h1:2K5LQkuWRS2YN1/YoNaHn9MAzjuTX8Gaqy6i8Mbfv8Y=", + "zh:06bd875932288f235c16e2237142b493c2c2b6aba0e82e8c85068332a8d2a29e", + "zh:0c681b481372afcaefddacc7ccdf1d3bb3a0c0d4678a526bc8b02d0c331479bc", + "zh:100fc5b3fc01ea463533d7bbfb01cb7113947a969a4ec12e27f5b2be49884d6c", + "zh:55c0d7ddddbd0a46d57c51fcfa9b91f14eed081a45101dbfc7fd9d2278aa1403", + "zh:73a5dd68379119167934c48afa1101b09abad2deb436cd5c446733e705869d6b", + "zh:841fc4ac6dc3479981330974d44ad2341deada8a5ff9e3b1b4510702dfbdbed9", + "zh:91be62c9b41edb137f7f835491183628d484e9d6efa82fcb75cfa538c92791c5", + "zh:acd5f442bd88d67eb948b18dc2ed421c6c3faee62d3a12200e442bfff0aa7d8b", + "zh:ad5720da5524641ad718a565694821be5f61f68f1c3c5d2cfa24426b8e774bef", + "zh:e63f12ea938520b3f83634fc29da28d92eed5cfbc5cc8ca08281a6a9c36cca65", + "zh:f6542918faa115df46474a36aabb4c3899650bea036b5f8a5e296be6f8f25767", + ] +} + +provider "registry.terraform.io/hashicorp/aws" { + version = "3.42.0" + hashes = [ + "h1:C6/yDp6BhuDFx0qdkBuJj/OWUJpAoraHTJaU6ac38Rw=", + "zh:126c856a6eedddd8571f161a826a407ba5655a37a6241393560a96b8c4beca1a", + "zh:1a4868e6ac734b5fc2e79a4a889d176286b66664aad709435aa6acee5871d5b0", + "zh:40fed7637ab8ddeb93bef06aded35d970f0628025b97459ae805463e8aa0a58a", + "zh:68def3c0a5a1aac1db6372c51daef858b707f03052626d3427ac24cba6f2014d", + "zh:6db7ec9c8d1803a0b6f40a664aa892e0f8894562de83061fa7ac1bc51ff5e7e5", + "zh:7058abaad595930b3f97dc04e45c112b2dbf37d098372a849081f7081da2fb52", + "zh:8c25adb15a19da301c478aa1f4a4d8647cabdf8e5dae8331d4490f80ea718c26", + "zh:8e129b847401e39fcbc54817726dab877f36b7f00ff5ed76f7b43470abe99ff9", + "zh:d268bb267a2d6b39df7ddee8efa7c1ef7a15cf335dfa5f2e64c9dae9b623a1b8", + "zh:d6eeb3614a0ab50f8e9ab5666ae5754ea668ce327310e5b21b7f04a18d7611a8", + "zh:f5d3c58055dff6e38562b75d3edc908cb2f1e45c6914f6b00f4773359ce49324", + ] +} + +provider "registry.terraform.io/hashicorp/external" { + version = "2.1.0" + hashes = [ + "h1:LTl5CGW8wiIEe16AC4MtXN/95xWWNDbap70zJsBTk0w=", + "zh:0d83ffb72fbd08986378204a7373d8c43b127049096eaf2765bfdd6b00ad9853", + "zh:7577d6edc67b1e8c2cf62fe6501192df1231d74125d90e51d570d586d95269c5", + "zh:9c669ded5d5affa4b2544952c4b6588dfed55260147d24ced02dca3a2829f328", + "zh:a404d46f2831f90633947ab5d57e19dbfe35b3704104ba6ec80bcf50b058acfd", + "zh:ae1caea1c936d459ceadf287bb5c5bd67b5e2a7819df6f5c4114b7305df7f822", + "zh:afb4f805477694a4b9dde86b268d2c0821711c8aab1c6088f5f992228c4c06fb", + "zh:b993b4a1de8a462643e78f4786789e44ce5064b332fee1cb0d6250ed085561b8", + "zh:c84b2c13fa3ea2c0aa7291243006d560ce480a5591294b9001ce3742fc9c5791", + "zh:c8966f69b7eccccb771704fd5335923692eccc9e0e90cb95d14538fe2e92a3b8", + "zh:d5fe68850d449b811e633a300b114d0617df6d450305e8251643b4d143dc855b", + "zh:ddebfd1e674ba336df09b1f27bbaa0e036c25b7a7087dc8081443f6e5954028b", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.0" + hashes = [ + "h1:xhbHC6in3nQryvTQBWKxebi3inG5OCgHgc4fRxL0ymc=", + "zh:02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2", + "zh:53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515", + "zh:5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521", + "zh:9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2", + "zh:a6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e", + "zh:a8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53", + "zh:c797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d", + "zh:cecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8", + "zh:e1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70", + "zh:fc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b", + "zh:fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e", + ] +} diff --git a/deployments/terraform/examples/complete/main.tf b/deployments/terraform/examples/complete/main.tf new file mode 100644 index 0000000..a1bbe6d --- /dev/null +++ b/deployments/terraform/examples/complete/main.tf @@ -0,0 +1,14 @@ +module "dashboard" { + source = "../../" + + name = "moot" + admin_user_email = var.admin_user_email + enable_delete_admin_user = false + github_token = var.github_token + gitlab_token = var.gitlab_token + slack_webhook_url = var.slack_webhook_url + fqdn_alias = "moot.link" + hosted_zone_name = "moot.link" + enable_api_gateway_access_logs = true + tags = var.tags +} diff --git a/deployments/terraform/examples/complete/provider.tf b/deployments/terraform/examples/complete/provider.tf new file mode 100644 index 0000000..c125940 --- /dev/null +++ b/deployments/terraform/examples/complete/provider.tf @@ -0,0 +1,3 @@ +provider "aws" { + region = "us-east-1" +} diff --git a/deployments/terraform/examples/complete/terraform.tfvars b/deployments/terraform/examples/complete/terraform.tfvars new file mode 100644 index 0000000..43a5b93 --- /dev/null +++ b/deployments/terraform/examples/complete/terraform.tfvars @@ -0,0 +1,4 @@ +tags = { + name = "moot" + managed_by = "terraform" +} diff --git a/deployments/terraform/examples/complete/variables.tf b/deployments/terraform/examples/complete/variables.tf new file mode 100644 index 0000000..ecc8acc --- /dev/null +++ b/deployments/terraform/examples/complete/variables.tf @@ -0,0 +1,40 @@ +variable "tags" { + type = map(string) + description = "Map of tags to be applied to resources." +} + +variable "admin_user_email" { + type = string + description = <<-DESC + Controls the creation of an admin user that is required to initially gain access to the + dashboard. + + If access to the dashboard is completely lost, do the following + • `var.enable_delete_admin_user = true` + • `terraform apply` + • `var.enable_delete_admin_user = false` + • `terraform apply` + + If the initial admin user should no longer be able to access the dashboard, revoke access by + setting `var.enable_delete_admin_user = true` and running `terraform apply` + DESC + default = "" +} + +variable "github_token" { + type = string + description = "Token for Github." + default = "" +} + +variable "gitlab_token" { + type = string + description = "Token for Gitlab." + default = "" +} + +variable "slack_webhook_url" { + type = string + description = "URL to send slack message payloads to." + default = "" +} diff --git a/deployments/terraform/locals.tf b/deployments/terraform/locals.tf index b2546f4..0b0997e 100644 --- a/deployments/terraform/locals.tf +++ b/deployments/terraform/locals.tf @@ -1,5 +1,5 @@ locals { - path = "${path.root}/../.." + path = "${path.module}/../.." ssm_parameters = { client_pool_secret = { @@ -20,6 +20,13 @@ locals { } } + null = { + lambda_binary_exists = { for key, _ in local.lambdas : key => fileexists("${local.path}/bin/${key}") } + } + + frontend_module_comprehension = [for module in jsondecode(file("${path.root}/.terraform/modules/modules.json"))["Modules"] : module if length(regexall("vuejs_frontend", module.Key)) > 0][0] + frontend_module_path = "${path.root}/${local.frontend_module_comprehension.Dir}" + lambdas = { auth = { description = "Administrates user login, token refreshes, and password resets." @@ -50,6 +57,7 @@ locals { description = "Creates github and gitlab releases for repository specified in the event." authorizer = true environment = { + DASHBOARD_NAME = var.name SLACK_WEBHOOK_URL = aws_ssm_parameter.this["slack_webhook_url"].value TABLE_NAME = aws_dynamodb_table.this.id } @@ -73,7 +81,8 @@ locals { description = "Writes github and gitlab repository details to DynamoDB." authorizer = true environment = { - TABLE_NAME = aws_dynamodb_table.this.id + DASHBOARD_NAME = var.name + TABLE_NAME = aws_dynamodb_table.this.id } routes = { "/repositories/create" = "POST" diff --git a/deployments/terraform/main.tf b/deployments/terraform/main.tf deleted file mode 100644 index 7abd241..0000000 --- a/deployments/terraform/main.tf +++ /dev/null @@ -1,5 +0,0 @@ -provider "aws" { - region = "us-east-1" - allowed_account_ids = ["744000309083"] - profile = "sean" -} diff --git a/deployments/terraform/modules.tf b/deployments/terraform/modules.tf index 2a4879d..06730a6 100644 --- a/deployments/terraform/modules.tf +++ b/deployments/terraform/modules.tf @@ -1,25 +1,26 @@ +module "vuejs_frontend" { + source = "github.com/seanturner026/moot-frontend.git?ref=v0.3.0" +} + module "cloudfront" { - source = "terraform-aws-modules/cloudfront/aws" - version = "v1.8.0" - - # aliases = [""] - comment = "Serverless Release Dashboard" - enabled = true - is_ipv6_enabled = true - price_class = "PriceClass_All" - retain_on_delete = false - wait_for_deployment = false - default_root_object = "/index.html" + source = "terraform-aws-modules/cloudfront/aws" + version = "v2.4.0" + depends_on = [aws_acm_certificate_validation.this[0]] + aliases = var.fqdn_alias != "" ? [var.fqdn_alias] : null + comment = "Moot, a Serverless Release Dashboard" + enabled = true + is_ipv6_enabled = true + price_class = "PriceClass_All" + retain_on_delete = false + wait_for_deployment = true + default_root_object = "/index.html" create_origin_access_identity = true + origin_access_identities = { - s3 = "Cloudfront access to Serverless Release Dashboard bucket" + s3 = "Cloudfront access to moot S3 bucket" } - # logging_config = { - # bucket = "logs-my-cdn.s3.amazonaws.com" - # } - origin = { s3 = { domain_name = aws_s3_bucket.this.bucket_domain_name @@ -40,8 +41,10 @@ module "cloudfront" { } viewer_certificate = { - cloudfront_default_certificate = true, - minimum_protocol_versione = "TLSv1" + cloudfront_default_certificate = var.hosted_zone_name != "" && var.fqdn_alias != "" ? false : true + minimum_protocol_versione = var.hosted_zone_name != "" && var.fqdn_alias != "" ? "TLSv1.2" : "TLSv1" + acm_certificate_arn = var.hosted_zone_name != "" && var.fqdn_alias != "" ? aws_acm_certificate.this[0].arn : null + ssl_support_method = var.hosted_zone_name != "" && var.fqdn_alias != "" ? "sni-only" : null } custom_error_response = { diff --git a/deployments/terraform/outputs.tf b/deployments/terraform/outputs.tf new file mode 100644 index 0000000..4dc6fb3 --- /dev/null +++ b/deployments/terraform/outputs.tf @@ -0,0 +1,4 @@ +output "cloudfront_domain_name" { + description = "FQDN of Cloudfront Distribution that can be used for DNS." + value = module.cloudfront.cloudfront_distribution_domain_name +} diff --git a/deployments/terraform/r_acm.tf b/deployments/terraform/r_acm.tf new file mode 100644 index 0000000..3b9040b --- /dev/null +++ b/deployments/terraform/r_acm.tf @@ -0,0 +1,18 @@ +resource "aws_acm_certificate" "this" { + count = var.hosted_zone_name != "" && var.fqdn_alias != "" ? 1 : 0 + + domain_name = var.fqdn_alias + validation_method = "DNS" + tags = merge(var.tags, { Name = var.name }) + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_acm_certificate_validation" "this" { + count = var.hosted_zone_name != "" && var.fqdn_alias != "" ? 1 : 0 + + certificate_arn = aws_acm_certificate.this[0].arn + validation_record_fqdns = [for record in aws_route53_record.acm : record.fqdn] +} diff --git a/deployments/terraform/r_api_gateway.tf b/deployments/terraform/r_api_gateway.tf index ca32208..48b1eef 100644 --- a/deployments/terraform/r_api_gateway.tf +++ b/deployments/terraform/r_api_gateway.tf @@ -1,13 +1,13 @@ resource "aws_apigatewayv2_api" "this" { - name = var.tags.name + name = var.name protocol_type = "HTTP" - description = "HTTP API for serverless release dashboard" + description = "HTTP API for moot, a serverless release dashboard" cors_configuration { allow_credentials = true allow_headers = ["Content-Type", "Authorization", "X-Session-Id"] allow_methods = ["GET", "OPTIONS", "POST"] - allow_origins = ["http://localhost:8080", var.dev_cloudfront_dns] + allow_origins = ["https://${var.fqdn_alias}"] max_age = 600 } @@ -19,26 +19,45 @@ resource "aws_apigatewayv2_stage" "this" { api_id = aws_apigatewayv2_api.this.id auto_deploy = true - access_log_settings { - destination_arn = aws_cloudwatch_log_group.this["api_gateway"].arn - format = jsonencode({ - "requestId" : "$context.requestId", - "ip" : "$context.identity.sourceIp", - "requestTime" : "$context.requestTime", - "httpMethod" : "$context.httpMethod", - "routeKey" : "$context.routeKey", - "status" : "$context.status", - "protocol" : "$context.protocol", - "responseLength" : "$context.responseLength", - "integrationError " : "$context.integrationErrorMessage" - }) + dynamic "access_log_settings" { + for_each = var.enable_api_gateway_access_logs ? [var.enable_api_gateway_access_logs] : [] + + content { + destination_arn = aws_cloudwatch_log_group.api_gateway[0].arn + format = jsonencode({ + "requestId" : "$context.requestId", + "ip" : "$context.identity.sourceIp", + "requestTime" : "$context.requestTime", + "httpMethod" : "$context.httpMethod", + "routeKey" : "$context.routeKey", + "status" : "$context.status", + "protocol" : "$context.protocol", + "responseLength" : "$context.responseLength", + "integrationError " : "$context.integrationErrorMessage" + }) + } + } + + tags = var.tags +} + +resource "aws_apigatewayv2_domain_name" "this" { + count = var.hosted_zone_name != "" && var.fqdn_alias != "" ? 1 : 0 + depends_on = [aws_acm_certificate_validation.this[0]] + + domain_name = var.fqdn_alias + + domain_name_configuration { + certificate_arn = aws_acm_certificate.this[0].arn + endpoint_type = "REGIONAL" + security_policy = "TLS_1_2" } tags = var.tags } resource "aws_apigatewayv2_authorizer" "this" { - name = var.tags.name + name = var.name api_id = aws_apigatewayv2_api.this.id authorizer_type = "JWT" identity_sources = ["$request.header.Authorization"] @@ -59,10 +78,6 @@ resource "aws_apigatewayv2_integration" "this" { integration_uri = aws_lambda_function.this[each.value.lambda_key].arn timeout_milliseconds = 10500 payload_format_version = "2.0" - - # tls_config { - # server_name_to_verify = "" - # } } resource "aws_apigatewayv2_route" "this" { diff --git a/deployments/terraform/r_cloudwatch.tf b/deployments/terraform/r_cloudwatch.tf index 7f5d0a8..4fa5f58 100644 --- a/deployments/terraform/r_cloudwatch.tf +++ b/deployments/terraform/r_cloudwatch.tf @@ -1,7 +1,15 @@ resource "aws_cloudwatch_log_group" "this" { - for_each = merge(local.lambdas, { "api_gateway" = {} }) + for_each = local.lambdas - name = "/aws/lambda/${each.key}" + name = "/aws/lambda/${var.name}_${each.key}" + retention_in_days = 7 + tags = var.tags +} + +resource "aws_cloudwatch_log_group" "api_gateway" { + count = var.enable_api_gateway_access_logs ? 1 : 0 + + name = "/aws/${var.name}/api_gateway/access" retention_in_days = 7 tags = var.tags } diff --git a/deployments/terraform/r_cognito.tf b/deployments/terraform/r_cognito.tf index 5dac78c..3d2bdcf 100644 --- a/deployments/terraform/r_cognito.tf +++ b/deployments/terraform/r_cognito.tf @@ -1,13 +1,12 @@ resource "aws_cognito_user_pool" "this" { - name = var.tags.name + name = var.name username_attributes = ["email"] auto_verified_attributes = ["email"] admin_create_user_config { - invite_message_template { - email_subject = "Serverless Release Dashboard User Signup" - email_message = file("${path.root}/assets/cognito_invite_template.html") + email_subject = "Moot User Signup" + email_message = file("${path.module}/assets/cognito_invite_template.html") sms_message = <<-MESSAGE username: {username} password: {####} @@ -35,12 +34,15 @@ resource "aws_cognito_user_pool" "this" { } resource "aws_cognito_user_pool_client" "this" { - name = var.tags.name + name = var.name user_pool_id = aws_cognito_user_pool.this.id generate_secret = true allowed_oauth_flows = ["code", "implicit"] allowed_oauth_flows_user_pool_client = true allowed_oauth_scopes = ["email", "openid"] + supported_identity_providers = ["COGNITO"] + callback_urls = ["https://${var.fqdn_alias}"] + explicit_auth_flows = [ "ALLOW_ADMIN_USER_PASSWORD_AUTH", "ALLOW_CUSTOM_AUTH", @@ -48,12 +50,10 @@ resource "aws_cognito_user_pool_client" "this" { "ALLOW_USER_PASSWORD_AUTH", "ALLOW_USER_SRP_AUTH", ] - supported_identity_providers = ["COGNITO"] - callback_urls = ["https://localhost:3000"] } resource "aws_cognito_identity_pool" "this" { - identity_pool_name = var.tags.name + identity_pool_name = var.name allow_unauthenticated_identities = false cognito_identity_providers { @@ -65,10 +65,17 @@ resource "aws_cognito_identity_pool" "this" { } resource "null_resource" "create_admin_user" { - count = var.enable_admin_user_creation ? 1 : 0 - depends_on = [aws_lambda_function.this] + count = var.admin_user_email != "" && !var.enable_delete_admin_user ? 1 : 0 provisioner "local-exec" { command = "aws --region ${data.aws_region.current.name} cognito-idp admin-create-user --user-pool-id ${aws_cognito_user_pool.this.id} --username ${var.admin_user_email} --user-attributes Name=email,Value=${var.admin_user_email}" } } + +resource "null_resource" "delete_admin_user" { + count = var.admin_user_email != "" && var.enable_delete_admin_user ? 1 : 0 + + provisioner "local-exec" { + command = "aws --region ${data.aws_region.current.name} cognito-idp admin-delete-user --user-pool-id ${aws_cognito_user_pool.this.id} --username ${var.admin_user_email}" + } +} diff --git a/deployments/terraform/r_dynamodb.tf b/deployments/terraform/r_dynamodb.tf index e26d171..5c5efda 100644 --- a/deployments/terraform/r_dynamodb.tf +++ b/deployments/terraform/r_dynamodb.tf @@ -1,5 +1,5 @@ resource "aws_dynamodb_table" "this" { - name = var.tags.name + name = var.name billing_mode = "PAY_PER_REQUEST" hash_key = "PK" range_key = "SK" @@ -17,12 +17,8 @@ resource "aws_dynamodb_table" "this" { tags = var.tags } -resource "random_uuid" "this" { - count = var.enable_admin_user_creation ? 1 : 0 -} - resource "aws_dynamodb_table_item" "this" { - count = var.enable_admin_user_creation ? 1 : 0 + count = var.admin_user_email != "" && !var.enable_delete_admin_user ? 1 : 0 depends_on = [null_resource.create_admin_user] table_name = aws_dynamodb_table.this.name @@ -30,10 +26,10 @@ resource "aws_dynamodb_table_item" "this" { range_key = aws_dynamodb_table.this.range_key item = templatefile( - "${path.root}/assets/dynamodb_put_item_input.json", + "${path.module}/assets/dynamodb_put_item_input.json", { admin_user_email = var.admin_user_email - uud = random_uuid.this[0].id + uuid = data.external.admin_user_id[0].result.user_id } ) } diff --git a/deployments/terraform/r_iam.tf b/deployments/terraform/r_iam.tf index 5b23385..9870f75 100644 --- a/deployments/terraform/r_iam.tf +++ b/deployments/terraform/r_iam.tf @@ -1,7 +1,7 @@ resource "aws_iam_role" "this" { for_each = local.lambdas - name = "${var.tags.name}_${each.key}" + name = "${var.name}_${each.key}" assume_role_policy = data.aws_iam_policy_document.role.json tags = var.tags } @@ -9,7 +9,7 @@ resource "aws_iam_role" "this" { resource "aws_iam_role_policy" "this" { for_each = local.lambdas - name = "${var.tags.name}_${each.key}" + name = "${var.name}_${each.key}" role = aws_iam_role.this[each.key].name policy = data.aws_iam_policy_document.policy[each.key].json } diff --git a/deployments/terraform/r_lambda.tf b/deployments/terraform/r_lambda.tf index 92930f6..bc1057b 100644 --- a/deployments/terraform/r_lambda.tf +++ b/deployments/terraform/r_lambda.tf @@ -2,7 +2,7 @@ resource "null_resource" "lambda_build" { for_each = local.lambdas triggers = { - binary_exists = fileexists("${local.path}/bin/${each.key}") + binary_exists = local.null.lambda_binary_exists[each.key] main = join("", [ for file in fileset("${local.path}/cmd/${each.key}", "*.go") : filebase64("${local.path}/cmd/${each.key}/${file}") @@ -45,7 +45,7 @@ resource "aws_lambda_function" "this" { for_each = local.lambdas filename = "${local.path}/archive/${each.key}.zip" - function_name = each.key + function_name = "${var.name}_${each.key}" description = each.value.description role = aws_iam_role.this[each.key].arn handler = each.key diff --git a/deployments/terraform/r_null.tf b/deployments/terraform/r_null.tf new file mode 100644 index 0000000..5e2fd28 --- /dev/null +++ b/deployments/terraform/r_null.tf @@ -0,0 +1,22 @@ +resource "null_resource" "build_frontend" { + + triggers = { + deploy_on_version_changes = split("?ref=", local.frontend_module_comprehension.Source)[1] + } + + provisioner "local-exec" { + command = "cd ${local.frontend_module_path} && yarn install" + } + + provisioner "local-exec" { + command = "cd ${local.frontend_module_path} && echo \"VUE_APP_API_GATEWAY_ENDPOINT=${aws_apigatewayv2_api.this.api_endpoint}\" > .env" + } + + provisioner "local-exec" { + command = "cd ${local.frontend_module_path} && yarn build" + } + + provisioner "local-exec" { + command = "cd ${local.frontend_module_path} && aws s3 sync --cache-control 'max-age=604800' dist/ s3://${aws_s3_bucket.this.id}" + } +} diff --git a/deployments/terraform/r_route53.tf b/deployments/terraform/r_route53.tf new file mode 100644 index 0000000..c18db7d --- /dev/null +++ b/deployments/terraform/r_route53.tf @@ -0,0 +1,30 @@ +resource "aws_route53_record" "alias" { + count = var.hosted_zone_name != "" && var.fqdn_alias != "" ? 1 : 0 + + zone_id = data.aws_route53_zone.this[0].zone_id + name = var.fqdn_alias + type = "A" + + alias { + name = module.cloudfront.cloudfront_distribution_domain_name + zone_id = module.cloudfront.cloudfront_distribution_hosted_zone_id + evaluate_target_health = false + } +} + +resource "aws_route53_record" "acm" { + for_each = { + for dvo in aws_acm_certificate.this[0].domain_validation_options : dvo.domain_name => { + name = dvo.resource_record_name + record = dvo.resource_record_value + type = dvo.resource_record_type + } if var.hosted_zone_name != "" && var.fqdn_alias != "" + } + + allow_overwrite = true + name = each.value.name + records = [each.value.record] + ttl = 60 + type = each.value.type + zone_id = data.aws_route53_zone.this[0].zone_id +} diff --git a/deployments/terraform/r_s3.tf b/deployments/terraform/r_s3.tf index ea91627..4469313 100644 --- a/deployments/terraform/r_s3.tf +++ b/deployments/terraform/r_s3.tf @@ -1,12 +1,13 @@ resource "aws_s3_bucket" "this" { - bucket = "release-dashboard-${data.aws_caller_identity.current.account_id}" - acl = "public-read" - policy = data.aws_iam_policy_document.s3.json + bucket = "${replace(var.name, "_", "-")}-${data.aws_caller_identity.current.account_id}" + acl = "public-read" + policy = data.aws_iam_policy_document.s3.json + force_destroy = true cors_rule { allowed_headers = ["*"] allowed_methods = ["GET", "POST"] - allowed_origins = [var.dev_cloudfront_dns] + allowed_origins = [var.fqdn_alias] expose_headers = ["ETag"] max_age_seconds = 3000 } diff --git a/deployments/terraform/r_ssm.tf b/deployments/terraform/r_ssm.tf index 96d3eee..22fc043 100644 --- a/deployments/terraform/r_ssm.tf +++ b/deployments/terraform/r_ssm.tf @@ -1,7 +1,7 @@ resource "aws_ssm_parameter" "this" { for_each = local.ssm_parameters - name = "/${var.tags.name}/${each.key}" + name = "/${var.name}/${each.key}" description = each.value.description type = "SecureString" value = each.value.parameter_value diff --git a/deployments/terraform/terraform.tfvars b/deployments/terraform/terraform.tfvars deleted file mode 100644 index 5011895..0000000 --- a/deployments/terraform/terraform.tfvars +++ /dev/null @@ -1,6 +0,0 @@ -tags = { - name = "dev_release_dashboard" - managed_by = "terraform" -} - -enable_admin_user_creation = true diff --git a/deployments/terraform/variables.tf b/deployments/terraform/variables.tf index f7bd871..1551424 100644 --- a/deployments/terraform/variables.tf +++ b/deployments/terraform/variables.tf @@ -1,29 +1,41 @@ +variable "name" { + type = string + description = "Name to be applied to all resources." + default = "release_dashboard" +} + variable "tags" { type = map(string) description = "Map of tags to be applied to resources." + default = {} } variable "admin_user_email" { type = string - description = "Email address for dashboard admin" -} - -variable "enable_admin_user_creation" { - type = bool description = <<-DESC Controls the creation of an admin user that is required to initially gain access to the dashboard. If access to the dashboard is completely lost, do the following - • `var.enable_admin_user_creation = false` - • terraform apply - • `var.enable_admin_user_creation = true` - • terraform apply + • `var.enable_delete_admin_user = true` + • `terraform apply` + • `var.enable_delete_admin_user = false` + • `terraform apply` If the initial admin user should no longer be able to access the dashboard, revoke access by - setting `var.enable_admin_user_creation = false` and running `terraform apply` + setting `var.enable_delete_admin_user = true` and running `terraform apply` + DESC + default = "" +} + +variable "enable_delete_admin_user" { + type = bool + description = <<-DESC + Destroys the admin user. + + Set this value to true to destroy the user, and to false to recreate the user. DESC - default = true + default = false } variable "github_token" { @@ -44,6 +56,23 @@ variable "slack_webhook_url" { default = "" } -variable "dev_cloudfront_dns" { - type = string +variable "hosted_zone_name" { + type = string + description = "Name of AWS Route53 Hosted Zone for DNS." + default = "" +} + +variable "fqdn_alias" { + type = string + description = <<-DESC + ALIAS for the Cloudfront distribution, S3, Cognito and API Gateway. Must be in the form of + `example.com`. + DESC + default = "" +} + +variable "enable_api_gateway_access_logs" { + type = bool + description = "Enables API Gateway access logging to cloudwatch for the default stage." + default = false } diff --git a/go.mod b/go.mod index fe8ccb8..ec10eef 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/seanturner026/serverless-release-dashboard +module github.com/seanturner026/moot go 1.16 @@ -11,6 +11,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.6.8 // indirect github.com/sirupsen/logrus v1.8.1 + github.com/urfave/cli/v2 v2.2.0 github.com/xanzy/go-gitlab v0.44.0 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93 diff --git a/go.sum b/go.sum index 1ca8957..9950ac9 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -134,7 +135,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -143,6 +146,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/xanzy/go-gitlab v0.44.0 h1:cEiGhqu7EpFGuei2a2etAwB+x6403E5CvpLn35y+GPs= github.com/xanzy/go-gitlab v0.44.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= diff --git a/internal/util/post_to_slack.go b/internal/util/post_to_slack.go index ce5eea4..0f075fb 100644 --- a/internal/util/post_to_slack.go +++ b/internal/util/post_to_slack.go @@ -28,7 +28,6 @@ func PostToSlack(webhookURL, message string) error { req.Header.Add("Content-Type", "application/json") clientSlack := &http.Client{Timeout: 4 * time.Second} - log.Printf("[INFO] sending slack notification...") resp, err := clientSlack.Do(req) if err != nil { log.Error(fmt.Sprintf("unable to send POST request, %v", err))