diff --git a/example-project/.gitlab-ci.yml b/example-project/.gitlab-ci.yml new file mode 100644 index 0000000..8f38b38 --- /dev/null +++ b/example-project/.gitlab-ci.yml @@ -0,0 +1,222 @@ +image: docker:stable + +services: + - docker:dind + +variables: + AWS_ECS_CLUSTER: example-project + AWS_S3_STATIC_BASE_URI: example-project-stage/public + CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH + DOCKER_HOST: tcp://docker:2375 + DOCKER_DRIVER: overlay2 + +before_script: + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com + +stages: + - build:base + - build:stage + - migrate:stage + - deploy:stage + - build:prod + - migrate:prod + - deploy:prod + +cache: + key: ${CI_COMMIT_REF_SLUG} + +# Everything should get this, whether through subtemplates or explicitly +# embedded in a job. +.job_tmpl: &job_tmpl + only: + - master + +.build_tmpl: &build_tmpl + <<: *job_tmpl + script: + - 'git clone http://gitlab+deploy-token-50199:z2WzSQKY9Crzvw98yzTy@gitlab.com/gitw/akverse.git' + - 'CI=1 PUSH=${PUSH} PUSH_AWS_REGISTRY=${PUSH_AWS_REGISTRY} ./akverse/scripts/build.sh $SERVICE $TARGET_ENV -' + +.deploy_tmpl: &deploy_tmpl + <<: *job_tmpl + script: + - 'git clone http://gitlab+deploy-token-50199:z2WzSQKY9Crzvw98yzTy@gitlab.com/gitw/akverse.git' + - 'LB=${ENABLE_LB} SD=${ENABLE_SD} VPC=${ENABLE_VPC} S3_BUCKET=${S3_BUCKET} S3_KEY=${S3_KEY} STATIC_S3_URI=${STATIC_S3_URI} ./akverse/scripts/deploy.sh $SERVICE $TARGET_ENV - ${ECS_CLUSTER}' + +.build_base_tmpl: &build_base_tmpl + <<: *build_tmpl + stage: build:base + tags: + - stage + only: + - master + - stage + - /^stage-.*$/ + - prod + - /^prod-.*$/ + +.build_stage_tmpl: &build_stage_tmpl + <<: *build_tmpl + stage: build:stage + tags: + - stage + +.build_prod_tmpl: &build_prod_tmpl + <<: *build_tmpl + stage: build:prod + tags: + - prod + +.deploy_stage_tmpl: &deploy_stage_tmpl + <<: *deploy_tmpl + stage: deploy:stage + tags: + - stage + environment: + name: 'stage/${SERVICE}-stage' + +.deploy_prod_tmpl: &deploy_prod_tmpl + <<: *deploy_tmpl + stage: deploy:prod + tags: + - prod + environment: + name: 'production/${SERVICE}' + when: manual + +.migrate_stage_tmpl: &migrate_stage_tmpl + <<: *build_tmpl + stage: migrate:stage + tags: + - stage + only: + - master + - stage + - /^stage-.*$/ + - prod + - /^prod-.*$/ + +.migrate_prod_tmpl: &migrate_prod_tmpl + <<: *build_tmpl + stage: migrate:prod + tags: + - prod + when: manual + only: + - master + - prod + - /^prod-.*$/ + +datadog-agent:build:stage: + <<: *build_stage_tmpl + variables: + TARGET_ENV: 'stage' + SERVICE: 'datadog-agent' + PUSH_AWS_REGISTRY: 1 + +datadog-agent:build:prod: + <<: *build_prod_tmpl + variables: + TARGET_ENV: 'prod' + SERVICE: 'datadog-agent' + PUSH_AWS_REGISTRY: 1 + dependencies: + - 'datadog-agent:build:stage' + +db:migrate:stage: + <<: *migrate_stage_tmpl + variables: + TARGET_ENV: 'stage' + SERVICE: 'schema' + +db:migrate:prod: + <<: *migrate_prod_tmpl + variables: + TARGET_ENV: 'prod' + SERVICE: 'schema' + dependencies: + - 'db:migrate:stage' + +webapi:build:stage: + <<: *build_stage_tmpl + variables: + TARGET_ENV: 'stage' + SERVICE: 'webapi' + only: + - master + - stage + - stage-webapi + - prod + - prod-webapi +webapi:deploy:stage: + <<: *deploy_stage_tmpl + variables: + TARGET_ENV: 'stage' + SERVICE: 'webapi' + ECS_CLUSTER: '${ECS_CLUSTER}' + STATIC_S3_URI: '${AWS_S3_STATIC_BASE_URI}/stage/webapi' + ENABLE_LB: 0 + dependencies: + - 'webapi:build:stage' + - 'db:migrate:stage' + - 'datadog-agent:build:stage' + only: + - master + - stage + - stage-webapi + - prod + - prod-webapi +webapi:build:prod: + <<: *build_prod_tmpl + variables: + TARGET_ENV: 'prod' + SERVICE: 'webapi' + dependencies: + - 'webapi:deploy:stage' + only: + - master + - prod + - prod-webapi +webapi:deploy:prod: + <<: *deploy_prod_tmpl + variables: + TARGET_ENV: 'prod' + SERVICE: 'webapi' + ECS_CLUSTER: '${ECS_CLUSTER}' + STATIC_S3_URI: '${AWS_S3_STATIC_BASE_URI}/prod/webapi' + ENABLE_LB: 0 + dependencies: + - 'webapi:build:prod' + - 'db:migrate:prod' + - 'datadog-agent:build:prod' + only: + - master + - prod + - prod-webapi + +#ddlogscollector:deploy:stage: +# <<: *deploy_stage_tmpl +# variables: +# TARGET_ENV: 'stage' +# ECS_CLUSTER: '${ECS_CLUSTER}' +# SERVICE: 'ddlogscollector' +# S3_BUCKET: 'keenispace-services-stage' +# S3_KEY: 'aws/lambda/ddlogscollector/src/ddlogscollector-stage.zip' +# ENABLE_VPC: 0 +# only: +# - master +# - stage +#ddlogscollector:deploy:prod: +# <<: *deploy_prod_tmpl +# variables: +# TARGET_ENV: 'prod' +# ECS_CLUSTER: '${ECS_CLUSTER}' +# SERVICE: 'ddlogscollector' +# S3_BUCKET: 'keenispace-services-prod' +# S3_KEY: 'aws/lambda/ddlogscollector/src/ddlogscollector-prod.zip' +# ENABLE_VPC: 0 +# only: +# - master +# - prod +# #dependencies: +# # - 'ddlogscollector:deploy:stage' diff --git a/example-project/cmd/web-api/Dockerfile b/example-project/cmd/web-api/Dockerfile index 77b9cfc..1c9cf2f 100644 --- a/example-project/cmd/web-api/Dockerfile +++ b/example-project/cmd/web-api/Dockerfile @@ -12,7 +12,8 @@ RUN GO111MODULE=off go get gopkg.in/go-playground/validator.v9 && \ GO111MODULE=off go get github.com/lib/pq/oid && \ GO111MODULE=off go get github.com/lib/pq/scram && \ GO111MODULE=off go get github.com/tinylib/msgp/msgp && \ - GO111MODULE=off go get gopkg.in/DataDog/dd-trace-go.v1/ddtrace + GO111MODULE=off go get gopkg.in/DataDog/dd-trace-go.v1/ddtrace && \ + GO111MODULE=off go get github.com/xwb1989/sqlparser # Install swag with go modules enabled. RUN GO111MODULE=on go get -u github.com/swaggo/swag/cmd/swag @@ -52,6 +53,12 @@ COPY --from=builder /gosrv / #COPY --from=builder /static /static COPY --from=builder /templates /templates +ARG service +ENV SERVICE_NAME $service + +ARG env="dev" +ENV ENV $env + ARG gogc="20" ENV GOGC $gogc diff --git a/example-project/cmd/web-api/docs/docs.go b/example-project/cmd/web-api/docs/docs.go index face63b..85ce532 100644 --- a/example-project/cmd/web-api/docs/docs.go +++ b/example-project/cmd/web-api/docs/docs.go @@ -1,6 +1,6 @@ // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // This file was generated by swaggo/swag at -// 2019-06-25 22:24:43.036451 -0800 AKDT m=+251.619672639 +// 2019-06-27 04:56:45.692511 -0800 AKDT m=+325.727343639 package docs @@ -62,7 +62,7 @@ var doc = `{ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -77,13 +77,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -136,13 +129,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "404": { "description": "Not Found", "schema": { @@ -191,38 +177,26 @@ var doc = `{ } ], "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "$ref": "#/definitions/user.Token" - }, - "headers": { - "Token": { - "type": "string", - "description": "qwerty" - } - } - }, + "200": {}, "400": { "description": "Bad Request", "schema": { "type": "object", - "$ref": "#/definitions/web.Error" + "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Unauthorized", "schema": { "type": "object", - "$ref": "#/definitions/web.Error" + "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", + "500": { + "description": "Internal Server Error", "schema": { "type": "object", - "$ref": "#/definitions/web.Error" + "$ref": "#/definitions/web.ErrorResponse" } } } @@ -343,8 +317,8 @@ var doc = `{ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "type": "object", "$ref": "#/definitions/project.ProjectResponse" @@ -410,7 +384,7 @@ var doc = `{ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -425,13 +399,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -473,7 +440,7 @@ var doc = `{ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -488,13 +455,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -547,13 +507,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "404": { "description": "Not Found", "schema": { @@ -597,7 +550,7 @@ var doc = `{ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -612,13 +565,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -655,8 +601,8 @@ var doc = `{ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "type": "object", "$ref": "#/definitions/signup.SignupResponse" @@ -669,6 +615,83 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } + } + }, + "/user_accounts": { + "get": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Find returns the existing user accounts in the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user_account" + ], + "summary": "List user accounts", + "parameters": [ + { + "type": "string", + "description": "Filter string, example: account_id = 'c4653bf9-5978-48b7-89c5-95704aebb7e2'", + "name": "where", + "in": "query" + }, + { + "type": "string", + "description": "Order columns separated by comma, example: created_at desc", + "name": "order", + "in": "query" + }, + { + "type": "integer", + "description": "Limit, example: 10", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset, example: 20", + "name": "offset", + "in": "query" + }, + { + "type": "boolean", + "description": "Included Archived, example: false", + "name": "included-archived", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/user_account.UserAccountResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, "403": { "description": "Forbidden", "schema": { @@ -684,6 +707,293 @@ var doc = `{ } } } + }, + "post": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Create inserts a new user account into the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user_account" + ], + "summary": "Create new user account.", + "parameters": [ + { + "description": "User Account details", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/user_account.UserAccountCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "$ref": "#/definitions/user_account.UserAccountResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Delete removes the specified user account from the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Delete user account by user ID and account ID", + "parameters": [ + { + "type": "string", + "description": "UserAccount ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": {}, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Update updates the specified user account in the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Update user account by user ID and account ID", + "parameters": [ + { + "description": "Update fields", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/user_account.UserAccountUpdateRequest" + } + } + ], + "responses": { + "204": {}, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } + } + }, + "/user_accounts/archive": { + "patch": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Archive soft-deletes the specified user account from the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Archive user account by user ID and account ID", + "parameters": [ + { + "description": "Update fields", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/user_account.UserAccountArchiveRequest" + } + } + ], + "responses": { + "204": {}, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } + } + }, + "/user_accounts/{id}": { + "get": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Read returns the specified user account from the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user_account" + ], + "summary": "Get user account by ID", + "parameters": [ + { + "type": "string", + "description": "UserAccount ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/user_account.UserAccountResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } } }, "/users": { @@ -753,13 +1063,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -799,8 +1102,8 @@ var doc = `{ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "type": "object", "$ref": "#/definitions/user.UserResponse" @@ -820,13 +1123,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -866,7 +1162,7 @@ var doc = `{ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -881,13 +1177,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -929,7 +1218,7 @@ var doc = `{ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -944,13 +1233,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -992,7 +1274,7 @@ var doc = `{ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -1007,13 +1289,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -1052,7 +1327,7 @@ var doc = `{ } ], "responses": { - "201": {}, + "200": {}, "400": { "description": "Bad Request", "schema": { @@ -1060,15 +1335,8 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, - "404": { - "description": "Not Found", + "401": { + "description": "Unauthorized", "schema": { "type": "object", "$ref": "#/definitions/web.ErrorResponse" @@ -1126,13 +1394,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "404": { "description": "Not Found", "schema": { @@ -1176,7 +1437,7 @@ var doc = `{ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -1191,13 +1452,6 @@ var doc = `{ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -1435,79 +1689,61 @@ var doc = `{ } } }, + "signup.SignupAccount": { + "type": "object", + "required": [ + "address1", + "city", + "country", + "name", + "region", + "zipcode" + ], + "properties": { + "address1": { + "type": "string", + "example": "221 Tatitlek Ave" + }, + "address2": { + "type": "string", + "example": "Box #1832" + }, + "city": { + "type": "string", + "example": "Valdez" + }, + "country": { + "type": "string", + "example": "USA" + }, + "name": { + "type": "string", + "example": "Company {RANDOM_UUID}" + }, + "region": { + "type": "string", + "example": "AK" + }, + "timezone": { + "type": "string", + "example": "America/Anchorage" + }, + "zipcode": { + "type": "string", + "example": "99686" + } + } + }, "signup.SignupRequest": { "type": "object", "properties": { "account": { "type": "object", - "required": [ - "name", - "address1", - "city", - "region", - "country", - "zipcode" - ], - "properties": { - "address1": { - "type": "string", - "example": "221 Tatitlek Ave" - }, - "address2": { - "type": "string", - "example": "Box #1832" - }, - "city": { - "type": "string", - "example": "Valdez" - }, - "country": { - "type": "string", - "example": "USA" - }, - "name": { - "type": "string", - "example": "Company {RANDOM_UUID}" - }, - "region": { - "type": "string", - "example": "AK" - }, - "timezone": { - "type": "string", - "example": "America/Anchorage" - }, - "zipcode": { - "type": "string", - "example": "99686" - } - } + "$ref": "#/definitions/signup.SignupAccount" }, "user": { "type": "object", - "required": [ - "name", - "email", - "password" - ], - "properties": { - "email": { - "type": "string", - "example": "{RANDOM_EMAIL}" - }, - "name": { - "type": "string", - "example": "Gabi May" - }, - "password": { - "type": "string", - "example": "SecretString" - }, - "password_confirm": { - "type": "string", - "example": "SecretString" - } - } + "$ref": "#/definitions/signup.SignupUser" } } }, @@ -1515,24 +1751,38 @@ var doc = `{ "type": "object", "properties": { "account": { - "type": "string" + "type": "object", + "$ref": "#/definitions/account.AccountResponse" }, "user": { - "type": "string" + "type": "object", + "$ref": "#/definitions/user.UserResponse" } } }, - "user.Token": { + "signup.SignupUser": { "type": "object", + "required": [ + "email", + "name", + "password" + ], "properties": { - "access_token": { - "type": "string" + "email": { + "type": "string", + "example": "{RANDOM_EMAIL}" }, - "expiry": { - "type": "string" + "name": { + "type": "string", + "example": "Gabi May" }, - "token_type": { - "type": "string" + "password": { + "type": "string", + "example": "SecretString" + }, + "password_confirm": { + "type": "string", + "example": "SecretString" } } }, @@ -1656,6 +1906,151 @@ var doc = `{ } } }, + "user_account.UserAccountArchiveRequest": { + "type": "object", + "required": [ + "account_id", + "user_id" + ], + "properties": { + "account_id": { + "type": "string", + "example": "c4653bf9-5978-48b7-89c5-95704aebb7e2" + }, + "user_id": { + "type": "string", + "example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2" + } + } + }, + "user_account.UserAccountCreateRequest": { + "type": "object", + "required": [ + "account_id", + "roles", + "user_id" + ], + "properties": { + "account_id": { + "type": "string", + "example": "c4653bf9-5978-48b7-89c5-95704aebb7e2" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "admin", + "user" + ] + }, + "example": [ + "admin" + ] + }, + "status": { + "type": "string", + "enum": [ + "active", + "invited", + "disabled" + ], + "example": "active" + }, + "user_id": { + "type": "string", + "example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2" + } + } + }, + "user_account.UserAccountResponse": { + "type": "object", + "required": [ + "roles" + ], + "properties": { + "account_id": { + "type": "string", + "example": "c4653bf9-5978-48b7-89c5-95704aebb7e2" + }, + "archived_at": { + "type": "object", + "$ref": "#/definitions/web.TimeResponse" + }, + "created_at": { + "type": "object", + "$ref": "#/definitions/web.TimeResponse" + }, + "id": { + "type": "string", + "example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "admin", + "user" + ] + }, + "example": [ + "admin" + ] + }, + "status": { + "type": "object", + "$ref": "#/definitions/web.EnumResponse" + }, + "updated_at": { + "type": "object", + "$ref": "#/definitions/web.TimeResponse" + }, + "user_id": { + "type": "string", + "example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2" + } + } + }, + "user_account.UserAccountUpdateRequest": { + "type": "object", + "required": [ + "account_id", + "user_id" + ], + "properties": { + "account_id": { + "type": "string", + "example": "c4653bf9-5978-48b7-89c5-95704aebb7e2" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "admin", + "user" + ] + }, + "example": [ + "user" + ] + }, + "status": { + "type": "string", + "enum": [ + "active", + "invited", + "disabled" + ], + "example": "disabled" + }, + "user_id": { + "type": "string", + "example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2" + } + } + }, "web.EnumOption": { "type": "object", "properties": { @@ -1692,23 +2087,6 @@ var doc = `{ } } }, - "web.Error": { - "type": "object", - "properties": { - "err": { - "type": "error" - }, - "fields": { - "type": "array", - "items": { - "$ref": "#/definitions/web.FieldError" - } - }, - "status": { - "type": "integer" - } - } - }, "web.ErrorResponse": { "type": "object", "properties": { diff --git a/example-project/cmd/web-api/docs/swagger.json b/example-project/cmd/web-api/docs/swagger.json index d05c9c3..8f38876 100644 --- a/example-project/cmd/web-api/docs/swagger.json +++ b/example-project/cmd/web-api/docs/swagger.json @@ -49,7 +49,7 @@ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -64,13 +64,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -123,13 +116,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "404": { "description": "Not Found", "schema": { @@ -178,38 +164,26 @@ } ], "responses": { - "200": { - "description": "OK", - "schema": { - "type": "object", - "$ref": "#/definitions/user.Token" - }, - "headers": { - "Token": { - "type": "string", - "description": "qwerty" - } - } - }, + "200": {}, "400": { "description": "Bad Request", "schema": { "type": "object", - "$ref": "#/definitions/web.Error" + "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", + "401": { + "description": "Unauthorized", "schema": { "type": "object", - "$ref": "#/definitions/web.Error" + "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", + "500": { + "description": "Internal Server Error", "schema": { "type": "object", - "$ref": "#/definitions/web.Error" + "$ref": "#/definitions/web.ErrorResponse" } } } @@ -330,8 +304,8 @@ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "type": "object", "$ref": "#/definitions/project.ProjectResponse" @@ -397,7 +371,7 @@ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -412,13 +386,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -460,7 +427,7 @@ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -475,13 +442,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -534,13 +494,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "404": { "description": "Not Found", "schema": { @@ -584,7 +537,7 @@ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -599,13 +552,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -642,8 +588,8 @@ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "type": "object", "$ref": "#/definitions/signup.SignupResponse" @@ -656,6 +602,83 @@ "$ref": "#/definitions/web.ErrorResponse" } }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } + } + }, + "/user_accounts": { + "get": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Find returns the existing user accounts in the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user_account" + ], + "summary": "List user accounts", + "parameters": [ + { + "type": "string", + "description": "Filter string, example: account_id = 'c4653bf9-5978-48b7-89c5-95704aebb7e2'", + "name": "where", + "in": "query" + }, + { + "type": "string", + "description": "Order columns separated by comma, example: created_at desc", + "name": "order", + "in": "query" + }, + { + "type": "integer", + "description": "Limit, example: 10", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset, example: 20", + "name": "offset", + "in": "query" + }, + { + "type": "boolean", + "description": "Included Archived, example: false", + "name": "included-archived", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/user_account.UserAccountResponse" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, "403": { "description": "Forbidden", "schema": { @@ -671,6 +694,293 @@ } } } + }, + "post": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Create inserts a new user account into the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user_account" + ], + "summary": "Create new user account.", + "parameters": [ + { + "description": "User Account details", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/user_account.UserAccountCreateRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "type": "object", + "$ref": "#/definitions/user_account.UserAccountResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } + }, + "delete": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Delete removes the specified user account from the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Delete user account by user ID and account ID", + "parameters": [ + { + "type": "string", + "description": "UserAccount ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": {}, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } + }, + "patch": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Update updates the specified user account in the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Update user account by user ID and account ID", + "parameters": [ + { + "description": "Update fields", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/user_account.UserAccountUpdateRequest" + } + } + ], + "responses": { + "204": {}, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } + } + }, + "/user_accounts/archive": { + "patch": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Archive soft-deletes the specified user account from the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Archive user account by user ID and account ID", + "parameters": [ + { + "description": "Update fields", + "name": "data", + "in": "body", + "required": true, + "schema": { + "type": "object", + "$ref": "#/definitions/user_account.UserAccountArchiveRequest" + } + } + ], + "responses": { + "204": {}, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } + } + }, + "/user_accounts/{id}": { + "get": { + "security": [ + { + "OAuth2Password": [] + } + ], + "description": "Read returns the specified user account from the system.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user_account" + ], + "summary": "Get user account by ID", + "parameters": [ + { + "type": "string", + "description": "UserAccount ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "$ref": "#/definitions/user_account.UserAccountResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "$ref": "#/definitions/web.ErrorResponse" + } + } + } } }, "/users": { @@ -740,13 +1050,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -786,8 +1089,8 @@ } ], "responses": { - "200": { - "description": "OK", + "201": { + "description": "Created", "schema": { "type": "object", "$ref": "#/definitions/user.UserResponse" @@ -807,13 +1110,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -853,7 +1149,7 @@ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -868,13 +1164,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -916,7 +1205,7 @@ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -931,13 +1220,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -979,7 +1261,7 @@ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -994,13 +1276,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -1039,7 +1314,7 @@ } ], "responses": { - "201": {}, + "200": {}, "400": { "description": "Bad Request", "schema": { @@ -1047,15 +1322,8 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, - "404": { - "description": "Not Found", + "401": { + "description": "Unauthorized", "schema": { "type": "object", "$ref": "#/definitions/web.ErrorResponse" @@ -1113,13 +1381,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "403": { - "description": "Forbidden", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "404": { "description": "Not Found", "schema": { @@ -1163,7 +1424,7 @@ } ], "responses": { - "201": {}, + "204": {}, "400": { "description": "Bad Request", "schema": { @@ -1178,13 +1439,6 @@ "$ref": "#/definitions/web.ErrorResponse" } }, - "404": { - "description": "Not Found", - "schema": { - "type": "object", - "$ref": "#/definitions/web.ErrorResponse" - } - }, "500": { "description": "Internal Server Error", "schema": { @@ -1422,79 +1676,61 @@ } } }, + "signup.SignupAccount": { + "type": "object", + "required": [ + "address1", + "city", + "country", + "name", + "region", + "zipcode" + ], + "properties": { + "address1": { + "type": "string", + "example": "221 Tatitlek Ave" + }, + "address2": { + "type": "string", + "example": "Box #1832" + }, + "city": { + "type": "string", + "example": "Valdez" + }, + "country": { + "type": "string", + "example": "USA" + }, + "name": { + "type": "string", + "example": "Company {RANDOM_UUID}" + }, + "region": { + "type": "string", + "example": "AK" + }, + "timezone": { + "type": "string", + "example": "America/Anchorage" + }, + "zipcode": { + "type": "string", + "example": "99686" + } + } + }, "signup.SignupRequest": { "type": "object", "properties": { "account": { "type": "object", - "required": [ - "name", - "address1", - "city", - "region", - "country", - "zipcode" - ], - "properties": { - "address1": { - "type": "string", - "example": "221 Tatitlek Ave" - }, - "address2": { - "type": "string", - "example": "Box #1832" - }, - "city": { - "type": "string", - "example": "Valdez" - }, - "country": { - "type": "string", - "example": "USA" - }, - "name": { - "type": "string", - "example": "Company {RANDOM_UUID}" - }, - "region": { - "type": "string", - "example": "AK" - }, - "timezone": { - "type": "string", - "example": "America/Anchorage" - }, - "zipcode": { - "type": "string", - "example": "99686" - } - } + "$ref": "#/definitions/signup.SignupAccount" }, "user": { "type": "object", - "required": [ - "name", - "email", - "password" - ], - "properties": { - "email": { - "type": "string", - "example": "{RANDOM_EMAIL}" - }, - "name": { - "type": "string", - "example": "Gabi May" - }, - "password": { - "type": "string", - "example": "SecretString" - }, - "password_confirm": { - "type": "string", - "example": "SecretString" - } - } + "$ref": "#/definitions/signup.SignupUser" } } }, @@ -1502,24 +1738,38 @@ "type": "object", "properties": { "account": { - "type": "string" + "type": "object", + "$ref": "#/definitions/account.AccountResponse" }, "user": { - "type": "string" + "type": "object", + "$ref": "#/definitions/user.UserResponse" } } }, - "user.Token": { + "signup.SignupUser": { "type": "object", + "required": [ + "email", + "name", + "password" + ], "properties": { - "access_token": { - "type": "string" + "email": { + "type": "string", + "example": "{RANDOM_EMAIL}" }, - "expiry": { - "type": "string" + "name": { + "type": "string", + "example": "Gabi May" }, - "token_type": { - "type": "string" + "password": { + "type": "string", + "example": "SecretString" + }, + "password_confirm": { + "type": "string", + "example": "SecretString" } } }, @@ -1643,6 +1893,151 @@ } } }, + "user_account.UserAccountArchiveRequest": { + "type": "object", + "required": [ + "account_id", + "user_id" + ], + "properties": { + "account_id": { + "type": "string", + "example": "c4653bf9-5978-48b7-89c5-95704aebb7e2" + }, + "user_id": { + "type": "string", + "example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2" + } + } + }, + "user_account.UserAccountCreateRequest": { + "type": "object", + "required": [ + "account_id", + "roles", + "user_id" + ], + "properties": { + "account_id": { + "type": "string", + "example": "c4653bf9-5978-48b7-89c5-95704aebb7e2" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "admin", + "user" + ] + }, + "example": [ + "admin" + ] + }, + "status": { + "type": "string", + "enum": [ + "active", + "invited", + "disabled" + ], + "example": "active" + }, + "user_id": { + "type": "string", + "example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2" + } + } + }, + "user_account.UserAccountResponse": { + "type": "object", + "required": [ + "roles" + ], + "properties": { + "account_id": { + "type": "string", + "example": "c4653bf9-5978-48b7-89c5-95704aebb7e2" + }, + "archived_at": { + "type": "object", + "$ref": "#/definitions/web.TimeResponse" + }, + "created_at": { + "type": "object", + "$ref": "#/definitions/web.TimeResponse" + }, + "id": { + "type": "string", + "example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "admin", + "user" + ] + }, + "example": [ + "admin" + ] + }, + "status": { + "type": "object", + "$ref": "#/definitions/web.EnumResponse" + }, + "updated_at": { + "type": "object", + "$ref": "#/definitions/web.TimeResponse" + }, + "user_id": { + "type": "string", + "example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2" + } + } + }, + "user_account.UserAccountUpdateRequest": { + "type": "object", + "required": [ + "account_id", + "user_id" + ], + "properties": { + "account_id": { + "type": "string", + "example": "c4653bf9-5978-48b7-89c5-95704aebb7e2" + }, + "roles": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "admin", + "user" + ] + }, + "example": [ + "user" + ] + }, + "status": { + "type": "string", + "enum": [ + "active", + "invited", + "disabled" + ], + "example": "disabled" + }, + "user_id": { + "type": "string", + "example": "d69bdef7-173f-4d29-b52c-3edc60baf6a2" + } + } + }, "web.EnumOption": { "type": "object", "properties": { @@ -1679,23 +2074,6 @@ } } }, - "web.Error": { - "type": "object", - "properties": { - "err": { - "type": "error" - }, - "fields": { - "type": "array", - "items": { - "$ref": "#/definitions/web.FieldError" - } - }, - "status": { - "type": "integer" - } - } - }, "web.ErrorResponse": { "type": "object", "properties": { diff --git a/example-project/cmd/web-api/docs/swagger.yaml b/example-project/cmd/web-api/docs/swagger.yaml index 7006701..f0022c7 100644 --- a/example-project/cmd/web-api/docs/swagger.yaml +++ b/example-project/cmd/web-api/docs/swagger.yaml @@ -164,77 +164,76 @@ definitions: required: - id type: object + signup.SignupAccount: + properties: + address1: + example: 221 Tatitlek Ave + type: string + address2: + example: 'Box #1832' + type: string + city: + example: Valdez + type: string + country: + example: USA + type: string + name: + example: Company {RANDOM_UUID} + type: string + region: + example: AK + type: string + timezone: + example: America/Anchorage + type: string + zipcode: + example: "99686" + type: string + required: + - address1 + - city + - country + - name + - region + - zipcode + type: object signup.SignupRequest: properties: account: - properties: - address1: - example: 221 Tatitlek Ave - type: string - address2: - example: 'Box #1832' - type: string - city: - example: Valdez - type: string - country: - example: USA - type: string - name: - example: Company {RANDOM_UUID} - type: string - region: - example: AK - type: string - timezone: - example: America/Anchorage - type: string - zipcode: - example: "99686" - type: string - required: - - name - - address1 - - city - - region - - country - - zipcode + $ref: '#/definitions/signup.SignupAccount' type: object user: - properties: - email: - example: '{RANDOM_EMAIL}' - type: string - name: - example: Gabi May - type: string - password: - example: SecretString - type: string - password_confirm: - example: SecretString - type: string - required: - - name - - email - - password + $ref: '#/definitions/signup.SignupUser' type: object type: object signup.SignupResponse: properties: account: - type: string + $ref: '#/definitions/account.AccountResponse' + type: object user: - type: string + $ref: '#/definitions/user.UserResponse' + type: object type: object - user.Token: + signup.SignupUser: properties: - access_token: + email: + example: '{RANDOM_EMAIL}' type: string - expiry: + name: + example: Gabi May type: string - token_type: + password: + example: SecretString type: string + password_confirm: + example: SecretString + type: string + required: + - email + - name + - password type: object user.UserArchiveRequest: properties: @@ -322,6 +321,110 @@ definitions: required: - id type: object + user_account.UserAccountArchiveRequest: + properties: + account_id: + example: c4653bf9-5978-48b7-89c5-95704aebb7e2 + type: string + user_id: + example: d69bdef7-173f-4d29-b52c-3edc60baf6a2 + type: string + required: + - account_id + - user_id + type: object + user_account.UserAccountCreateRequest: + properties: + account_id: + example: c4653bf9-5978-48b7-89c5-95704aebb7e2 + type: string + roles: + example: + - admin + items: + enum: + - admin + - user + type: string + type: array + status: + enum: + - active + - invited + - disabled + example: active + type: string + user_id: + example: d69bdef7-173f-4d29-b52c-3edc60baf6a2 + type: string + required: + - account_id + - roles + - user_id + type: object + user_account.UserAccountResponse: + properties: + account_id: + example: c4653bf9-5978-48b7-89c5-95704aebb7e2 + type: string + archived_at: + $ref: '#/definitions/web.TimeResponse' + type: object + created_at: + $ref: '#/definitions/web.TimeResponse' + type: object + id: + example: d69bdef7-173f-4d29-b52c-3edc60baf6a2 + type: string + roles: + example: + - admin + items: + enum: + - admin + - user + type: string + type: array + status: + $ref: '#/definitions/web.EnumResponse' + type: object + updated_at: + $ref: '#/definitions/web.TimeResponse' + type: object + user_id: + example: d69bdef7-173f-4d29-b52c-3edc60baf6a2 + type: string + required: + - roles + type: object + user_account.UserAccountUpdateRequest: + properties: + account_id: + example: c4653bf9-5978-48b7-89c5-95704aebb7e2 + type: string + roles: + example: + - user + items: + enum: + - admin + - user + type: string + type: array + status: + enum: + - active + - invited + - disabled + example: disabled + type: string + user_id: + example: d69bdef7-173f-4d29-b52c-3edc60baf6a2 + type: string + required: + - account_id + - user_id + type: object web.EnumOption: properties: selected: @@ -347,17 +450,6 @@ definitions: example: active_etc type: string type: object - web.Error: - properties: - err: - type: error - fields: - items: - $ref: '#/definitions/web.FieldError' - type: array - status: - type: integer - type: object web.ErrorResponse: properties: error: @@ -440,7 +532,7 @@ paths: produces: - application/json responses: - "201": {} + "204": {} "400": description: Bad Request schema: @@ -451,11 +543,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "404": - description: Not Found - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "500": description: Internal Server Error schema: @@ -490,11 +577,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "403": - description: Forbidden - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "404": description: Not Found schema: @@ -527,29 +609,21 @@ paths: produces: - application/json responses: - "200": - description: OK - headers: - Token: - description: qwerty - type: string - schema: - $ref: '#/definitions/user.Token' - type: object + "200": {} "400": description: Bad Request schema: - $ref: '#/definitions/web.Error' + $ref: '#/definitions/web.ErrorResponse' type: object - "403": - description: Forbidden + "401": + description: Unauthorized schema: - $ref: '#/definitions/web.Error' + $ref: '#/definitions/web.ErrorResponse' type: object - "404": - description: Not Found + "500": + description: Internal Server Error schema: - $ref: '#/definitions/web.Error' + $ref: '#/definitions/web.ErrorResponse' type: object security: - BasicAuth: [] @@ -627,7 +701,7 @@ paths: produces: - application/json responses: - "201": {} + "204": {} "400": description: Bad Request schema: @@ -638,11 +712,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "404": - description: Not Found - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "500": description: Internal Server Error schema: @@ -668,8 +737,8 @@ paths: produces: - application/json responses: - "200": - description: OK + "201": + description: Created schema: $ref: '#/definitions/project.ProjectResponse' type: object @@ -712,7 +781,7 @@ paths: produces: - application/json responses: - "201": {} + "204": {} "400": description: Bad Request schema: @@ -723,11 +792,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "404": - description: Not Found - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "500": description: Internal Server Error schema: @@ -761,11 +825,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "403": - description: Forbidden - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "404": description: Not Found schema: @@ -797,7 +856,7 @@ paths: produces: - application/json responses: - "201": {} + "204": {} "400": description: Bad Request schema: @@ -808,11 +867,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "404": - description: Not Found - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "500": description: Internal Server Error schema: @@ -839,11 +893,39 @@ paths: produces: - application/json responses: - "200": - description: OK + "201": + description: Created schema: $ref: '#/definitions/signup.SignupResponse' type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + summary: Signup handles new account creation. + tags: + - signup + /user_accounts: + delete: + consumes: + - application/json + description: Delete removes the specified user account from the system. + parameters: + - description: UserAccount ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": {} "400": description: Bad Request schema: @@ -859,9 +941,222 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - summary: Signup handles new account creation. + security: + - OAuth2Password: [] + summary: Delete user account by user ID and account ID tags: - - signup + - user + get: + consumes: + - application/json + description: Find returns the existing user accounts in the system. + parameters: + - description: 'Filter string, example: account_id = ''c4653bf9-5978-48b7-89c5-95704aebb7e2''' + in: query + name: where + type: string + - description: 'Order columns separated by comma, example: created_at desc' + in: query + name: order + type: string + - description: 'Limit, example: 10' + in: query + name: limit + type: integer + - description: 'Offset, example: 20' + in: query + name: offset + type: integer + - description: 'Included Archived, example: false' + in: query + name: included-archived + type: boolean + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/user_account.UserAccountResponse' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "403": + description: Forbidden + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + security: + - OAuth2Password: [] + summary: List user accounts + tags: + - user_account + patch: + consumes: + - application/json + description: Update updates the specified user account in the system. + parameters: + - description: Update fields + in: body + name: data + required: true + schema: + $ref: '#/definitions/user_account.UserAccountUpdateRequest' + type: object + produces: + - application/json + responses: + "204": {} + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "403": + description: Forbidden + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + security: + - OAuth2Password: [] + summary: Update user account by user ID and account ID + tags: + - user + post: + consumes: + - application/json + description: Create inserts a new user account into the system. + parameters: + - description: User Account details + in: body + name: data + required: true + schema: + $ref: '#/definitions/user_account.UserAccountCreateRequest' + type: object + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/user_account.UserAccountResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "403": + description: Forbidden + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "404": + description: Not Found + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + security: + - OAuth2Password: [] + summary: Create new user account. + tags: + - user_account + /user_accounts/{id}: + get: + consumes: + - application/json + description: Read returns the specified user account from the system. + parameters: + - description: UserAccount ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/user_account.UserAccountResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "404": + description: Not Found + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + security: + - OAuth2Password: [] + summary: Get user account by ID + tags: + - user_account + /user_accounts/archive: + patch: + consumes: + - application/json + description: Archive soft-deletes the specified user account from the system. + parameters: + - description: Update fields + in: body + name: data + required: true + schema: + $ref: '#/definitions/user_account.UserAccountArchiveRequest' + type: object + produces: + - application/json + responses: + "204": {} + "400": + description: Bad Request + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "403": + description: Forbidden + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/web.ErrorResponse' + type: object + security: + - OAuth2Password: [] + summary: Archive user account by user ID and account ID + tags: + - user /users: get: consumes: @@ -903,11 +1198,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "403": - description: Forbidden - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "500": description: Internal Server Error schema: @@ -933,7 +1223,7 @@ paths: produces: - application/json responses: - "201": {} + "204": {} "400": description: Bad Request schema: @@ -944,11 +1234,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "404": - description: Not Found - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "500": description: Internal Server Error schema: @@ -974,8 +1259,8 @@ paths: produces: - application/json responses: - "200": - description: OK + "201": + description: Created schema: $ref: '#/definitions/user.UserResponse' type: object @@ -989,11 +1274,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "404": - description: Not Found - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "500": description: Internal Server Error schema: @@ -1018,7 +1298,7 @@ paths: produces: - application/json responses: - "201": {} + "204": {} "400": description: Bad Request schema: @@ -1029,11 +1309,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "404": - description: Not Found - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "500": description: Internal Server Error schema: @@ -1067,11 +1342,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "403": - description: Forbidden - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "404": description: Not Found schema: @@ -1103,7 +1373,7 @@ paths: produces: - application/json responses: - "201": {} + "204": {} "400": description: Bad Request schema: @@ -1114,11 +1384,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "404": - description: Not Found - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "500": description: Internal Server Error schema: @@ -1145,7 +1410,7 @@ paths: produces: - application/json responses: - "201": {} + "204": {} "400": description: Bad Request schema: @@ -1156,11 +1421,6 @@ paths: schema: $ref: '#/definitions/web.ErrorResponse' type: object - "404": - description: Not Found - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object "500": description: Internal Server Error schema: @@ -1185,19 +1445,14 @@ paths: produces: - application/json responses: - "201": {} + "200": {} "400": description: Bad Request schema: $ref: '#/definitions/web.ErrorResponse' type: object - "403": - description: Forbidden - schema: - $ref: '#/definitions/web.ErrorResponse' - type: object - "404": - description: Not Found + "401": + description: Unauthorized schema: $ref: '#/definitions/web.ErrorResponse' type: object diff --git a/example-project/cmd/web-api/handlers/routes.go b/example-project/cmd/web-api/handlers/routes.go index f492986..c559bdf 100644 --- a/example-project/cmd/web-api/handlers/routes.go +++ b/example-project/cmd/web-api/handlers/routes.go @@ -11,6 +11,7 @@ import ( "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/web" "github.com/jmoiron/sqlx" "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" + _ "geeks-accelerator/oss/saas-starter-kit/example-project/internal/signup" ) // API returns a handler for a set of routes. diff --git a/example-project/cmd/web-app/Dockerfile b/example-project/cmd/web-app/Dockerfile index fb0dc3b..4f9e2e0 100644 --- a/example-project/cmd/web-app/Dockerfile +++ b/example-project/cmd/web-app/Dockerfile @@ -37,6 +37,12 @@ COPY --from=builder /gosrv / COPY --from=builder /static /static COPY --from=builder /templates /templates +ARG service +ENV SERVICE_NAME $service + +ARG env="dev" +ENV ENV $env + ARG gogc="20" ENV GOGC $gogc diff --git a/example-project/go.mod b/example-project/go.mod index 380aed4..d584bc1 100644 --- a/example-project/go.mod +++ b/example-project/go.mod @@ -2,7 +2,7 @@ module geeks-accelerator/oss/saas-starter-kit/example-project require ( github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc - github.com/aws/aws-sdk-go v1.19.33 + github.com/aws/aws-sdk-go v1.20.15 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dimfeld/httptreemux v5.0.1+incompatible github.com/dustin/go-humanize v1.0.0 diff --git a/example-project/go.sum b/example-project/go.sum index 306696c..b219dec 100644 --- a/example-project/go.sum +++ b/example-project/go.sum @@ -7,6 +7,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5Vpd github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/aws/aws-sdk-go v1.19.33 h1:qz9ZQtxCUuwBKdc5QiY6hKuISYGeRQyLVA2RryDEDaQ= github.com/aws/aws-sdk-go v1.19.33/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.20.15 h1:y9ts8MJhB7ReUidS6Rq+0KxdFeL01J+pmOlGq6YqpiQ= +github.com/aws/aws-sdk-go v1.20.15/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 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= diff --git a/example-project/tools/truss/cmd/devops/devops.go b/example-project/tools/truss/cmd/devops/devops.go new file mode 100644 index 0000000..0e2f4a1 --- /dev/null +++ b/example-project/tools/truss/cmd/devops/devops.go @@ -0,0 +1,152 @@ +package devops + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/pkg/errors" +) + +func findProjectGoModFile() (string, error) { + var err error + projectRoot, err := os.Getwd() + if err != nil { + return "", errors.WithMessage(err, "failed to get current working directory") + } + + // Try to find the project root for looking for the go.mod file in a parent directory. + var goModFile string + testDir := projectRoot + for i := 0; i < 3; i++ { + if goModFile != "" { + testDir = filepath.Join(testDir, "../") + } + goModFile = filepath.Join(testDir, "go.mod") + ok, _ := exists(goModFile) + if ok { + projectRoot = testDir + break + } + } + + // Verify the go.mod file was found. + ok, err := exists(goModFile) + if err != nil { + return "", errors.WithMessagef(err, "failed to load go.mod for project using project root %s") + } else if !ok { + return "", errors.Errorf("failed to locate project go.mod in project root %s", projectRoot) + } + + return goModFile, nil +} + +// findServiceDockerFile finds the service directory. +func findServiceDockerFile(projectRoot, targetService string) (string, error) { + checkDirs := []string{ + filepath.Join(projectRoot, "cmd", targetService), + filepath.Join(projectRoot, "tools", targetService), + } + + var dockerFile string + for _, cd := range checkDirs { + // Check to see if directory contains Dockerfile. + tf := filepath.Join(cd, "Dockerfile") + + fmt.Println(tf) + + ok, _ := exists(tf) + if ok { + dockerFile = tf + break + } + } + + if dockerFile == "" { + return "", errors.Errorf("failed to locate Dockerfile for service %s", targetService) + } + + return dockerFile, nil +} + +// getTargetEnv checks for an env var that is prefixed with the current target env. +func getTargetEnv(targetEnv, envName string) string { + k := fmt.Sprintf("%s_%s", strings.ToUpper(targetEnv), envName) + + if v := os.Getenv(k); v != "" { + // Set the non prefixed env var with the prefixed value. + os.Setenv(envName, v ) + return v + } + + return os.Getenv(envName) +} + +// loadGoModName parses out the module name from go.mod. +func loadGoModName(goModFile string ) (string, error) { + ok, err := exists(goModFile) + if err != nil { + return "", errors.WithMessage(err, "Failed to load go.mod for project") + } else if !ok { + return "", errors.Errorf("Failed to locate project go.mod at %s", goModFile) + } + + b, err := ioutil.ReadFile(goModFile) + if err != nil { + return"", errors.WithMessagef(err, "Failed to read go.mod at %s", goModFile) + } + + var name string + lines := strings.Split(string(b), "\n") + for _, l := range lines { + if strings.HasPrefix(l, "module ") { + name = strings.TrimSpace(strings.Split(l, " ")[1]) + break + } + } + + return name, nil +} + +// exists returns a bool as to whether a file path exists. +func exists(path string) (bool, error) { + _, err := os.Stat(path) + if err == nil { + return true, nil + } + if os.IsNotExist(err) { + return false, nil + } + return true, err +} + +type EnvVars []string + +// execCmds executes a set of commands. +func execCmds(workDir string, envVars *EnvVars, cmds ...[]string) ([]string, error) { + if envVars == nil { + ev := EnvVars(os.Environ()) + envVars = &ev + } + + var results []string + for _, cmdVals := range cmds { + cmd := exec.Command(cmdVals[0], cmdVals[1:]...) + cmd.Dir = workDir + cmd.Env = *envVars + out, err := cmd.CombinedOutput() + if err != nil { + return results, errors.WithMessagef(err, "failed to execute %s - %s\n%s", strings.Join(cmdVals, " "), string(out)) + } + results = append(results, string(out)) + + // Update the current env vars after command has been executed. + ev := EnvVars(cmd.Env) + envVars = &ev + } + + return results, nil +} \ No newline at end of file diff --git a/example-project/tools/truss/cmd/devops/service_build.go b/example-project/tools/truss/cmd/devops/service_build.go new file mode 100644 index 0000000..ef59407 --- /dev/null +++ b/example-project/tools/truss/cmd/devops/service_build.go @@ -0,0 +1,213 @@ +package devops + +import ( + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" + "github.com/pkg/errors" +) + +// requiredCmdsBuild proves a list of required executables for completing build. +var requiredCmdsBuild = [][]string{ + []string{"docker", "version", "-f", "{{.Client.Version}}"}, +} + +// Run is the main entrypoint for building a service for a given target env. +func ServiceBuild(log *log.Logger, projectRoot, targetService, targetEnv, releaseImage string, noPush, noCache bool) error { + + log.SetPrefix(log.Prefix() + " build : ") + + // + log.Println("Verify required commands are installed.") + for _, cmdVals := range requiredCmdsBuild { + cmd := exec.Command(cmdVals[0], cmdVals[1:]...) + cmd.Env = os.Environ() + + out, err := cmd.CombinedOutput() + if err != nil { + return errors.WithMessagef(err, "failed to execute %s - %s\n%s", strings.Join(cmdVals, " "), string(out)) + } + + log.Printf("\t%s\t%s - %s", tests.Success, cmdVals[0], string(out)) + } + + // When project root directory is empty or set to current working path, then search for the project root by locating + // the go.mod file. + var goModFile string + if projectRoot == "" || projectRoot == "." { + log.Println("Attempting to location project root directory from current working directory.") + + var err error + goModFile, err = findProjectGoModFile() + if err != nil { + return err + } + projectRoot = filepath.Dir(goModFile) + } else { + log.Println("Using supplied project root directory.") + goModFile = filepath.Join(projectRoot, "go.mod") + } + + log.Printf("\t\tproject root: %s", projectRoot) + log.Printf("\t\tgo.mod: %s", goModFile) + log.Printf("\t%s\tFound project root directory.", tests.Success) + + log.Println("Extracting go module name from go.mod.") + modName, err := loadGoModName(goModFile) + if err != nil { + return err + } + log.Printf("\t\tmodule name: %s", modName) + log.Printf("\t%s\tgo module name.", tests.Success) + + log.Println("Determining the project name.") + projectName := getTargetEnv(targetEnv, "PROJECT_NAME") + if projectName != "" { + log.Printf("\t\tproject name: %s", projectName) + log.Printf("\t%s\tFound env variable.", tests.Success) + } else { + projectName = filepath.Base(modName) + log.Printf("\t\tproject name: %s", projectName) + log.Printf("\t%s\tSet from go module.", tests.Success) + } + + log.Println("Attempting to locate service directory from project root directory.") + dockerFile, err := findServiceDockerFile(projectRoot, targetService) + if err != nil { + return err + } + serviceDir := filepath.Dir(dockerFile) + + log.Printf("\t\tservice directory: %s", serviceDir) + log.Printf("\t\tdockerfile: %s", dockerFile) + log.Printf("\t%s\tFound service directory.", tests.Success) + + + log.Println("Verify release image.") + if releaseImage == "" { + if v := os.Getenv("CI_REGISTRY_IMAGE"); v != "" { + releaseImage = fmt.Sprintf("%s:%s-%s", v, targetService, targetEnv) + } else { + releaseImage = fmt.Sprintf("%s/%s:latest", targetService, targetEnv) + noPush = true + } + } + log.Printf("\t\trelease image: %s", releaseImage) + log.Printf("\t%s\tRelease image valid.", tests.Success) + + // Load the AWS + log.Println("Verify AWS credentials.") + awsCreds, err := GetAwsCredentials(targetEnv) + if err != nil { + return err + } + + log.Printf("\t\tAccessKeyID: %s", awsCreds.AccessKeyID) + log.Printf("\t\tRegion: %s", awsCreds.Region) + + // Set default AWS Registry Name if needed. + if awsCreds.RepositoryName == "" { + awsCreds.RepositoryName = projectName + log.Printf("\t\tSetting Repository Name to Project Name.") + } + log.Printf("\t\tRepository Name: %s", awsCreds.RepositoryName) + log.Printf("\t%s\tAWS credentials valid.", tests.Success) + + // Pull the current env variables to be passed in for command execution. + envVars := EnvVars(os.Environ()) + + // Do the docker build. + { + cmdVals := []string{ + "docker", + "build", + "--file=" + dockerFile, + "--build-arg", "service=" + targetService, + "--build-arg", "env=" + targetEnv, + "-t", releaseImage, + } + + if noCache { + cmdVals = append(cmdVals, "--no-cache") + } + cmdVals = append(cmdVals, ".") + + log.Printf("starting docker build: \n\t%s\n", strings.Join(cmdVals, " ")) + out, err := execCmds(projectRoot, &envVars, cmdVals) + if err != nil { + return err + } + log.Printf("build complete\n\t%s\n", string(out[0])) + } + + // Push the newly built docker container to the registry. + if !noPush { + log.Println("Push release image.") + _, err = execCmds(projectRoot, &envVars, []string{"docker", "push", releaseImage}) + if err != nil { + return err + } + } + + + if awsCreds.RepositoryName != "" { + awsRepo, err := EcrGetOrCreateRepository(awsCreds, projectName, targetEnv) + if err != nil { + return err + } + + maxImages := defaultAwsRegistryMaxImages + if v := getTargetEnv(targetEnv, "AWS_REPOSITORY_MAX_IMAGES"); v != "" { + maxImages, err = strconv.Atoi(v) + if err != nil { + return errors.WithMessagef(err, "Failed to parse max ECR images") + } + } + + log.Println("Purging old ECR images.") + err = EcrPurgeImages(log, awsCreds, maxImages) + if err != nil { + return err + } + + log.Println("Retrieve ECR authorization token used for docker login.") + dockerLogin, err := GetEcrLogin(awsCreds) + if err != nil { + return err + } + + awsRegistryImage := *awsRepo.RepositoryUri + + // Login to AWS docker registry and pull release image locally. + log.Println("Push release image to ECR.") + log.Printf("\t\t%s", awsRegistryImage) + _, err = execCmds(projectRoot, &envVars, dockerLogin, []string{"docker", "tag", releaseImage, awsRegistryImage}, []string{"docker", "push", awsRegistryImage}) + if err != nil { + return err + } + + tag1 := targetEnv+"-"+targetService + log.Printf("\t\ttagging as %s", tag1) + _, err = execCmds(projectRoot, &envVars, []string{"docker", "tag", releaseImage, awsRegistryImage+":"+tag1}, []string{"docker", "push", awsRegistryImage+":"+tag1}) + if err != nil { + return err + } + + if v := os.Getenv("CI_COMMIT_REF_NAME"); v != "" { + tag2 := tag1 + "-" + v + log.Printf("\t\ttagging as %s", tag2) + _, err = execCmds(projectRoot, &envVars, []string{"docker", "tag", releaseImage, awsRegistryImage+ ":" + tag2}, []string{"docker", "push", awsRegistryImage + ":" + tag2}) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/example-project/tools/truss/cmd/devops/service_deploy.go b/example-project/tools/truss/cmd/devops/service_deploy.go new file mode 100644 index 0000000..4f8de74 --- /dev/null +++ b/example-project/tools/truss/cmd/devops/service_deploy.go @@ -0,0 +1,195 @@ +package devops + +import ( + "fmt" + "geeks-accelerator/oss/saas-starter-kit/example-project/internal/platform/tests" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/pkg/errors" + "log" + "os" + "os/exec" + "path/filepath" + "strings" +) + +/* +secretsmanager:GetSecretValue +ecr:GetAuthorizationToken +ecr:ListImages +ecr:DescribeRepositories +CreateRepository + */ + +// requiredCmdsBuild proves a list of required executables for completing build. +var requiredCmdsDeploy = [][]string{ + []string{"docker", "version", "-f", "{{.Client.Version}}"}, +} + + +// Run is the main entrypoint for deploying a service for a given target env. +func ServiceDeploy(log *log.Logger, projectRoot, targetService, targetEnv, releaseImage, ecsCluster string, enableVpc bool, noBuild, noDeploy, noCache bool) error { + + log.SetPrefix(log.Prefix() + " deploy : ") + + // + log.Println("Verify required commands are installed.") + for _, cmdVals := range requiredCmdsDeploy { + cmd := exec.Command(cmdVals[0], cmdVals[1:]...) + cmd.Env = os.Environ() + + out, err := cmd.CombinedOutput() + if err != nil { + return errors.WithMessagef(err, "failed to execute %s - %s\n%s", strings.Join(cmdVals, " "), string(out)) + } + + log.Printf("\t%s\t%s - %s", tests.Success, cmdVals[0], string(out)) + } + + // When project root directory is empty or set to current working path, then search for the project root by locating + // the go.mod file. + var goModFile string + if projectRoot == "" || projectRoot == "." { + log.Println("Attempting to location project root directory from current working directory.") + + var err error + goModFile, err = findProjectGoModFile() + if err != nil { + return err + } + projectRoot = filepath.Dir(goModFile) + } else { + log.Println("Using supplied project root directory.") + goModFile = filepath.Join(projectRoot, "go.mod") + } + + log.Printf("\t\tproject root: %s", projectRoot) + log.Printf("\t\tgo.mod: %s", goModFile) + log.Printf("\t%s\tFound project root directory.", tests.Success) + + log.Println("Extracting go module name from go.mod.") + modName, err := loadGoModName(goModFile) + if err != nil { + return err + } + log.Printf("\t\tmodule name: %s", modName) + log.Printf("\t%s\tgo module name.", tests.Success) + + log.Println("Determining the project name.") + projectName := getTargetEnv(targetEnv, "PROJECT_NAME") + if projectName != "" { + log.Printf("\t\tproject name: %s", projectName) + log.Printf("\t%s\tFound env variable.", tests.Success) + } else { + projectName = filepath.Base(modName) + log.Printf("\t\tproject name: %s", projectName) + log.Printf("\t%s\tSet from go module.", tests.Success) + } + + log.Println("Attempting to locate service directory from project root directory.") + dockerFile, err := findServiceDockerFile(projectRoot, targetService) + if err != nil { + return err + } + serviceDir := filepath.Dir(dockerFile) + + log.Printf("\t\tservice directory: %s", serviceDir) + log.Printf("\t\tdockerfile: %s", dockerFile) + log.Printf("\t%s\tFound service directory.", tests.Success) + + log.Println("Verify release image.") + var noPull bool + if releaseImage == "" { + if v := os.Getenv("CI_REGISTRY_IMAGE"); v != "" { + releaseImage = fmt.Sprintf("%s:%s-%s", v, targetService, targetEnv) + } else { + releaseImage = fmt.Sprintf("%s/%s:latest", targetService, targetEnv) + noPull = true + } + } + log.Printf("\t\trelease image: %s", releaseImage) + log.Printf("\t%s\tRelease image valid.", tests.Success) + + log.Println("Verify AWS credentials.") + awsCreds, err := GetAwsCredentials(targetEnv) + if err != nil { + return err + } + + if ecsCluster == "" { + ecsCluster = filepath.Base(goModFile) + "-" + targetEnv + log.Printf("AWS ECS cluster not set, assigning default value %s.", ecsCluster) + } + + // Create default service name used for deployment. + serviceName := targetService + "-" + targetEnv + _ = serviceName + + + + awsRepo, err := EcrGetOrCreateRepository(awsCreds, projectName, targetEnv) + if err != nil { + return err + } + + + + + envVars := EnvVars(os.Environ()) + + + + + + if !noPull { + log.Println("Retrieve ECR authorization token used for docker login.") + dockerLogin, err := GetEcrLogin(awsCreds) + if err != nil { + return err + } + + // Login to AWS docker registry and pull release image locally. + log.Println("Pull Release image from ECR.") + log.Printf("\t\t%s", releaseImage) + _, err = execCmds(projectRoot, &envVars, dockerLogin, []string{"docker", "pull", releaseImage}) + if err != nil { + return err + } + } + + + return nil +} + +// GetDatadogApiKey returns the Datadog API Key which can be either stored in an env var or in AWS Secrets Manager. +func GetDatadogApiKey(targetEnv string, creds *AwsCredentials) (string, error) { + + // 1. Check env vars for [DEV|STAGE|PROD]_DD_API_KEY and DD_API_KEY + apiKey := getTargetEnv(targetEnv, "DD_API_KEY") + if apiKey != "" { + return apiKey, nil + } + + // 2. Check AWS Secrets Manager for datadog entry prefixed with target env. + prefixedSecretId := strings.ToUpper(targetEnv) + "/DATADOG" + var err error + apiKey, err = GetAwsSecretValue(creds, prefixedSecretId) + if err != nil { + if aerr, ok := errors.Cause(err).(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceNotFoundException { + return "", err + } + } else if apiKey != "" { + return apiKey, nil + } + + // 3. Check AWS Secrets Manager for datadog entry. + secretId := "DATADOG" + apiKey, err = GetAwsSecretValue(creds, secretId) + if err != nil { + if aerr, ok := errors.Cause(err).(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceNotFoundException { + return "", err + } + } + + return apiKey, nil +} \ No newline at end of file diff --git a/example-project/tools/truss/main.go b/example-project/tools/truss/main.go index 1bb8e53..7656dd5 100644 --- a/example-project/tools/truss/main.go +++ b/example-project/tools/truss/main.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" + "geeks-accelerator/oss/saas-starter-kit/example-project/tools/truss/cmd/devops" "geeks-accelerator/oss/saas-starter-kit/example-project/tools/truss/cmd/dbtable2crud" "github.com/kelseyhightower/envconfig" "github.com/lib/pq" @@ -205,6 +206,63 @@ func main() { return dbtable2crud.Run(masterDb, log, cfg.DB.Database, dbTable, modelFile, modelName, templateDir, projectPath, c.Bool("saveChanges")) }, }, + { + Name: "build", + Aliases: []string{"serviceBuild"}, + Usage: "-service=web-api -env=dev [-image=gitlab.com/example-project:latest] [-root=.]", + Flags: []cli.Flag{ + cli.StringFlag{Name: "service", Usage: "name of cmd"}, + cli.StringFlag{Name: "env", Usage: "dev, stage, or prod"}, + cli.StringFlag{Name: "image", Usage: "release image used to tag docker build"}, + cli.StringFlag{Name: "root", Usage: "project root directory"}, + cli.BoolFlag{Name: "no_push", Usage: "skip docker push after build"}, + cli.BoolFlag{Name: "no_cache", Usage: "skip docker cache"}, + }, + Action: func(c *cli.Context) error { + service := strings.TrimSpace(c.String("service")) + env := strings.TrimSpace(c.String("env")) + image := strings.TrimSpace(c.String("image")) + projectRoot := strings.TrimSpace(c.String("root")) + noPush := c.Bool("no_push") + noCache := c.Bool("no_cache") + + if image == "-" { + image = "" + } + + return devops.ServiceBuild(log, projectRoot, service, env, image, noPush, noCache) + }, + }, + { + Name: "deploy", + Aliases: []string{"serviceDeploy"}, + Usage: "-service=web-api -env=dev [-image=gitlab.com/example-project:latest] [-root=.]", + Flags: []cli.Flag{ + cli.StringFlag{Name: "service", Usage: "name of cmd"}, + cli.StringFlag{Name: "env", Usage: "dev, stage, or prod"}, + cli.StringFlag{Name: "image", Usage: "release image used to tag docker build"}, + cli.StringFlag{Name: "root", Usage: "project root directory"}, + cli.StringFlag{Name: "cluster, ecs_cluster", Usage: "name of the AWS EC2 cluster."}, + cli.BoolFlag{Name: "vpc, enable_vpc", Usage: "skip docker push after build"}, + cli.BoolFlag{Name: "no_build", Usage: "skip docker push after build"}, + cli.BoolFlag{Name: "no_deploy", Usage: "skip docker push after build"}, + cli.BoolFlag{Name: "no_cache", Usage: "skip docker cache"}, + }, + Action: func(c *cli.Context) error { + service := strings.TrimSpace(c.String("service")) + env := strings.TrimSpace(c.String("env")) + image := strings.TrimSpace(c.String("image")) + projectRoot := strings.TrimSpace(c.String("root")) + ecsCluster := strings.TrimSpace(c.String("cluster")) + enableVpc := c.Bool("vpc") + + if image == "-" { + image = "" + } + + return devops.ServiceDeploy(log, projectRoot, service, env, image, ecsCluster, enableVpc) + }, + }, } err = app.Run(os.Args) diff --git a/example-project/tools/truss/sample.env b/example-project/tools/truss/sample.env index 7c5b8a1..2e3d1a9 100644 --- a/example-project/tools/truss/sample.env +++ b/example-project/tools/truss/sample.env @@ -1,4 +1,32 @@ +# Variables to configure Postgres for database migration. export TRUSS_DB_HOST=127.0.0.1:5433 export TRUSS_DB_USER=postgres export TRUSS_DB_PASS=postgres export TRUSS_DB_DISABLE_TLS=true + + +# Variables to configure service build and deploy. +# export PROJECT_NAME=example-project + +# Variables to configure AWS for service build and deploy. +# Use the same set for AWS credentials for all target envinments. +#AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXX +#AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXX +#AWS_REGION=us-west-2 +#AWS_REPOSITORY_NAME=example-project +#AWS_REPOSITORY_MAX_IMAGES=1000 + + +# AWS credentials can be prefixed with the target uppercased target envinments. +# This allows credentials unique accounts to be used for each target envinments. +# Default target envinments are: DEV, STAGE, PROD +#DEV_AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXX +#DEV_AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXX +#DEV_AWS_REGION=us-west-2 +#DEV_AWS_REPOSITORY_NAME=example-project +#DEV_AWS_REPOSITORY_MAX_IMAGES=1000 + +# GitLab CI/CD environment variables. These are set by the GitLab when the build +# pipeline is running. These can be optional set for testing/debugging locally. +#CI_REGISTRY_IMAGE="registry.example.com/gitlab-org/gitlab-ce" +#CI_COMMIT_REF_NAME=master