diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index a14437ad..79f7cecf 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -36,6 +36,11 @@ "[sql]": { "editor.formatOnSave": true }, + // There are handly utility scripts within /scripts that we invoke via go run. + // These scripts (and its dependencies) should never be consumed by the actual server directly + // Thus they are flagged to require the "scripts" build tag. + // We only inform gopls and the vscode go compiler here, that it has to set this build tag if it sees such a file. + "go.buildTags": "scripts", "gopls": { // Add parameter placeholders when completing a function. "usePlaceholders": true, @@ -60,13 +65,16 @@ "-count=1", "-v" ], + "go.coverMode": "atomic", // atomic is required when utilizing -race "go.delveConfig": { "dlvLoadConfig": { // increase max length of strings displayed in debugger "maxStringLen": 2048, }, "apiVersion": 2, - } + }, + // ensure that the pgFormatter VSCode extension uses the pgFormatter that comes preinstalled in the Dockerfile + "pgFormatter.pgFormatterPath": "/usr/local/bin/pg_format" }, // Add the IDs of extensions you want installed when the container is created. "extensions": [ diff --git a/.dockerignore b/.dockerignore index 21c53564..4728cccc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,3 @@ -**/.git .devcontainer .vscode .pkg diff --git a/.drone.yml b/.drone.yml index eab99c22..0a66de46 100644 --- a/.drone.yml +++ b/.drone.yml @@ -23,7 +23,10 @@ alias: # The image name, defaults to lowercase repo name /, e.g. aw/aaa-cab-kubernetes-test - &IMAGE_DEPLOY_NAME ${DRONE_REPO,,} - # The full uniquely tagged image name + # The intermediate builder image name + - &IMAGE_BUILDER_ID ${DRONE_REPO,,}-builder:${DRONE_COMMIT_SHA} + + # The full uniquely tagged app image name - &IMAGE_DEPLOY_ID ${DRONE_REPO,,}:${DRONE_COMMIT_SHA} # # Defines which branches will trigger a docker image push our Google Cloud Registry (tags are always published) @@ -65,7 +68,7 @@ alias: # mgmt_repo: https://git.allaboutapps.at/scm/aw/a3cloud-mgmt.git # mgmt_git_email: infrastructure+drone@allaboutapps.at - # ENV variables for executing yarn:test (typically only the DB connector is relevant) + # ENV variables for executing within the test env (similar to the env in docker-compose.yml) - &TEST_ENV CI: ${CI} @@ -87,11 +90,11 @@ alias: PSQL_PORT: *PGPORT PSQL_SSLMODE: *PGSSLMODE - # optional: project root directory, used for relative path resolution (e.g. fixtures) + # required for drone: project root directory, used for relative path resolution (e.g. fixtures) PROJECT_ROOT_DIR: /app - # optional: env for integresql client testing - # INTEGRESQL_CLIENT_BASE_URL: "http://integresql:5000/api" + # docker run related. + SERVER_MANAGEMENT_SECRET: "mgmt-secret" # Which build events should trigger the main pipeline (defaults to all) - &BUILD_EVENTS [push, pull_request, tag] @@ -109,7 +112,7 @@ pipeline: "database connection": group: build - image: postgres:12.2-alpine + image: postgres:12.4-alpine # should be the same version as used in .drone.yml, .github/workflows, Dockerfile and live commands: # wait for postgres service to become available - | @@ -127,32 +130,92 @@ pipeline: image: docker:latest volumes: - /var/run/docker.sock:/var/run/docker.sock + environment: + IMAGE_TAG: *IMAGE_BUILDER_ID commands: - - "docker build --target builder --compress -t ${DRONE_REPO,,}:${DRONE_COMMIT_SHA} ." + - "docker build --target builder --compress -t $${IMAGE_TAG} ." + <<: *WHEN_BUILD_EVENT + + "docker build (target app)": + group: build-app + image: docker:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + IMAGE_TAG: *IMAGE_DEPLOY_ID + commands: + - "docker build --target app --compress -t $${IMAGE_TAG} ." <<: *WHEN_BUILD_EVENT # --------------------------------------------------------------------------- # CHECK # --------------------------------------------------------------------------- + "trivy scan": + group: pre-test + image: aquasec/trivy:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /server/drone/trivy-cache:/root/.cache/ + environment: + IMAGE_TAG: *IMAGE_DEPLOY_ID + commands: + # Print report + - "trivy image --exit-code 0 --no-progress $${IMAGE_TAG}" + # Fail on severity HIGH and CRITICAL + - "trivy image --exit-code 1 --severity HIGH,CRITICAL --no-progress --ignore-unfixed $${IMAGE_TAG}" + <<: *WHEN_BUILD_EVENT + "build & diff": group: pre-test - image: *IMAGE_DEPLOY_ID + image: *IMAGE_BUILDER_ID commands: + - cd $PROJECT_ROOT_DIR # reuse go build cache from Dockerfile builder stage - make tidy - make build - - git diff --exit-code + - /bin/cp -Rf $PROJECT_ROOT_DIR/* $DRONE_WORKSPACE # switch back to drone workspace ... + - cd $DRONE_WORKSPACE + - "git diff --exit-code" # ... for git diffing (otherwise not possible as .git is .dockerignored) + environment: *TEST_ENV + <<: *WHEN_BUILD_EVENT + + "info": + group: test + image: *IMAGE_BUILDER_ID + commands: + - cd $PROJECT_ROOT_DIR # reuse go build cache from Dockerfile builder stage + - make info environment: *TEST_ENV <<: *WHEN_BUILD_EVENT "test": group: test - image: *IMAGE_DEPLOY_ID + image: *IMAGE_BUILDER_ID commands: + - cd $PROJECT_ROOT_DIR # reuse go build cache from Dockerfile builder stage - make test environment: *TEST_ENV <<: *WHEN_BUILD_EVENT + "test-scripts (gsdev, go-starter only)": + group: test + image: *IMAGE_BUILDER_ID + commands: + - cd $PROJECT_ROOT_DIR # reuse go build cache from Dockerfile builder stage + - make test-scripts + environment: *TEST_ENV + when: + repo: AW/go-starter + event: *BUILD_EVENTS + + "git-compare-go-starter": + group: test + image: *IMAGE_BUILDER_ID + commands: + - make git-compare-go-starter + environment: *TEST_ENV + <<: *WHEN_BUILD_EVENT + "swagger-codegen-cli": group: test # https://github.com/swagger-api/swagger-codegen/blob/master/modules/swagger-codegen-cli/Dockerfile @@ -162,72 +225,187 @@ pipeline: - "java -jar /opt/swagger-codegen-cli/swagger-codegen-cli.jar validate -i ./api/swagger.yml" <<: *WHEN_BUILD_EVENT + "binary: deps": + group: test + image: *IMAGE_BUILDER_ID + commands: + - cd $PROJECT_ROOT_DIR + - make get-embedded-modules-count + - make get-embedded-modules + environment: *TEST_ENV + <<: *WHEN_BUILD_EVENT + + "binary: licenses": + group: test + image: *IMAGE_BUILDER_ID + commands: + - cd $PROJECT_ROOT_DIR + - make get-licenses + environment: *TEST_ENV + <<: *WHEN_BUILD_EVENT + + "docker run (target app)": + group: test + image: docker:latest + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + <<: *TEST_ENV + IMAGE_TAG: *IMAGE_DEPLOY_ID + commands: + # Note: NO network related tests are possible here, dnd can just + # run sibling containers. We have no possibility to connect them + # into the drone user defined per build docker network! + # https://github.com/drone-plugins/drone-docker/issues/193 + # https://jpetazzo.github.io/2015/09/03/do-not-use-docker-in-docker-for-ci/ + - (env | grep "=" | grep -v -e "DRONE=" -e "DRONE_" -e "CI_" -e "CI=" -e "HOME=" -e "HOSTNAME=" -e "SHELL=" -e "PWD=" -e "PATH=") > .hostenv + - cat .hostenv + - "docker run --env-file .hostenv $${IMAGE_TAG} help" + - "docker run --env-file .hostenv $${IMAGE_TAG} -v" + - "docker run --env-file .hostenv $${IMAGE_TAG} env" + <<: *WHEN_BUILD_EVENT + # --------------------------------------------------------------------------- # PUBLISH # --------------------------------------------------------------------------- -# # Built a tag? Push to cloud registry -# "publish tag_${DRONE_COMMIT_SHA:0:10}": -# group: publish -# <<: *GCR_REGISTRY_SETTINGS -# tags: -# - build_${DRONE_BUILD_NUMBER} -# - tag_${DRONE_COMMIT_SHA:0:10} -# - *IMAGE_DEPLOY_TAG -# - latest -# - ${DRONE_TAG} -# - ${DRONE_COMMIT_SHA:0:10} -# when: -# event: tag + # # Built a allowed branch? Push to cloud registry + # "publish ${DRONE_BRANCH}_${DRONE_COMMIT_SHA:0:10}": + # group: publish + # <<: *GCR_REGISTRY_SETTINGS + # tags: + # - build_${DRONE_BUILD_NUMBER} + # - ${DRONE_BRANCH/\//-}_${DRONE_COMMIT_SHA:0:10} + # - *IMAGE_DEPLOY_TAG + # - latest + # - ${DRONE_BRANCH/\//-} + # - '${DRONE_COMMIT_SHA:0:10}' + # when: + # branch: *GCR_PUBLISH_BRANCHES + # event: *BUILD_EVENTS + + # # Built a tag? Push to cloud registry + # "publish tag_${DRONE_COMMIT_SHA:0:10}": + # group: publish + # <<: *GCR_REGISTRY_SETTINGS + # tags: + # - build_${DRONE_BUILD_NUMBER} + # - tag_${DRONE_COMMIT_SHA:0:10} + # - *IMAGE_DEPLOY_TAG + # - latest + # - ${DRONE_TAG} + # - ${DRONE_COMMIT_SHA:0:10} + # when: + # event: tag # --------------------------------------------------------------------------- # DEPLOYMENT # --------------------------------------------------------------------------- -# # autodeploy dev if it hits the branch -# "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_DEV} (auto)": -# <<: *K8S_DEPLOY_SETTINGS -# namespace: ${K8S_DEPLOY_NS_DEV} -# mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_DEV}/app.deployment.yaml -# when: -# event: *BUILD_EVENTS -# branch: [dev] - -# # promote dev through "drone deploy dev" -# "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_DEV} (promote)": -# <<: *K8S_DEPLOY_SETTINGS -# namespace: ${K8S_DEPLOY_NS_DEV} -# mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_DEV}/app.deployment.yaml -# when: -# environment: dev -# event: deployment - -# # autodeploy staging if it hits the branch -# "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_STAGING} (auto)": -# <<: *K8S_DEPLOY_SETTINGS -# namespace: ${K8S_DEPLOY_NS_STAGING} -# mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_STAGING}/app.deployment.yaml -# when: -# event: *BUILD_EVENTS -# branch: [staging] - -# # promote staging through "drone deploy staging" -# "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_STAGING} (promote)": -# <<: *K8S_DEPLOY_SETTINGS -# namespace: ${K8S_DEPLOY_NS_STAGING} -# mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_STAGING}/app.deployment.yaml -# when: -# environment: staging -# event: deployment - -# # promote production through "drone deploy production" -# "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_PRODUCTION} (promote)": -# <<: *K8S_DEPLOY_SETTINGS -# namespace: ${K8S_DEPLOY_NS_PRODUCTION} -# mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_PRODUCTION}/app.deployment.yaml -# when: -# environment: production -# event: deployment + # # autodeploy dev if it hits the branch + # "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_DEV} (auto)": + # <<: *K8S_DEPLOY_SETTINGS + # namespace: ${K8S_DEPLOY_NS_DEV} + # mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_DEV}/app.deployment.yaml + # when: + # event: *BUILD_EVENTS + # branch: [dev] + + # # promote dev through "drone deploy dev" + # "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_DEV} (promote)": + # <<: *K8S_DEPLOY_SETTINGS + # namespace: ${K8S_DEPLOY_NS_DEV} + # mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_DEV}/app.deployment.yaml + # when: + # environment: dev + # event: deployment + + # # autodeploy staging if it hits the branch + # "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_STAGING} (auto)": + # <<: *K8S_DEPLOY_SETTINGS + # namespace: ${K8S_DEPLOY_NS_STAGING} + # mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_STAGING}/app.deployment.yaml + # when: + # event: *BUILD_EVENTS + # branch: [staging] + + # # promote staging through "drone deploy staging" + # "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_STAGING} (promote)": + # <<: *K8S_DEPLOY_SETTINGS + # namespace: ${K8S_DEPLOY_NS_STAGING} + # mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_STAGING}/app.deployment.yaml + # when: + # environment: staging + # event: deployment + + # # promote production through "drone deploy production" + # "deploy ${DRONE_COMMIT_SHA:0:10} to ${K8S_DEPLOY_NS_PRODUCTION} (promote)": + # <<: *K8S_DEPLOY_SETTINGS + # namespace: ${K8S_DEPLOY_NS_PRODUCTION} + # mgmt_deployment_yaml: namespaces/${K8S_DEPLOY_NS_PRODUCTION}/app.deployment.yaml + # when: + # environment: production + # event: deployment + + # --------------------------------------------------------------------------- + # DEPLOYMENT go-starter + # Purpose: go-starter drone specific publish and deployment steps + # NOTE: you do not need to uncomment them for our customer projects + # These steps won't be executed unless we work in the main "AW/go-starter" repo + # --------------------------------------------------------------------------- + + "go-starter publish ${DRONE_BRANCH}_${DRONE_COMMIT_SHA:0:10}": + group: go-starter-publish + image: plugins/gcr + repo: a3cloud-192413/${DRONE_REPO,,} + registry: eu.gcr.io + secrets: + - source: AAA_GCR_SERVICE_ACCOUNT_JSON + target: google_credentials + # local short-time-cache: don't cleanup any image layers after pushing + purge: false + # force compress of docker build context + compress: true + volumes: # mount needed to push the already build container + - /var/run/docker.sock:/var/run/docker.sock + tags: + - build_${DRONE_BUILD_NUMBER} + - ${DRONE_BRANCH/\//-}_${DRONE_COMMIT_SHA:0:10} + - *IMAGE_DEPLOY_TAG + - latest + - ${DRONE_BRANCH/\//-} + - "${DRONE_COMMIT_SHA:0:10}" + when: + repo: AW/go-starter + branch: [master, mr/a3cloud, mr/liveness-probing] + event: [push, pull_request, tag] + + "go-starter deploy ${DRONE_COMMIT_SHA:0:10} to allaboutapps-go-starter-dev (auto)": + group: go-starter-deploy + image: eu.gcr.io/a3cloud-192413/aw/aaa-drone-kubernetes:latest + pull: true + secrets: + - source: AAA_K8S_SERVER + target: KUBERNETES_SERVER + - source: AAA_K8S_SERVICE_ACCOUNT_CRT + target: KUBERNETES_CERT + - source: AAA_K8S_SERVICE_ACCOUNT_TOKEN + target: KUBERNETES_TOKEN + - source: AAA_GCR_SERVICE_ACCOUNT_JSON + target: GCR_SERVICE_ACCOUNT + deployment: app + repo: eu.gcr.io/a3cloud-192413/${DRONE_REPO,,} + container: [app] + tag: *IMAGE_DEPLOY_TAG + gcr_service_account_email: drone-ci-a3cloud@a3cloud-192413.iam.gserviceaccount.com + mgmt_repo: https://git.allaboutapps.at/scm/aw/a3cloud-mgmt.git + mgmt_git_email: infrastructure+drone@allaboutapps.at + namespace: allaboutapps-go-starter-dev + mgmt_deployment_yaml: namespaces/allaboutapps-go-starter-dev/app.deployment.yaml + when: + repo: AW/go-starter + branch: [master, mr/a3cloud, mr/liveness-probing] + event: [push, pull_request, tag] # Long living services where the startup order does not matter (otherwise use detach: true) services: @@ -241,7 +419,7 @@ services: - "env | sort" "postgres": - image: postgres:12.2-alpine + image: postgres:12.4-alpine # should be the same version as used in .drone.yml, .github/workflows, Dockerfile and live environment: POSTGRES_DB: *PGDATABASE POSTGRES_USER: *PGUSER @@ -267,4 +445,4 @@ services: "mailhog": image: mailhog/mailhog - <<: *WHEN_BUILD_EVENT \ No newline at end of file + <<: *WHEN_BUILD_EVENT diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..5dbe5311 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + # Enable version updates for gomod + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + + # Enable version updates for Docker + - package-ecosystem: "docker" + # Look for a `Dockerfile` in the `root` directory + directory: "/" + schedule: + interval: "daily" + + # Enable version updates for github-actions + - package-ecosystem: "github-actions" + # Workflow files stored in the + # default location of `.github/workflows` + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index a9c0ef15..a93d1412 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -2,9 +2,10 @@ name: Build & Test on: push: - branches: [master] - pull_request: - branches: [master] + branches: "**" + # pull_request: + # branches: [master] + # types: [opened, reopened] # avoid running twice (on push above), see https://github.com/open-telemetry/opentelemetry-python/issues/1370 env: DOCKER_ENV_FILE: ".github/workflows/docker.env" jobs: @@ -12,7 +13,7 @@ jobs: runs-on: ubuntu-latest services: postgres: - image: postgres:12.2-alpine + image: postgres:12.4-alpine # should be the same version as used in .drone.yml, .github/workflows, Dockerfile and live env: POSTGRES_DB: "development" POSTGRES_USER: "dbuser" @@ -33,23 +34,65 @@ jobs: mailhog: image: mailhog/mailhog steps: - - uses: actions/checkout@v2 - - name: Build the Docker image - run: docker build --target builder --file Dockerfile --tag allaboutapps/go-starter:${GITHUB_SHA:8} . - - name: Create container - run: docker run -d --env-file $DOCKER_ENV_FILE --network "${{ job.services.postgres.network }}" --name=builder -it allaboutapps/go-starter:${GITHUB_SHA:8} - - name: make tidy - run: docker exec builder make tidy - - name: make build - run: docker exec builder make build - # - name: git diff --exit-code - # run: docker exec builder git diff --exit-code - - name: make test + - uses: actions/checkout@v2.3.4 + - name: docker build (target builder) + run: docker build --target builder --file Dockerfile --tag allaboutapps.dev/aw/go-starter:builder-${GITHUB_SHA} . + - name: docker build (target app) + run: docker build --target app --file Dockerfile --tag allaboutapps.dev/aw/go-starter:app-${GITHUB_SHA} . + - name: trivy scan + uses: aquasecurity/trivy-action@master + with: + image-ref: 'allaboutapps.dev/aw/go-starter:app-${{ github.sha }}' + format: 'template' + template: '@/contrib/sarif.tpl' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH' + ignore-unfixed: true + - name: docker run (target builder) + run: docker run -d --env-file $DOCKER_ENV_FILE --network "${{ job.services.postgres.network }}" --name=builder -it allaboutapps.dev/aw/go-starter:builder-${GITHUB_SHA} + - name: "build & diff" + # Note builder stage now includes .git, thus we rm it again to again diff with the original git workspace + run: | + docker exec builder make tidy + docker exec builder make build + docker cp builder:/app ./post-build && rm -rf ./post-build/.git && git -C post-build diff --exit-code + - name: test run: docker exec builder make test + - name: upload coverage to codecov + run: docker cp builder:/tmp/coverage.out ./coverage.out && bash <(curl -s https://codecov.io/bash) + - name: test-scripts (gsdev, go-starter only) + if: ${{ github.repository == 'allaboutapps/go-starter' }} + run: docker exec builder make test-scripts + - name: info + run: docker exec builder make info + - name: "binary: deps" + run: docker exec builder bash -c 'make get-embedded-modules-count && make get-embedded-modules' + - name: "binary: licenses" + run: docker exec builder make get-licenses + - name: docker run (target app) + run: | + docker run --env-file $DOCKER_ENV_FILE --network "${{ job.services.postgres.network }}" allaboutapps.dev/aw/go-starter:app-${GITHUB_SHA} help + docker run --env-file $DOCKER_ENV_FILE --network "${{ job.services.postgres.network }}" allaboutapps.dev/aw/go-starter:app-${GITHUB_SHA} -v + docker run --env-file $DOCKER_ENV_FILE --network "${{ job.services.postgres.network }}" allaboutapps.dev/aw/go-starter:app-${GITHUB_SHA} env + - name: upload trivy scan results to GitHub security tab + # Currently limited to master because of the following: + # Workflows triggered by Dependabot on the "push" event run with read-only access. Uploading Code Scanning results requires write access. + # To use Code Scanning with Dependabot, please ensure you are using the "pull_request" event for this workflow and avoid triggering on the "push" event for Dependabot branches. + # See https://docs.github.com/en/code-security/secure-coding/configuring-code-scanning#scanning-on-push for more information on how to configure these events. + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + uses: github/codeql-action/upload-sarif@v1 + with: + sarif_file: 'trivy-results.sarif' + - name: stop container + if: ${{ always() }} + run: docker stop builder + - name: remove container + if: ${{ always() }} + run: docker rm builder swagger-codegen-cli: runs-on: ubuntu-latest container: swaggerapi/swagger-codegen-cli steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2.3.4 - name: run the main swagger.yml validation run: java -jar /opt/swagger-codegen-cli/swagger-codegen-cli.jar validate -i ./api/swagger.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..c4052d6b --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + # Currently limited to master because of the following: + # Workflows triggered by Dependabot on the "push" event run with read-only access. Uploading Code Scanning results requires write access. + # To use Code Scanning with Dependabot, please ensure you are using the "pull_request" event for this workflow and avoid triggering on the "push" event for Dependabot branches. + # See https://docs.github.com/en/code-security/secure-coding/configuring-code-scanning#scanning-on-push for more information on how to configure these events. + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: '0 19 * * 5' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: ['go'] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2.3.4 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.gitignore b/.gitignore index 16f50102..93dc518c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,5 @@ tmp # directory for rw files (PV mounted into the container) # /assets/mnt folder should stay gitignored! assets/mnt/** -!assets/mnt/.gitkeep \ No newline at end of file +!assets/mnt/.gitkeep +dumps \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 4d2cde38..1cb39bd8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,23 @@ +run: + # also lint files within /scripts. Those have "//go:build scripts" set. + build-tags: + - scripts linters: enable: # https://github.com/golangci/golangci-lint#enabled-by-default-linters - # additional linter you want to activate may be specified here... - - golint # official golang linter \ No newline at end of file + # Additional linters you want to activate may be specified here... + + # --- + # https://github.com/mgechev/revive + # # replacement for the now deprecated official golint linter, see https://github.com/golang/go/issues/38968 + - revive + + # --- + # https://github.com/maratori/testpackage + # used to enforce blackbox testing + - testpackage + + # --- + # https://github.com/securego/gosec + # inspects source code for security problems by scanning the Go AST. + - gosec diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 00000000..af57e18d --- /dev/null +++ b/.trivyignore @@ -0,0 +1,2 @@ +# Allow https://nvd.nist.gov/vuln/detail/CVE-2020-26160 (JWT unused, still waiting for child deps upgrade) +CVE-2020-26160 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 05c19ebc..c5d24671 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,6 +23,17 @@ "program": "${fileDirname}", "env": {}, "args": [] + }, + { + "name": "Launch file and update snapshots", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "env": { + "TEST_UPDATE_GOLDEN": true + }, + "args": [] } ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..55d84366 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,529 @@ +# Changelog + +- All notable changes to this project will be documented in this file. +- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +- We do not follow [semantic versioning](https://semver.org/). +- All changes are solely **tracked by date** and have a git tag available (from 2021-10-19 onwards): + - format `go-starter-YYYY-MM-DD` + - e.g. [`go-starter-2021-10-19`](https://github.com/allaboutapps/go-starter/releases/tag/go-starter-2021-10-19) +- The latest `master` is considered **stable** and should be periodically merged into our customer projects. + +## Unreleased + +### Changed + +## 2021-10-22 + +### Changed + +- Fixes minor `Makefile` typos. +- New go-starter releases are now git tagged (starting from the previous release `go-starter-2021-10-19` onwards). See [FAQ: What's the process of a new go-starter release?](https://github.com/allaboutapps/go-starter/wiki/FAQ#whats-the-process-of-a-new-go-starter-release) +- You may now specify a **specific** tag/branch/commit from the upstream [go-starter](https://github.com/allaboutapps/go-starter) project while running `make git-fetch-go-starter`, `make git-compare-go-starter` and `make git-merge-go-starter`. This will especially come in handy if you want to do a multi-phased merge (for projects that haven't been updated in a long time): + - Merge with the latest: `make git-merge-go-starter` + - Merge with a specific tag, e.g. the tag [`go-starter-2021-10-19`](https://github.com/allaboutapps/go-starter/releases/tag/go-starter-2021-10-19): `GIT_GO_STARTER_TARGET=go-starter-2021-10-19 make git-merge-go-starter` + - Merge with a specific branch, e.g. the branch [`mr/housekeeping`](https://github.com/allaboutapps/go-starter/tree/mr/housekeeping): `GIT_GO_STARTER_TARGET=go-starter/mr/housekeeping make git-merge-go-starter` (heads up! it's `go-starter/`) + - Merge with a specific commit, e.g. the commit [`e85bedb94c3562602bc23d2bfd09fca3b13d1e02`](https://github.com/allaboutapps/go-starter/commit/e85bedb94c3562602bc23d2bfd09fca3b13d1e02): `GIT_GO_STARTER_TARGET=e85bedb94c3562602bc23d2bfd09fca3b13d1e02 make git-merge-go-starter` +- The primary GitHub Action pipeline `.github/workflows/build-test.yml` has been synced to include most validation tasks from our internal `.drone.yml` pipeline. Furthermore: + - Avoid `Build & Test` GitHub Action running twice (on `push` and on `pull_request`). + - Add trivy scan to our base Build & Test pipeline (as we know also build and test the `app` target docker image). + - Our GitHub Action pipeline will no longer attempt to cache the previously built Docker images by other pipelines, as extracting/restoring from cache (docker buildx) typically takes **longer** than fully rebuilding the whole image. We will reinvestigate caching mechanisms in the future if GitHub Actions provides a speedier and official integration for Docker images. + +## 2021-10-19 + +### Changed + +- **BREAKING** Upgrades to [Go 1.17.1](https://golang.org/doc/go1.17) `golang:1.17.1-buster` + - Switch to `//go:build ` from `// +build `. + - Migrates `go.mod` via `go mod tidy -go=1.17` (pruned module graphs). + - Do the following to upgrade: + 1. `make git-merge-go-starter` + 2. `./docker-helper --rebuild` + 3. Manually remove the new **second** `require` block (with all the `// indirect` modules) within your `go.mod` + 4. Execute `go mod tidy -go=1.17` once so the **second** `require` block appears again. + 5. Find `// +build ` and replace it with `//go:build `. + 6. `make all`. + 7. Recheck your `go.mod` that the newly added `// indirect` transitive dependencies are the proper version as you were previously using (e.g. via the output from `make get-licenses` and `make get-embedded-modules`). Feel free to move any `// indirect` tagged dependencies in your **first** `require` block to the **second** block. This is where they should live. +- **BREAKING** You now need to take special care when it comes to parsing **semicolons** (`;`) in **query strings** via `net/url` and `net/http` from Go >1.17! + - Anything before the semicolon will now be stripped. e.g. `example?a=1;b=2&c=3` would have returned `map[a:[1] b:[2] c:[3]]`, while now it returns `map[c:[3]]` + - See [Go 1.17 URL query parsing](https://golang.org/doc/go1.17#semicolons). + - You may need to manually migrate your handlers/tests regarding this new default handling. + +## 2021-09-27 + +### Changed + +- Added `make test-update-golden` for easily refreshing **all** golden files / snapshot tests (`y + ENTER` confirmation). +- Upgrades [golangci-lint](https://github.com/golangci/golangci-lint) from `v1.41.1` to [`v1.42.1`](https://github.com/golangci/golangci-lint/releases/tag/v1.42.1) (for reference [`v1.42.0`](https://github.com/golangci/golangci-lint/releases/tag/v1.42.0)). +- Bump github.com/go-openapi/strfmt from [0.20.1 to 0.20.2](https://github.com/go-openapi/strfmt/compare/v0.20.1...v0.20.2) +- Bump github.com/go-openapi/errors from [0.20.0 to 0.20.1](https://github.com/go-openapi/errors/compare/v0.20.0...v0.20.1) +- Bump github.com/go-openapi/runtime from [0.19.29 to 0.19.31](https://github.com/go-openapi/runtime/compare/v0.19.29...v0.19.31) +- Bump github.com/rs/zerolog from [1.23.0 to 1.25.0](https://github.com/rs/zerolog/compare/v1.23.0...v1.25.0) +- Bump google.golang.org/api from [0.52.0 to 0.57.0](https://github.com/allaboutapps/go-starter/pull/124) +- Bump github.com/lib/pq from [v1.10.2 to v1.10.3](https://github.com/lib/pq/releases/tag/v1.10.3) +- Bump github.com/spf13/viper from [1.8.1 to v1.9.0](https://github.com/spf13/viper/releases/tag/v1.9.0) +- Bump github.com/labstack/echo from [4.5.0 to v4.6.1](https://github.com/labstack/echo/compare/v4.5.0...v4.6.1) +- Update golang.org/x/crypto and golang.org/x/sys + +## 2021-08-17 + +### Changed + +- **Hotfix**: We will pin the `Dockerfile` development and builder stage to `golang:1.16.7-buster` (+ `-buster`) for now, as currently the [new debian bullseye release within the go official docker images](https://github.com/docker-library/golang/commit/48a7371ed6055a97a10adb0b75756192ad5f1c97) breaks some tooling. The upgrade to debian bullseye and Go 1.17 will happen ~simultaneously~ **separately** within go-starter in the following weeks. + +## 2021-08-16 + +### Changed + +- remove ioutil (https://golang.org/doc/go1.16#ioutil) + +## 2021-08-06 + +### Changed + +- Bump golang from 1.16.6 to [1.16.7](https://github.com/golang/go/issues?q=milestone%3AGo1.16.7+label%3ACherryPickApproved) (requires `./docker-helper.sh --rebuild`). +- Adds `util.GetEnvAsStringArrTrimmed` and minor `util` test coverage upgrades. + +## 2021-08-04 + +### Changed + +- `README.md` badges for go-starter. +- Fix some misspellings of English words within `internal/test/*.go` comments. +- Upgrades + - Bump `github.com/labstack/echo/v4` from 4.4.0 to [4.5.0](https://github.com/labstack/echo/blob/master/CHANGELOG.md#v450---2021-08-01): + - Switch from `github.com/dgrijalva/jwt-go` to [`github.com/golang-jwt/jwt`](https://github.com/golang-jwt/jwt) to mitigate [CVE-2020-26160](https://nvd.nist.gov/vuln/detail/CVE-2020-26160). + - Note that it might take some time until the former dep fully leaves our dependency graph, as it is also a transitive dependency of various versions of [`github.com/spf13/viper`](https://github.com/spf13/viper/issues/997). + - However, even though this functionality was never used by go-starter, this change fixes an important part: The original `github.com/dgrijalva/jwt-go` is no longer included in the **final `app` binary**, it is fully replaced by `github.com/golang-jwt/jwt`. + - Our `.trivyignore` still excludes [CVE-2020-26160](https://nvd.nist.gov/vuln/detail/CVE-2020-26160) as trivy cannot skip checking transitive dependencies. + - **Breaking**: If you have actually directly depended upon `github.com/dgrijalva/jwt-go`, please switch to `github.com/golang-jwt/jwt` via the following command: `find -type f -name "*.go" -exec sed -i "s/dgrijalva\/jwt-go/golang-jwt\/jwt/g" {} \;` + +## 2021-07-30 + +### Changed + +- Upgrades: + - Bump golang from 1.16.5 to [1.16.6](https://groups.google.com/g/golang-announce/c/n9FxMelZGAQ) + - Bump github.com/labstack/echo/v4 from 4.3.0 to [4.4.0](https://github.com/labstack/echo/blob/master/CHANGELOG.md) (adds `binder.BindHeaders` support, not affecting our goswagger `runtime.Validatable` bind helpers) + - Bump github.com/gabriel-vasile/mimetype from 1.3.0 to [1.3.1](https://github.com/gabriel-vasile/mimetype/releases/tag/v1.3.1) + - Bump github.com/spf13/cobra from 1.1.3 to [1.2.1](https://github.com/spf13/cobra/releases/tag/v1.2.1) (and see all the big completion upgrades in [1.2.0](https://github.com/spf13/cobra/releases/tag/v1.2.0)) + - Bump google.golang.org/api from 0.49.0 to [0.52.0](https://github.com/allaboutapps/go-starter/pull/106) + - Bump gotestsum to [1.7.0](https://github.com/gotestyourself/gotestsum/releases/tag/v1.7.0) (adds handy keybindings while you are in `make watch-tests` mode, see [While in watch mode, pressing some keys will perform an action](https://github.com/gotestyourself/gotestsum#run-tests-when-a-file-is-saved)) + - Bump watchexec to [1.17.0](https://github.com/watchexec/watchexec/releases/tag/cli-v1.17.0) + - Bump golang.org/x/crypto to `v0.0.0-20210711020723-a769d52b0f97` + +## 2021-07-29 + +### Changed + +- Fixed `Makefile` has disregarded `pipefail`s in executed targets (e.g. `make sql-spec-migrate` previously returned exit code `0` even if there were migration errors as its output was piped internally). We now set `-cEeuo pipefail` for make's shell args, preventing these issues. + +## 2021-06-30 + +### Changed + +- **BREAKING** Switched from [`golint`](https://github.com/golang/lint) to [`revive`](https://github.com/mgechev/revive) + - [`golint` is deprecated](https://github.com/golang/go/issues/38968). + - [`revive`](https://github.com/mgechev/revive) is considered to be a drop-in replacement for `golint`, however this change still might lead to breaking changes in your codebase. +- **BREAKING** `make lint` no longer uses `--fast` when calling `golangci-lint` + - Up until now, `make lint` also ran `golangci-lint` using the `--fast` flag to remain consistent with the linting performed by VSCode automatically. + - As running only fast linters in both steps meant skipping quite a few validations (only 4/13 enabled linters are actually active), a decision has been made to break consistency between the two lint-steps and perform "full" linting during the build pipeline. + - This change could potentially bring up additional warnings and thus fail your build until fixed. +- **BREAKING** `gosec` is now also applied to test packages + - All linters are now applied to every source code file in this project, removing the previous exclusion of `gosec` from test files/packages + - As `gosec` might (incorrectly) detect some hardcoded credentials in your tests (variable names such as `passwordResetLink` get flagged), this change might require some fixes after merging. +- Extended auth middleware to allow for multiple auth token sources + - Default token validator uses access token table, maintaining previous behavior without any changes required. + - Token validator can be changed to e.g. use separate API keys for specific endpoints, allowing for more flexibility if so desired. +- Changed `util.LogFromContext` to always return a valid logger + - Helper no longer returns a disabled logger if context provided did not have an associated logger set (e.g. by middleware). If you still need to disable the logger for a certain context/function, use `util.DisableLogger(ctx, true)` to force-disable it. + - Added request ID to context in logger middleware. +- Extended DB query helpers + - Fixed TSQuery escaping, should now properly handle all type of user input. + - Implemented helper for JSONB queries (see `ExampleWhereJSON` for implementation details). + - Added `LeftOuterJoin` helper, similar to already existing `LeftJoin` variants. + - Managed transactions (via `WithTransaction`) can now have their options configured via `WithConfiguredTransaction`. + - Added util to combine query mods with `OR` expression. +- Implemented middleware for parsing `Cache-Control` header + - Allows for cache handling in relevant services, parsed directive is stored in request context. + - New middleware is enabled by default, can be disabled via env var (`SERVER_ECHO_ENABLE_CACHE_CONTROL_MIDDLEWARE`). +- Added extra misc. helpers + - Extra helpers for slice handling and generating random strings from a given character set have been included (`util.ContainsAllString`, `util.UniqueString`, `util.GenerateRandomString`). + - Added util to check whether current execution runs inside a test environment (`util.RunningInTest`). +- Test and snapshot util improvements + - Added `snapshoter.SaveU` as a shorthand for updating a single test + - Implemented `GenericArrayPayload` with respective request helpers for array payloads in tests + - Added VScode launch task for updating all snapshots in a single test file + +## 2021-06-29 + +### Changed + +- We now directly bake the `gsdev` cli "bridge" (it actually just runs `go run -tags scripts /app/scripts/main.go "$@"`) into the `development` stage of our `Dockerfile` and create it at `/usr/bin/gsdev` (requires `./docker-helper.sh --rebuild`). + - `gsdev` was previously symlinked to `/app/bin` from `/app/scripts/gsdev` (within the projects' workspace) and `chmod +x` via the `Makefile` during `init`. + - However this lead to problems with WSL2 VSCode related development setups (always dirty git workspaces as WSL2 tries to prevent `+x` flags). + - **BREAKING** encountered at **2021-06-30**: Upgrading your project via `make git-merge-go-starter` if you already have installed our previous `gsdev` approach from **2021-06-22** may require additional steps: + - It might be necessary to unlink the current `gsdev` symlink residing at `/app/bin/gsdev` before merging up (as this symlinked file will no longer exist)! + - Do this by issuing `rm -f /app/bin/gsdev` which will remove the symlink which pointed to the previous (now gone bash script) at `/app/scripts/gsdev`. + - It might also be handy to install the newer variant directly into your container (without requiring a image rebuild). Do this by: + - `sudo su` to become root in the container, + - issuing the following command: `printf '#!/bin/bash\nset -Eeo pipefail\ncd /app && go run -tags scripts ./scripts/main.go "$@"' > /usr/bin/gsdev && chmod 755 /usr/bin/gsdev` (in sync with what we do in our `Dockerfile`) and + - `[CTRL + c]` to return to being the `development` user within your container. + +## 2021-06-24 + +### Changed + +- Introduces GitHub Actions docker layer caching via docker buildx. For details see `.github/workflows/build-test.yml`. +- Upgrades: + - Bump golang from 1.16.4 to [1.16.5](https://groups.google.com/g/golang-announce/c/RgCMkAEQjSI/m/r_EP-NlKBgAJ) + - golangci-lint@[v1.41.1](https://github.com/golangci/golangci-lint/releases/tag/v1.41.1) + - Bump github.com/rs/zerolog from 1.22.0 to [1.23.0](https://github.com/allaboutapps/go-starter/pull/92) + - Bump github.com/go-openapi/runtime from 0.19.28 to 0.19.29 + - Bump github.com/volatiletech/sqlboiler/v4 from 4.5.0 to [4.6.0](https://github.com/volatiletech/sqlboiler/blob/HEAD/CHANGELOG.md#v460---2021-06-06) + - Bump github.com/rubenv/sql-migrate v0.0.0-20210408115534-a32ed26c37ea to v0.0.0-20210614095031-55d5740dbbcc + - Bump github.com/spf13/viper v1.7.1 to v1.8.0 + - Bump golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a to v0.0.0-20210616213533-5ff15b29337e + - Bump golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea to v0.0.0-20210616094352-59db8d763f22 + - Bump google.golang.org/api v0.47.0 to v0.49.0 +- Fixes linting within `/scripts/**/*.go`, now activated by default. + +## 2021-06-22 + +### Changed + +- Development scripts are no longer called via `go run [script]` but via `gsdev`: + - The `gsdev` cli is our new entrypoint for development workflow specific scripts, these scripts are not available in the final `app` binary. + - All previous `go run` scripts have been moved to their respective `/scripts/cmd` cli entrypoint + internal implementation within `/scripts/internal/**`. + - Please use `gsdev --help` to get an overview of available development specific commands. + - `gsdev` relys on a tiny helper bash script `scripts/gsdev` which gets symlinked to `/app/bin` on `make init`. + - Use `make test-scripts` to run tests regarding these internal scripts within `/scripts/**/*_test.go`. + - We now enforce that all `/scripts/**/*.go` files set the `// +build scripts` build tag. We do this to ensure these files are not directly depended upon from the actual `app` source-code within `/internal`. +- VSCode's `.devcontainer/devcontainer.json` now defines that the go tooling must use the `scripts` build tag for its IntelliSense. This is neccessary to still get proper code-completion when modifying resources at `/scripts/**/*.go`. You may need to reattach VSCode and/or run `./docker-helper.sh --rebuild`. + +### Added + +- Scaffolding tool to quickly generate generic CRUD endpoint stubs. Usage: `gsdev scaffold [resource name] [flags]`, also see `gsdev scaffold --help`. + +## 2021-05-26 + +### Changed + +- Scans for [CVE-2020-26160](https://nvd.nist.gov/vuln/detail/CVE-2020-26160) also match for our final `app` binary, however, we do not use `github.com/dgrijalva/jwt-go` as part of our auth logic. This dependency is mostly here because of child dependencies, that yet need to upgrade to `>=v4.0.0`. Therefore, we currently disable this CVE for scans in this project (via `.trivyignore`). +- Upgrades `Dockerfile`: [`watchexec@v1.16.1`](https://github.com/watchexec/watchexec/releases/tag/cli-v1.16.1), [`lichen@v0.1.4`](https://github.com/uw-labs/lichen/releases/tag/v0.1.4) (requires `./docker-helper.sh --rebuild`). + +## 2021-05-18 + +### Changed + +- Upgraded `Dockerfile` to `golang:1.16.4`, `gotestsum@v1.6.4`, `golangci-lint@v1.40.1`, `watchexec@v1.16.0` (requires `./docker-helper.sh --rebuild`). +- Upgraded `go.mod`: + - [github.com/labstack/echo/v4@v4.3.0](https://github.com/labstack/echo/releases/tag/v4.3.0) + - [github.com/lib/pq@v1.10.2](https://github.com/lib/pq/releases/tag/v1.10.2) + - [github.com/gabriel-vasile/mimetype@v1.3.0](https://github.com/gabriel-vasile/mimetype/releases/tag/v1.3.0) + - `github.com/go-openapi/runtime@v0.19.28` + - [github.com/rs/zerolog@v1.22.0](https://github.com/rs/zerolog/releases/tag/v1.22.0) + - `github.com/rubenv/sql-migrate@v0.0.0-20210408115534-a32ed26c37ea` + - `golang.org/x/crypto@v0.0.0-20210513164829-c07d793c2f9a` + - `golang.org/x/sys@v0.0.0-20210514084401-e8d321eab015` + - [google.golang.org/api@v0.46.0](https://github.com/googleapis/google-api-go-client/releases/tag/v0.46.0) +- GitHub Actions: + - Pin to `actions/checkout@v2.3.4`. + - Remove unnecessary `git checkout HEAD^2` in CodeQL step (Code Scanning recommends analyzing the merge commit for best results). + - Limit trivy and codeQL actions to `push` against `master` and `pull_request` against `master` to overcome read-only access workflow errors. + +## 2021-04-27 + +### Added + +- Adds `test.WithTestDatabaseFromDump*`, `test.WithTestServerFromDump` methods for writing tests based on a database dump file that needs to be imported first: + - We dynamically setup IntegreSQL pools for all combinations passed through a `test.DatabaseDumpConfig{}` object: + - `DumpFile string` is required, absolute path to dump file + - `ApplyMigrations bool` optional, default `false`, automigrate after installing the dump + - `ApplyTestFixtures bool` optional, default `false`, import fixtures after (migrating) installing the dump + - `test.ApplyDump(ctx context.Context, t *testing.T, db *sql.DB, dumpFile string) error` may be used to apply a dump to an existing database connection. + - As we have dedicated IntegreSQL pools for each combination, testing performance should be on par with the default IntegreSQL database pool. +- Adds `test.WithTestDatabaseEmpty*` methods for writing tests based on an empty database (also a dedicated IntegreSQL pool). +- Adds context aware `test.WithTest*Context` methods reusing the provided `context.Context` (first arg). +- Adds `make sql-dump` command to easily create a dump of the local `development` database to `/app/dumps/development_YYYY-MM-DD-hh-mm-ss.sql` (.gitignored). + +### Changed + +- `test.ApplyMigrations(t *testing.T, db *sql.DB) (countMigrations int, err error)` is now public (e.g. for usage with `test.WithTestDatabaseEmpty*` or `test.WithTestDatabaseFromDump*`) +- `test.ApplyTestFixtures(ctx context.Context, t *testing.T, db *sql.DB) (countFixtures int, err error)` is now public (e.g. for usage with `test.WithTestDatabaseEmpty*` or `test.WithTestDatabaseFromDump*`) +- `internal/test/test_database_test.go` and `/app/internal/test/test_server_test.go` were massively refactored to allow for better extensibility later on (non breaking, all method signatures are backward-compatible). + +## 2021-04-12 + +### Added + +- Adds echo `NoCache` middleware: Use `middleware.NoCache()` and `middleware.NoCacheWithConfig(Skipper)` to explicitly force browsers to never cache calls to these handlers/groups. + +### Changed + +- `/swagger.yml` and `/-/*` now explicity set no-cache headers by default, forcing browsers to re-execute calls each and every time. +- Upgrade [watchexec@v1.15.0](https://github.com/watchexec/watchexec/releases/tag/1.15.0) (requires `./docker-helper.sh --rebuild`). + +## 2021-04-08 + +### Added + +- Live-Reload for our swagger-ui is now available out of the box: + - [allaboutapps/browser-sync](https://hub.docker.com/r/allaboutapps/browser-sync) acts as proxy at [localhost:8081](http://localhost:8081/). + - Requires `./docker-helper.sh --up`. + - Best used in combination with `make watch-swagger` (still refreshes `make all` or `make swagger` of course). + +### Changed + +- Upgrades to [swaggerapi/swagger-ui:v3.46.0](https://github.com/swagger-api/swagger-ui/tree/v3.46.0) from [swaggerapi/swagger-ui:v3.28.0](https://github.com/swagger-api/swagger-ui/compare/v3.28.0...v3.46.0) +- Upgrades to [github.com/labstack/echo@v4.2.2](https://github.com/labstack/echo/releases/tag/v4.2.2) +- `golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2` +- Upgrades to [google.golang.org/api@v0.44.0](https://github.com/googleapis/google-api-go-client/releases/tag/v0.44.0) + +## 2021-04-07 + +### Changed + +- Moved `/api/main.yml` to `/api/config/main.yml` to overcome path resolve issues (`../definitions`) with the VSCode [42crunch.vscode-openapi](https://github.com/42Crunch/vscode-openapi) extension (auto-included in our devContainer) and our go-swagger concat behaviour. +- Updated [api/README.md](https://github.com/allaboutapps/go-starter/blob/master/api/README.md) information about `/api/swagger.yml` generation logic and changed `make swagger-concat` accordingly + +## 2021-04-02 + +### Changed + +- Bump [golang from v1.16.2 to v1.16.3](https://github.com/golang/go/issues?q=milestone%3AGo1.16.3+label%3ACherryPickApproved) (requires `./docker-helper.sh --rebuild`). + +## 2021-04-01 + +### Changed + +- Bump golang.org/x/crypto@v0.0.0-20210322153248-0c34fe9e7dc2 +- Bump golang.org/x/sys@v0.0.0-20210331175145-43e1dd70ce54 +- Bump [github.com/go-openapi/swag@v0.19.15](https://github.com/allaboutapps/go-starter/pull/71) +- Bump [github.com/go-openapi/strfmt@v0.20.1](https://github.com/allaboutapps/go-starter/pull/70) + +## 2021-03-30 + +### Changed + +- Bump [github.com/gotestyourself/gotestsum@v1.6.3](https://github.com/gotestyourself/gotestsum/releases/tag/v1.6.3) (requires `./docker-helper.sh --rebuild`). + +## 2021-03-26 + +### Changed + +- Bump [golangci-lint@v1.39.0](https://github.com/golangci/golangci-lint/releases/tag/v1.39.0) (requires `./docker-helper.sh --rebuild`). + +## 2021-03-25 + +### Changed + +- Bump github.com/rs/zerolog from [1.20.0 to 1.21.0](https://github.com/allaboutapps/go-starter/pull/69) +- Bump google.golang.org/api from [0.42.0 to 0.43.0](https://github.com/allaboutapps/go-starter/pull/68) + +## 2021-03-24 + +### Changed + +- We no longer do explicit calls to `t.Parallel()` in our go-starter tests (except autogenerated code). For the reasons why see [FAQ: Should I use `t.Parallel()` in my tests?](https://github.com/allaboutapps/go-starter/wiki/FAQ#should-i-use-tparallel-in-my-tests). +- Switched to [github.com/uw-labs/lichen](https://github.com/uw-labs/lichen) for getting license information of embedded dependencies in our final `./bin/app` binary. +- The following make targets are no longer flagged as `(opt)` and thus move into the main `make help` target (use `make help-all` to see all targets): + - `make lint`: Runs golangci-lint and make check-\*. + - `make go-test-print-slowest`: Print slowest running tests (must be done after running tests). + - `make get-licenses`: Prints licenses of embedded modules in the compiled bin/app. + - `make get-embedded-modules`: Prints embedded modules in the compiled bin/app. + - `make clean`: Cleans ./tmp and ./api/tmp folder. + - `make get-module-name`: Prints current go module-name (pipeable). +- `make check-gen-dirs` now ignores `.DS_Store` within `/internal/models/**/*` and `/internal/types/**/*` and echo an errors detailing what happened. +- Upgrade to [`github.com/go-openapi/runtime@v0.19.27`](https://github.com/go-openapi/runtime/compare/v0.19.26...v0.19.27) + +## 2021-03-16 + +### Changed + +- `make all` no longer executes `make info` as part of its targets chain. + - It's very common to use `make all` multiple times per day during development and thats fine! However, the output of `make info` is typically ignored by our engineers (if they explicitly want this information, they use `make info`). So `make all` was just too spammy in it's previous form. + - `make info` does network calls and typically takes around 5sec to execute. This slowdown is not acceptable when running `make all`, especially if the information it provides isn't used anyways. + - Thus: Just trigger `make info` manually if you need the information of the `[spec DB]` structure, current `[handlers]` and `[go.mod]` information. Furthermore you may also visit `tmp/.info-db`, `tmp/.info-handlers` and `tmp/.info-go` after triggering `make info` as we store this information there after a run. + +## 2021-03-15 + +### Changed + +- Upgrades `go.mod`: + - [`github.com/volatiletech/sqlboiler/v4@v4.5.0`](https://github.com/volatiletech/sqlboiler/blob/master/CHANGELOG.md#v450---2021-03-14) + - [`github.com/rogpeppe/go-internal@v1.8.0`](https://github.com/rogpeppe/go-internal/releases/tag/v1.8.0) + - `golang.org/x/crypto@v0.0.0-20210314154223-e6e6c4f2bb5b` + - ~`golang.org/x/sys@v0.0.0-20210314195730-07df6a141424`~ + - `golang.org/x/sys@v0.0.0-20210315160823-c6e025ad8005` + - [`google.golang.org/api@v0.42.0`](https://github.com/googleapis/google-api-go-client/releases/tag/v0.42.0) +- `make help` no longer reports `(opt)` flagged targets, use `make help-all` instead. +- `make tools` now executes `go install {}` in parallel +- `make info` now fetches information in parallel +- Seeding: Switch to `db|dbUtil.WithTransaction` instead of manually managing the db transaction. _Note_: We will enforce using `WithTransaction` instead of manually managing the life-cycle of db transactions through a custom linter in an upcoming change. It's way safer and manually managing db transactions only makes sense in very very special cases (where you will be able to opt-out via linter excludes). Also see [What's `WithTransaction`, shouldn't I use `db.BeginTx`, `db.Commit`, and `db.Rollback`?](https://github.com/allaboutapps/go-starter/wiki/FAQ#whats-withtransaction-shouldnt-i-use-dbbegintx-dbcommit-and-dbrollback). + +### Fixed + +- The correct implementation of `(util|scripts).GetProjectRootDir() string` now gets automatically selected based on the `scripts` build tag. + - We currently have 2 different `GetProjectRootDir()` implementations and each one is useful on its own: + - `util.GetProjectRootDir()` gets used while `app` or `go test` runs and resolves in the following way: use `PROJECT_ROOT_DIR` (if set), else default to the resolved path to the executable unless we can't resolve that, then **panic**! + - `scripts.GetProjectRootDir()` gets used while **generation time** (`make go-generate`) and resolves in the following way: use `PROJECT_ROOT_DIR` (if set), otherwise default to `/app` (baked, as we can assume we are in the `development` container). + - `/internal/util/(get_project_root_dir.go|get_project_root_dir_scripts.go)` is now introduced to automatically switch to the proper implementation based on the `// +build !scripts` or `// +build scripts` build tag, thus it's now consistent to import `util.GetProjectRootDir()`, especially while handler generation time (`make go-generate`). + +## 2021-03-12 + +### Changed + +- Upgrades to `golang@v1.16.2` (use `./docker-helper.sh --rebuild`). +- Silence resolve of `GO_MODULE_NAME` if `go` was not found in path (typically host env related). + +## 2021-03-11 + +### Added + +- `make build` (`make go-build`) now sets `internal/config.ModuleName`, `internal/config.Commit` and `internal/config.BuildDate` via `-ldflags`. + - `/-/version` (mgmt key auth) endpoint is now available, prints the same as `app -v`. + - `app -v` is now available and prints out buildDate and commit. Sample: + +```bash +app -v +allaboutapps.dev/aw/go-starter @ 19c4cdd0da151df432cd5ab33c35c8987b594cac (2021-03-11T15:42:27+00:00) +``` + +### Changed + +- Upgrades to `golang@v1.16.1` (use `./docker-helper.sh --rebuild`). +- Updates `google.golang.org/api@v0.41.0`, `github.com/gabriel-vasile/mimetype@v1.2.0` ([new supported formats](https://github.com/gabriel-vasile/mimetype/tree/v1.2.0)), `golang.org/x/sys` +- Removed `**/.git` from `.dockerignore` (`builder` stage) as we want the local git repo available while running `make go-build`. +- `app --help` now prominently includes the module name of the project. +- Prominently recommend `make force-module-name` after running `make git-merge-go-starter` to fix all import paths. + +## 2021-03-09 + +### Added + +- Introduces `CHANGELOG.md` + +### Changed + +- `make git-merge-go-starter` now uses `--allow-unrelated-histories` by default. + - `README.md` and FAQ now mention that it's recommended to execute `make git-merge-go-starter` during project setup (especially for single commit generated from template project project setups). + - See [FAQ: I want to compare or update my project/fork to the latest go-starter master.](https://github.com/allaboutapps/go-starter/wiki/FAQ#i-want-to-compare-or-update-my-projectfork-to-the-latest-go-starter-master) +- Various typos in `README.md` and `Makefile`. +- Upgrade to [`golangci-lint@v1.38.0`](https://github.com/golangci/golangci-lint/releases/tag/v1.38.0) + +## 2021-03-08 + +### Added + +- `allaboutapps/nullable` is now included by default. See [#58](https://github.com/allaboutapps/go-starter/pull/58), [FAQ: I need an optional Swagger payload property that is nullable!](https://github.com/allaboutapps/go-starter/wiki/FAQ#i-need-an-optional-swagger-payload-property-that-is-nullable) + +### Changed + +- Upgrade to [`labstack/echo@v4.2.1`](https://github.com/labstack/echo/releases/tag/v4.2.1), [`lib/pq@v1.10.0`](https://github.com/lib/pq/releases/tag/v1.10.0) + +## 2021-02-23 + +### Deprecated + +- `util.BindAndValidate` is now marked as deprecated as [`labstack/echo@v4.2.0`](https://github.com/labstack/echo/releases/tag/v4.2.0) exposes a more granular binding through its `DefaultBinder`. + +### Added + +- The more specialized variants `util.BindAndValidatePathAndQueryParams` and `util.BindAndValidateBody` are now available. See [`/internal/util/http.go`](https://github.com/allaboutapps/go-starter/blob/master/internal/util/http.go#L87). + +### Changed + +- `golang@v1.16.0` +- [`labstack/echo@v4.2.0`](https://github.com/labstack/echo/releases/tag/v4.2.0) + +## 2021-02-16 + +### Changed + +- Upgrades to [`pgFormatter@v5.0.0`](https://github.com/darold/pgFormatter/releases) + forces VSCode to use that version within the devcontainer through it's extension. + +## 2021-02-09 + +### Changed + +- `golang@v1.15.8`, `go-swagger@v0.26.1` + +## 2021-02-01 + +### Changed + +``` +- Dockerfile updates: + - golang@1.15.7 + - apt add icu-devtools (VSCode live sharing) + - gotestsum@1.6.1 + - golangci-lint@v1.36.0 + - goswagger@v0.26.0 +- go.mod: + - sqlboiler@4.4.0 + - swag@0.19.3 + - strfmt@0.20.0 + - testify@1.7.0 + - go-openapi/runtime@v0.19.26 + - go-openapi/swag@v0.19.13 + - go-openapi/validate@v0.20.1 + - jordan-wright/email + - rogpeppe/go-internal@v1.7.0 + - golang.org/x/crypto + - golang.org/x/sys + - google.golang.org/api@v0.38.0 +``` + +### Fixed + +- disabled goswagger generate server flag `--keep-spec-order` as relative resolution of its temporal created yml file is broken - see https://github.com/go-swagger/go-swagger/issues/2216 + +## 2020-11-04 + +### Added + +- `make watch-swagger` and `make watch-sql` + +### Changed + +- sqlboiler@4.3.0 + +## 2020-11-02 + +### Added + +- `make watch-tests`: Watches .go files and runs package tests on modifications. + +## 2020-09-30 + +### Added + +- `pprof` handlers, see [FAQ: I need to (remotely) pprof my running service!](https://github.com/allaboutapps/go-starter/wiki/FAQ#i-need-to-remotely-pprof-my-running-service) + +## 2020-09-24 + +### Added + +- `make git-merge-go-starter`, see [FAQ: I want to compare or update my project/fork to the latest go-starter master.](https://github.com/allaboutapps/go-starter/wiki/FAQ#i-want-to-compare-or-update-my-projectfork-to-the-latest-go-starter-master) + +## 2020-09-22 + +### Added + +- `app probe readiness` and `app probe liveness` sub-commands. +- `/-/ready` and `/-/healthy` handlers. + +## 2020-09-16 + +### Changed + +- Force VSCode to use our installed version of golang-cilint +- All `*.go` files in `/scripts` now use the build tag `scripts` so we can ensure they are not compiled into the final `app` binary. + +### Added + +- `go.not` file to ensure certain generation- / test-only dependencies don't end up in the final `app` binary. Automatically checked though `make` (sub-target `make check-embedded-modules-go-not`). + +## 2020-09-11 + +- Switch to `distroless` as final app stage, see [FAQ: Should I use distroless/base or debian:buster-slim in the Dockerfile app stage?](https://github.com/allaboutapps/go-starter/wiki/FAQ#should-i-use-distrolessbase-or-debianbuster-slim-in-the-dockerfile-app-stage) diff --git a/Dockerfile b/Dockerfile index 58959acc..910ab1de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,14 @@ ### ----------------------- # --- Stage: development +# --- Purpose: Local development environment # --- https://hub.docker.com/_/golang # --- https://github.com/microsoft/vscode-remote-try-go/blob/master/.devcontainer/Dockerfile ### ----------------------- -FROM golang:1.15.0 AS development +FROM golang:1.17.1-buster AS development # Avoid warnings by switching to noninteractive ENV DEBIAN_FRONTEND=noninteractive -# https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md#walk-through -ENV GOBIN /app/bin -ENV PATH $GOBIN:$PATH - # Our Makefile / env fully supports parallel job execution ENV MAKEFLAGS "-j 8 --no-print-directory" @@ -42,6 +39,8 @@ RUN apt-get update \ # https://github.com/microsoft/vscode-remote-try-go/blob/master/.devcontainer/Dockerfile # https://raw.githubusercontent.com/microsoft/vscode-dev-containers/master/script-library/common-debian.sh # + # icu-devtools: https://stackoverflow.com/questions/58736399/how-to-get-vscode-liveshare-extension-working-when-running-inside-vscode-remote + # graphviz: https://github.com/google/pprof#building-pprof # -- START DEVELOPMENT -- apt-utils \ dialog \ @@ -54,7 +53,10 @@ RUN apt-get update \ sudo \ bash-completion \ bsdmainutils \ + graphviz \ + xz-utils \ postgresql-client-12 \ + icu-devtools \ # --- END DEVELOPMENT --- # && apt-get clean \ @@ -69,15 +71,15 @@ RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ ENV LANG en_US.UTF-8 -# sql pgFormatter: Install the same version of pg_formatter as used in your editors, as of 2020-04 thats v4.3 +# sql pgFormatter: Integrates with vscode-pgFormatter (we pin pgFormatter.pgFormatterPath for the extension to this version) # requires perl to be installed # https://github.com/bradymholt/vscode-pgFormatter/commits/master # https://github.com/darold/pgFormatter/releases RUN mkdir -p /tmp/pgFormatter \ && cd /tmp/pgFormatter \ - && wget https://github.com/darold/pgFormatter/archive/v4.3.tar.gz \ - && tar xzf v4.3.tar.gz \ - && cd pgFormatter-4.3 \ + && wget https://github.com/darold/pgFormatter/archive/v5.0.tar.gz \ + && tar xzf v5.0.tar.gz \ + && cd pgFormatter-5.0 \ && perl Makefile.PL \ && make && make install \ && rm -rf /tmp/pgFormatter @@ -86,8 +88,8 @@ RUN mkdir -p /tmp/pgFormatter \ # https://github.com/gotestyourself/gotestsum/releases RUN mkdir -p /tmp/gotestsum \ && cd /tmp/gotestsum \ - && wget https://github.com/gotestyourself/gotestsum/releases/download/v0.5.2/gotestsum_0.5.2_linux_amd64.tar.gz \ - && tar xzf gotestsum_0.5.2_linux_amd64.tar.gz \ + && wget https://github.com/gotestyourself/gotestsum/releases/download/v1.7.0/gotestsum_1.7.0_linux_amd64.tar.gz \ + && tar xzf gotestsum_1.7.0_linux_amd64.tar.gz \ && cp gotestsum /usr/local/bin/gotestsum \ && rm -rf /tmp/gotestsum @@ -95,14 +97,33 @@ RUN mkdir -p /tmp/gotestsum \ # https://github.com/golangci/golangci-lint#binary # https://github.com/golangci/golangci-lint/releases RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \ - | sh -s -- -b $(go env GOPATH)/bin v1.30.0 + | sh -s -- -b $(go env GOPATH)/bin v1.42.1 # go swagger: (this package should NOT be installed via go get) # https://github.com/go-swagger/go-swagger/releases RUN curl -o /usr/local/bin/swagger -L'#' \ - "https://github.com/go-swagger/go-swagger/releases/download/v0.25.0/swagger_linux_amd64" \ + "https://github.com/go-swagger/go-swagger/releases/download/v0.26.1/swagger_linux_amd64" \ && chmod +x /usr/local/bin/swagger +# lichen: go license util +# TODO: Install from static binary as soon as it becomes available. +# https://github.com/uw-labs/lichen/releases +RUN go install github.com/uw-labs/lichen@v0.1.4 + +# watchexec +# https://github.com/watchexec/watchexec/releases +RUN mkdir -p /tmp/watchexec \ + && cd /tmp/watchexec \ + && wget https://github.com/watchexec/watchexec/releases/download/cli-v1.17.0/watchexec-1.17.0-x86_64-unknown-linux-gnu.tar.xz \ + && tar xf watchexec-1.17.0-x86_64-unknown-linux-gnu.tar.xz \ + && cp watchexec-1.17.0-x86_64-unknown-linux-gnu/watchexec /usr/local/bin/watchexec \ + && rm -rf /tmp/watchexec + +# gsdev +# The sole purpose of the "gsdev" cli util is to provide a handy short command for the following (all args are passed): +# go run -tags scripts /app/scripts/main.go "$@" +RUN printf '#!/bin/bash\nset -Eeo pipefail\ncd /app && go run -tags scripts ./scripts/main.go "$@"' > /usr/bin/gsdev && chmod 755 /usr/bin/gsdev + # linux permissions / vscode support: Add user to avoid linux file permission issues # Detail: Inside the container, any mounted files/folders will have the exact same permissions # as outside the container - including the owner user ID (UID) and group ID (GID). @@ -133,8 +154,18 @@ RUN mkdir -p /home/$USERNAME/.vscode-server/extensions \ # Note that this should be the final step after installing all build deps RUN mkdir -p /$GOPATH/pkg && chown -R $USERNAME /$GOPATH +# $GOBIN is where our own compiled binaries will live and other go.mod / VSCode binaries will be installed. +# It should always come AFTER our other $PATH segments and should be earliest targeted in stage "builder", +# as /app/bin will the shadowed by a volume mount via docker-compose! +# E.g. "which golangci-lint" should report "/go/bin" not "/app/bin" (where VSCode will place it). +# https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md#walk-through +WORKDIR /app +ENV GOBIN /app/bin +ENV PATH $PATH:$GOBIN + ### ----------------------- # --- Stage: builder +# --- Purpose: Statically built binaries and CI environment ### ----------------------- FROM development as builder @@ -146,41 +177,54 @@ RUN make modules COPY tools.go /app/tools.go RUN make tools COPY . /app/ - -### ----------------------- -# --- Stage: builder-app -### ----------------------- - -FROM builder as builder-app RUN make go-build ### ----------------------- # --- Stage: app +# --- Purpose: Image for actual deployment +# --- Prefer https://github.com/GoogleContainerTools/distroless over +# --- debian:buster-slim https://hub.docker.com/_/debian (if you need apt-get). ### ----------------------- -FROM debian:buster-slim as app - -RUN apt-get update \ - && apt-get install -y \ - # - # Mandadory minimal linux packages - # Installed at development stage and app stage - # Do not forget to add mandadory linux packages to the base development Dockerfile stage above! - # - # -- START MANDADORY -- - ca-certificates \ - # --- END MANDADORY --- - # - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -COPY --from=builder-app /app/bin/app /app/bin/sql-migrate /app/ -COPY --from=builder-app /app/dbconfig.yml /app/ -COPY --from=builder-app /app/api/swagger.yml /app/api/ -COPY --from=builder-app /app/assets /app/assets/ -COPY --from=builder-app /app/migrations /app/migrations/ -COPY --from=builder-app /app/web /app/web/ +# Distroless images are minimal and lack shell access. +# https://github.com/GoogleContainerTools/distroless/blob/master/base/README.md +# The :debug image provides a busybox shell to enter (base-debian10 only, not static). +# https://github.com/GoogleContainerTools/distroless#debug-images +FROM gcr.io/distroless/base-debian10:debug as app + +# FROM debian:buster-slim as app +# RUN apt-get update \ +# && apt-get install -y \ +# # +# # Mandadory minimal linux packages +# # Installed at development stage and app stage +# # Do not forget to add mandadory linux packages to the base development Dockerfile stage above! +# # +# # -- START MANDADORY -- +# ca-certificates \ +# # --- END MANDADORY --- +# # +# && apt-get clean \ +# && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /app/bin/app /app/ +COPY --from=builder /app/api/swagger.yml /app/api/ +COPY --from=builder /app/assets /app/assets/ +COPY --from=builder /app/migrations /app/migrations/ +COPY --from=builder /app/web /app/web/ WORKDIR /app -CMD [ "/bin/sh", "-c", "/app/sql-migrate up && /app/app server" ] \ No newline at end of file +# Must comply to vector form +# https://github.com/GoogleContainerTools/distroless#entrypoints +# Sample usage of this image: +# docker run help +# docker run db migrate +# docker run db seed +# docker run env +# docker run probe readiness +# docker run probe liveness +# docker run server +# docker run server --migrate +ENTRYPOINT ["/app/app"] +CMD ["server", "--migrate"] diff --git a/Makefile b/Makefile index 0fb8f376..e818400f 100644 --- a/Makefile +++ b/Makefile @@ -2,58 +2,58 @@ # --- Building ### ----------------------- -# go module name (as in go.mod) -# only evaluated if required by a recipe -# http://make.mad-scientist.net/deferred-simple-variable-expansion/ -GO_MODULE_NAME = $(eval GO_MODULE_NAME := $$(shell \ - (mkdir -p tmp 2> /dev/null && cat tmp/.modulename 2> /dev/null) \ - || (go run scripts/modulename/modulename.go | tee tmp/.modulename) \ -))$(GO_MODULE_NAME) - # first is default target when running "make" without args -build: ##- Default make target: make build-pre, go-format, go-build and lint. +build: ##- Default 'make' target: sql, swagger, go-generate, go-format, go-build and lint. @$(MAKE) build-pre @$(MAKE) go-format @$(MAKE) go-build @$(MAKE) lint # useful to ensure that everything gets resetuped from scratch -all: ##- Runs (pretty much) all targets: make clean, init, build, info and test. - @$(MAKE) clean - @$(MAKE) init +all: clean init ##- Runs all of our common make targets: clean, init, build and test. @$(MAKE) build - @$(MAKE) info @$(MAKE) test -info: ##- Prints database spec, implemented vs. speced handlers, go module-name and current go version. - @echo "database:" - @cat scripts/sql/info.sql | psql -q -d "${PSQL_DBNAME}" - @echo "handlers:" - @go run scripts/handlers/check_handlers.go --print-all - @echo "" - @$(MAKE) info-module-name - @go version +info: info-db info-handlers info-go ##- Prints info about spec db, handlers, and go.mod updates, module-name and current go version. + +info-db: ##- (opt) Prints info about spec db. + @echo "[spec DB]" > tmp/.info-db + @cat scripts/sql/info.sql | psql -q -d "${PSQL_DBNAME}" >> tmp/.info-db + @cat tmp/.info-db + +info-handlers: ##- (opt) Prints info about handlers. + @echo "[handlers]" > tmp/.info-handlers + @gsdev handlers check --print-all >> tmp/.info-handlers + @echo "" >> tmp/.info-handlers + @cat tmp/.info-handlers + +info-go: ##- (opt) Prints go.mod updates, module-name and current go version. + @echo "[go.mod]" > tmp/.info-go + @$(MAKE) get-go-outdated-modules >> tmp/.info-go + @$(MAKE) info-module-name >> tmp/.info-go + @go version >> tmp/.info-go + @cat tmp/.info-go -lint: check-gen-dirs check-handlers go-lint ##- (opt) Lints and checks handler paths match swagger spec. +lint: check-gen-dirs check-script-dir check-handlers check-embedded-modules-go-not go-lint ##- Runs golangci-lint and make check-*. # these recipies may execute in parallel -build-pre: sql-generate swagger ##- (opt) Runs prebuild related targets (sql, swagger, go-generate). +build-pre: sql swagger ##- (opt) Runs pre-build related targets (sql, swagger, go-generate). @$(MAKE) go-generate go-format: ##- (opt) Runs go format. go fmt ./... go-build: ##- (opt) Runs go build. - go build -o bin/app + go build -ldflags $(LDFLAGS) -o bin/app go-lint: ##- (opt) Runs golangci-lint. - golangci-lint run --fast --timeout 5m + golangci-lint run --timeout 5m go-generate: ##- (opt) Generates the internal/api/handlers/handlers.go binding. - go run scripts/handlers/gen_handlers.go + gsdev handlers gen check-handlers: ##- (opt) Checks if implemented handlers match their spec (path). - go run scripts/handlers/check_handlers.go + gsdev handlers check # https://golang.org/pkg/cmd/go/internal/generate/ # To convey to humans and machine tools that code is generated, @@ -62,8 +62,12 @@ check-handlers: ##- (opt) Checks if implemented handlers match their spec (path) # ^// Code generated .* DO NOT EDIT\.$ check-gen-dirs: ##- (opt) Ensures internal/models|types only hold generated files. @echo "make check-gen-dirs" - @grep -R -L '^// Code generated .* DO NOT EDIT\.$$' ./internal/types/ && exit 1 || exit 0 - @grep -R -L '^// Code generated .* DO NOT EDIT\.$$' ./internal/models/ && exit 1 || exit 0 + @grep -R -L '^// Code generated .* DO NOT EDIT\.$$' --exclude ".DS_Store" ./internal/types/ && echo "Error: Non generated file(s) in ./internal/types!" && exit 1 || exit 0 + @grep -R -L '^// Code generated .* DO NOT EDIT\.$$' --exclude ".DS_Store" ./internal/models/ && echo "Error: Non generated file(s) in ./internal/models!" && && exit 1 || exit 0 + +check-script-dir: ##- (opt) Ensures all scripts/**/*.go files have the "//go:build scripts" build tag set. + @echo "make check-script-dir" + @grep -R --include=*.go -L '//go:build scripts' ./scripts && echo "Error: Found unset '//go:build scripts' in ./scripts/**/*.go!" && exit 1 || exit 0 # https://github.com/gotestyourself/gotestsum#format # w/o cache https://github.com/golang/go/issues/24573 - see "go help testflag" @@ -79,6 +83,11 @@ test-by-name: ##- Run tests, output by testname, print coverage. @$(MAKE) go-test-by-name @$(MAKE) go-test-print-coverage +test-update-golden: ##- Refreshes all golden files / snapshot tests by running tests, output by package. + @echo "Attempting to refresh all golden files / snapshot tests (TEST_UPDATE_GOLDEN=true)!" + @echo -n "Are you sure? [y/N] " && read ans && [ $${ans:-N} = y ] + @TEST_UPDATE_GOLDEN=true gotestsum --hide-summary=skipped -- -race -count=1 ./... + # note that we explicitly don't want to use a -coverpkg=./... option, per pkg coverage take precedence go-test-by-pkg: ##- (opt) Run tests, output by package. gotestsum --format pkgname-and-test-fails --jsonfile /tmp/test.log -- -race -cover -count=1 -coverprofile=/tmp/coverage.out ./... @@ -90,18 +99,32 @@ go-test-print-coverage: ##- (opt) Print overall test coverage (must be done afte @printf "coverage " @go tool cover -func=/tmp/coverage.out | tail -n 1 | awk '{$$1=$$1;print}' -go-test-print-slowest: ##- (opt) Print slowest running tests (must be done after running tests). +go-test-print-slowest: ##- Print slowest running tests (must be done after running tests). gotestsum tool slowest --jsonfile /tmp/test.log --threshold 2s +# TODO: switch to "-m direct" after go 1.17 hits: https://github.com/golang/go/issues/40364 +get-go-outdated-modules: ##- (opt) Prints outdated (direct) go modules (from go.mod). + @((go list -u -m -f '{{if and .Update (not .Indirect)}}{{.}}{{end}}' all) 2>/dev/null | grep " ") || echo "go modules are up-to-date." + +watch-tests: ##- Watches *.go files and runs package tests on modifications. + gotestsum --format testname --watch -- -race -count=1 + +test-scripts: ##- (opt) Run scripts tests (gsdev), output by package, print coverage. + @$(MAKE) go-test-scripts-by-pkg + @printf "coverage " + @go tool cover -func=/tmp/coverage-scripts.out | tail -n 1 | awk '{$$1=$$1;print}' + +go-test-scripts-by-pkg: ##- (opt) Run scripts tests (gsdev), output by package. + gotestsum --format pkgname-and-test-fails --jsonfile /tmp/test.log -- $$(go list -tags scripts ./... | grep "${GO_MODULE_NAME}/scripts") -tags scripts -race -cover -count=1 -coverprofile=/tmp/coverage-scripts.out ./... + ### ----------------------- # --- Initializing ### ----------------------- -init: ##- Runs make modules, tools and tidy. +init: ##- Runs make modules, tools and tidy. @$(MAKE) modules @$(MAKE) tools @$(MAKE) tidy - @go version # cache go modules (locally into .pkg) modules: ##- (opt) Cache packages as specified in go.mod. @@ -109,7 +132,7 @@ modules: ##- (opt) Cache packages as specified in go.mod. # https://marcofranssen.nl/manage-go-tools-via-go-modules/ tools: ##- (opt) Install packages as specified in tools.go. - cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI % go install % + @cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -P $$(nproc) -L 1 -tI % go install % tidy: ##- (opt) Tidy our go.sum file. go mod tidy @@ -137,9 +160,11 @@ sql-drop-all: ##- Wizard to drop ALL databases: spec, development and tracked by @echo "Done. Please run 'make sql-reset && make sql-spec-reset && make sql-spec-migrate' to reinitialize." # This step is only required to be executed when the "migrations" folder has changed! -# MIGRATION_FILES = $(find ./migrations/ -type f -iname '*.sql') -sql-generate: ##- (opt) Runs all sql related formats/checks and finally generates internal/models/*.go. +sql: ##- Runs sql format, all sql related checks and finally generates internal/models/*.go. @$(MAKE) sql-format + @$(MAKE) sql-regenerate + +sql-regenerate: ##- (opt) Runs sql related checks and finally generates internal/models/*.go. @$(MAKE) sql-check-files @$(MAKE) sql-spec-reset @$(MAKE) sql-spec-migrate @@ -153,20 +178,24 @@ sql-boiler: ##- (opt) Runs sql-boiler introspects the spec db to generate intern sql-format: ##- (opt) Formats all *.sql files. @echo "make sql-format" - @find ${PWD} -name ".*" -prune -o -type f -iname "*.sql" -print \ + @find ${PWD} -path "*/tmp/*" -prune -name ".*" -prune -o -type f -iname "*.sql" -print \ + | grep --invert "/app/dumps/" \ + | grep --invert "/app/test/" \ | xargs -i pg_format {} -o {} -sql-check-files: sql-check-syntax sql-check-migrations-unnecessary-null ##- (opt) Check syntax and unneccessary use of NULL keyword. +sql-check-files: sql-check-syntax sql-check-migrations-unnecessary-null ##- (opt) Check syntax and unnecessary use of NULL keyword. # check syntax via the real database # https://stackoverflow.com/questions/8271606/postgresql-syntax-check-without-running-the-query sql-check-syntax: ##- (opt) Checks syntax of all *.sql files. @echo "make sql-check-syntax" - @find ${PWD} -name ".*" -prune -o -type f -iname "*.sql" -print \ + @find ${PWD} -path "*/tmp/*" -prune -name ".*" -prune -path ./dumps -prune -false -o -type f -iname "*.sql" -print \ + | grep --invert "/app/dumps/" \ + | grep --invert "/app/test/" \ | xargs -i sed '1s#^#DO $$SYNTAX_CHECK$$ BEGIN RETURN;#; $$aEND; $$SYNTAX_CHECK$$;' {} \ | psql -d postgres --quiet -v ON_ERROR_STOP=1 -sql-check-migrations-unnecessary-null: ##- (opt) Checks migrations/*.sql for unneccessary use of NULL keywords. +sql-check-migrations-unnecessary-null: ##- (opt) Checks migrations/*.sql for unnecessary use of NULL keywords. @echo "make sql-check-migrations-unnecessary-null" @(grep -R "NULL" ./migrations/ | grep --invert "DEFAULT NULL" | grep --invert "NOT NULL" | grep --invert "WITH NULL" | grep --invert "NULL, " | grep --invert ", NULL" | grep --invert "RETURN NULL" | grep --invert "SET NULL") \ && exit 1 || exit 0 @@ -178,7 +207,7 @@ sql-spec-reset: ##- (opt) Drop and creates our spec database. sql-spec-migrate: ##- (opt) Applies migrations/*.sql to our spec database. @echo "make sql-spec-migrate" - @sql-migrate up -env spec + @sql-migrate up -env spec | xargs -i echo "[spec DB]" {} sql-check-structure: sql-check-structure-fk-missing-index sql-check-structure-default-zero-values ##- (opt) Runs make sql-check-structure-*. @@ -190,11 +219,21 @@ sql-check-structure-default-zero-values: ##- (opt) Ensures spec database objects @echo "make sql-check-structure-default-zero-values" @cat scripts/sql/default_zero_values.sql | psql -qtz0 --no-align -d "${PSQL_DBNAME}" -v ON_ERROR_STOP=1 +dumpfile := /app/dumps/development_$(shell date '+%Y-%m-%d-%H-%M-%S').sql +sql-dump: ##- Dumps the development database to '/app/dumps/development_YYYY-MM-DD-hh-mm-ss.sql'. + @mkdir -p /app/dumps + @pg_dump development --format=p --clean --if-exists > $(dumpfile) + @echo "Dumped '$(dumpfile)'. Use 'cat $(dumpfile) | psql' to restore" + +watch-sql: ##- Watches *.sql files in /migrations and runs 'make sql-regenerate' on modifications. + @echo Watching /migrations. Use Ctrl-c to stop a run or exit. + watchexec -p -w migrations --exts sql $(MAKE) sql-regenerate + ### ----------------------- # --- Swagger ### ----------------------- -swagger: ##- (opt) Runs make swagger-concat and swagger-server. +swagger: ##- Runs make swagger-concat and swagger-server. @$(MAKE) swagger-concat @$(MAKE) swagger-server @@ -208,7 +247,7 @@ swagger-concat: ##- (opt) Regenerates api/swagger.yml based on api/paths/*. --output=api/tmp/tmp.yml \ --format=yaml \ --keep-spec-order \ - api/main.yml api/paths/* \ + api/config/main.yml api/paths/* \ -q @swagger flatten api/tmp/tmp.yml \ --output=api/swagger.yml \ @@ -219,6 +258,7 @@ swagger-concat: ##- (opt) Regenerates api/swagger.yml based on api/paths/*. # https://goswagger.io/generate/server.html # Note that we first flag all files to delete (as previously generated), regenerate, then delete all still flagged files # This allows us to ensure that any filewatchers (VScode) don't panic as these files are removed. +# --keep-spec-order is broken (/tmp spec resolving): https://github.com/go-swagger/go-swagger/issues/2216 swagger-server: ##- (opt) Regenerates internal/types based on api/swagger.yml. @echo "make swagger-server" @grep -R -L '^// Code generated .* DO NOT EDIT\.$$$$' ./internal/types \ @@ -231,19 +271,77 @@ swagger-server: ##- (opt) Regenerates internal/types based on api/swagger.yml. --model-package=internal/types \ --exclude-main \ --config-file=api/config/go-swagger-config.yml \ - --keep-spec-order \ -q @find internal/types -type f -exec grep -q '^// DELETE ME; DO NOT EDIT\.$$' {} \; -delete +watch-swagger: ##- Watches *.yml|yaml|gotmpl files in /api and runs 'make swagger' on modifications. + @echo "Watching /api/**/*.yml|yaml|gotmpl. Use Ctrl-c to stop a run or exit." + watchexec -p -w api -i tmp -i api/swagger.yml --exts yml,yaml,gotmpl $(MAKE) swagger + +### ----------------------- +# --- Binary checks +### ----------------------- + +# Got license issues with some dependencies? Provide a custom lichen --config +# see https://github.com/uw-labs/lichen#config +get-licenses: ##- Prints licenses of embedded modules in the compiled bin/app. + lichen bin/app + +get-embedded-modules: ##- Prints embedded modules in the compiled bin/app. + go version -m -v bin/app + +get-embedded-modules-count: ##- (opt) Prints count of embedded modules in the compiled bin/app. + go version -m -v bin/app | grep $$'\tdep' | wc -l + +check-embedded-modules-go-not: ##- (opt) Checks embedded modules in compiled bin/app against go.not, throws on occurrence. + @echo "make check-embedded-modules-go-not" + @(mkdir -p tmp 2> /dev/null && go version -m -v bin/app > tmp/.modules) + grep -f go.not -F tmp/.modules && (echo "go.not: Found disallowed embedded module(s) in bin/app!" && exit 1) || exit 0 + +### ----------------------- +# --- Git related +### ----------------------- + +# This is the default upstream go-starter branch we will use for our comparisons. +# You may use a different tag/branch/commit like this: +# - Merge with a specific tag, e.g. `go-starter-2021-10-19`: `GIT_GO_STARTER_TARGET=go-starter-2021-10-19 make git-merge-go-starter` +# - Merge with a specific branch, e.g. `mr/housekeeping`: `GIT_GO_STARTER_TARGET=go-starter/mr/housekeeping make git-merge-go-starter` (heads up! it's `go-starter/`) +# - Merge with a specific commit, e.g. `e85bedb9`: `GIT_GO_STARTER_TARGET=e85bedb94c3562602bc23d2bfd09fca3b13d1e02 make git-merge-go-starter` +GIT_GO_STARTER_TARGET ?= go-starter/master +GIT_GO_STARTER_BASE ?= $(GIT_GO_STARTER_TARGET:go-starter/%=%) + +git-fetch-go-starter: ##- (opt) Fetches upstream GIT_GO_STARTER_TARGET (creating git remote 'go-starter'). + @echo "GIT_GO_STARTER_TARGET=${GIT_GO_STARTER_TARGET} GIT_GO_STARTER_BASE=${GIT_GO_STARTER_BASE}" + @git config remote.go-starter.url >&- || git remote add go-starter https://github.com/allaboutapps/go-starter.git + @git fetch go-starter ${GIT_GO_STARTER_BASE} + +git-compare-go-starter: ##- (opt) Compare upstream GIT_GO_STARTER_TARGET to HEAD displaying commits away and git log. + @$(MAKE) git-fetch-go-starter + @echo "Commits away from upstream go-starter ${GIT_GO_STARTER_TARGET}:" + git --no-pager rev-list --pretty=oneline --left-only --count ${GIT_GO_STARTER_TARGET}...HEAD + @echo "" + @echo "Git log:" + git --no-pager log --left-only --pretty="%C(Yellow)%h %C(reset)%ad (%C(Green)%cr%C(reset))%x09 %C(Cyan)%an: %C(reset)%s" --abbrev-commit --count ${GIT_GO_STARTER_TARGET}...HEAD + +git-merge-go-starter: ##- Merges upstream GIT_GO_STARTER_TARGET into current HEAD. + @$(MAKE) git-compare-go-starter + @(echo "" \ + && echo "Attempting to execute 'git merge --no-commit --no-ff --allow-unrelated-histories ${GIT_GO_STARTER_TARGET}' into your current HEAD." \ + && echo -n "Are you sure? [y/N]" \ + && read ans && [ $${ans:-N} = y ]) || exit 1 + git merge --no-commit --no-ff --allow-unrelated-histories ${GIT_GO_STARTER_TARGET} || true + @echo "Done. We recommend to run 'make force-module-name' to automatically fix all import paths." + ### ----------------------- # --- Helpers ### ----------------------- -clean: ##- (opt) Cleans tmp and api/tmp folder. - rm -rf tmp - rm -rf api/tmp +clean: ##- Cleans ./tmp and ./api/tmp folder. + @echo "make clean" + @rm -rf tmp 2> /dev/null + @rm -rf api/tmp 2> /dev/null -get-module-name: ##- (opt) Prints current go module-name (pipeable). +get-module-name: ##- Prints current go module-name (pipeable). @echo "${GO_MODULE_NAME}" info-module-name: ##- (opt) Prints current go module-name. @@ -258,17 +356,59 @@ set-module-name: ##- Wizard to set a new go module-name. && echo -n "Are you sure? [y/N]" \ && read ans && [ $${ans:-N} = y ] \ && echo -n "Please wait..." \ - && find . -not -path '*/\.*' -type f -exec sed -i "s|${GO_MODULE_NAME}|$${new_module_name}|g" {} \; \ + && find . -not -path '*/\.*' -not -path './Makefile' -type f -exec sed -i "s|${GO_MODULE_NAME}|$${new_module_name}|g" {} \; \ && echo "new go module-name: '$${new_module_name}'!" @rm -f tmp/.modulename +force-module-name: ##- Overwrite occurrences of 'allaboutapps.dev/aw/go-starter' with current go module-name. + find . -not -path '*/\.*' -not -path './Makefile' -type f -exec sed -i "s|allaboutapps.dev/aw/go-starter|${GO_MODULE_NAME}|g" {} \; + +get-go-ldflags: ##- (opt) Prints used -ldflags as evaluated in Makefile used in make go-build + @echo $(LDFLAGS) + # https://gist.github.com/prwhite/8168133 - based on comment from @m000 -help: ##- Show this help. +help: ##- Show common make targets. + @echo "usage: make " + @echo "note: use 'make help-all' to see all make targets." + @echo "" + @sed -e '/#\{2\}-/!d; s/\\$$//; s/:[^#\t]*/@/; s/#\{2\}- *//' $(MAKEFILE_LIST) | grep --invert "(opt)" | sort | column -t -s '@' + +help-all: ##- Show all make targets. @echo "usage: make " - @echo "note: targets flagged with '(opt)' are *internal* and executed as part of another target." + @echo "note: make targets flagged with '(opt)' are part of a main target." @echo "" @sed -e '/#\{2\}-/!d; s/\\$$//; s/:[^#\t]*/@/; s/#\{2\}- *//' $(MAKEFILE_LIST) | sort | column -t -s '@' +### ----------------------- +# --- Make variables +### ----------------------- + +# only evaluated if required by a recipe +# http://make.mad-scientist.net/deferred-simple-variable-expansion/ + +# go module name (as in go.mod) +GO_MODULE_NAME = $(eval GO_MODULE_NAME := $$(shell \ + (mkdir -p tmp 2> /dev/null && cat tmp/.modulename 2> /dev/null) \ + || (gsdev modulename 2> /dev/null | tee tmp/.modulename) || echo "unknown" \ +))$(GO_MODULE_NAME) + +# https://medium.com/the-go-journey/adding-version-information-to-go-binaries-e1b79878f6f2 +ARG_COMMIT = $(eval ARG_COMMIT := $$(shell \ + (git rev-list -1 HEAD 2> /dev/null) \ + || (echo "unknown") \ +))$(ARG_COMMIT) + +ARG_BUILD_DATE = $(eval ARG_BUILD_DATE := $$(shell \ + (date -Is 2> /dev/null || date 2> /dev/null || echo "unknown") \ +))$(ARG_BUILD_DATE) + +# https://www.digitalocean.com/community/tutorials/using-ldflags-to-set-version-information-for-go-applications +LDFLAGS = $(eval LDFLAGS := "\ +-X '$(GO_MODULE_NAME)/internal/config.ModuleName=$(GO_MODULE_NAME)'\ +-X '$(GO_MODULE_NAME)/internal/config.Commit=$(ARG_COMMIT)'\ +-X '$(GO_MODULE_NAME)/internal/config.BuildDate=$(ARG_BUILD_DATE)'\ +")$(LDFLAGS) + ### ----------------------- # --- Special targets ### ----------------------- @@ -280,7 +420,7 @@ help: ##- Show this help. # https://unix.stackexchange.com/questions/153763/dont-stop-makeing-if-a-command-fails-but-check-exit-status # https://www.gnu.org/software/make/manual/html_node/One-Shell.html -# required to ensure make fails if one recipe fails (even on parallel jobs) +# required to ensure make fails if one recipe fails (even on parallel jobs) and on pipefails .ONESHELL: SHELL = /bin/bash -.SHELLFLAGS = -ec +.SHELLFLAGS = -cEeuo pipefail diff --git a/README.md b/README.md index 0f1f8d7b..2d6bbe84 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,19 @@ # go-starter -`go-starter` is an opinionated [golang](https://golang.org/) backend development template by [allaboutapps](https://allaboutapps.at/). +**go-starter** is an opinionated *production-ready* RESTful JSON backend template written in [Go](https://golang.org/), highly integrated with [VSCode DevContainers](https://code.visualstudio.com/docs/remote/containers) by [allaboutapps](https://allaboutapps.at/). + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/allaboutapps/go-starter/blob/master/LICENSE) +[![Build and Test](https://github.com/allaboutapps/go-starter/actions/workflows/build-test.yml/badge.svg)](https://github.com/allaboutapps/go-starter/actions) +[![codecov](https://codecov.io/gh/allaboutapps/go-starter/branch/master/graph/badge.svg?token=220E44857K)](https://codecov.io/gh/allaboutapps/go-starter) +[![Go Report Card](https://goreportcard.com/badge/github.com/allaboutapps/go-starter)](https://goreportcard.com/report/github.com/allaboutapps/go-starter) +[![Swagger Validator](https://img.shields.io/swagger/valid/3.0?specUrl=https%3A%2F%2Fraw.githubusercontent.com%2Fallaboutapps%2Fgo-starter%2Fmaster%2Fapi%2Fswagger.yml)](https://go-starter.allaboutapps.at/documentation/) +![GitHub contributors](https://img.shields.io/github/contributors/allaboutapps/go-starter) +[![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) + +![go starter overview](https://public.allaboutapps.at/go-starter-wiki/go-starter-main-overview.png) + +Demo: **[https://go-starter.allaboutapps.at](https://go-starter.allaboutapps.at)** +FAQ: **[https://github.com/allaboutapps/go-starter/wiki/FAQ](https://github.com/allaboutapps/go-starter/wiki/FAQ)** ## Table of Contents @@ -8,14 +21,15 @@ - [Table of Contents](#table-of-contents) - [Features](#features) - [Usage](#usage) + - [Demo](#demo) - [Requirements](#requirements) - [Quickstart](#quickstart) - - [Set project module name](#set-project-module-name) - - [Typical commands](#typical-commands) - - [Running locally](#running-locally) - - [`./docker-helper.sh`](#docker-helpersh) - - [PostgreSQL](#postgresql) - - [SwaggerUI](#swaggerui) + - [Merge with the go-starter template repository to get future updates](#merge-with-the-go-starter-template-repository-to-get-future-updates) + - [Set project module name for your new project](#set-project-module-name-for-your-new-project) + - [Visual Studio Code](#visual-studio-code) + - [Building and testing](#building-and-testing) + - [Running](#running) + - [Uninstall](#uninstall) - [Additional resources](#additional-resources) - [Contributing](#contributing) - [Maintainers](#maintainers) @@ -27,7 +41,7 @@ - Adheres to the project layout defined in [golang-standard/project-layout](https://github.com/golang-standards/project-layout). - Provides database migration ([sql-migrate](https://github.com/rubenv/sql-migrate)) and models generation ([SQLBoiler](https://github.com/volatiletech/sqlboiler)) workflows for [PostgreSQL](https://www.postgresql.org/) databases. - Integrates [IntegreSQL](https://github.com/allaboutapps/integresql) for fast, concurrent and isolated integration testing with real PostgreSQL databases. -- Autoinstalls our recommended VSCode extensions for golang development. +- Auto-installs our recommended VSCode extensions for golang development. - Integrates [go-swagger](https://github.com/go-swagger/go-swagger) for compile-time generation of `swagger.yml`, structs and request/response validation functions. - Integrates [MailHog](https://github.com/mailhog/MailHog) for easy SMTP-based email testing. - Integrates [SwaggerUI](https://github.com/swagger-api/swagger-ui) for live-previewing your Swagger v2 schema. @@ -35,12 +49,27 @@ - Comes with fully implemented `auth` package, an OAuth2 RESTful JSON API ready to be extended according to your requirements. - Implements [OAuth 2.0 Bearer Tokens](https://tools.ietf.org/html/rfc6750) and password authentication using [argon2id](https://godoc.org/github.com/alexedwards/argon2id) hashes. - Comes with a tested mock and [FCM](https://firebase.google.com/docs/cloud-messaging) provider for sending push notifications and storing push tokens. -- CLI layer provided by [spf13/cobra](https://github.com/spf13/cobra). It's exceptionally easy to add additional subcommands. -- Parallel jobs optimized `Makefile` and various convenience scripts (see all targets and description with `make help`). A full rebuild via `make build` only takes seconds. -- Multi-staged `Dockerfile` (`development` -> `builder` -> `builder-app` -> `app`). +- CLI layer provided by [spf13/cobra](https://github.com/spf13/cobra). It's exceptionally easy to add additional sub-commands. +- Comes with an initial [PostgreSQL](https://www.postgresql.org/) database structure (see [/migrations](https://github.com/allaboutapps/go-starter/tree/master/migrations)), covering: + - auth tokens (access-, refresh-, password-reset-tokens), + - a generic auth-related `user` model + - an app-specific bare-bones `app_user_profile` model, + - push notification tokens and + - a health check sequence (for performing writeable checks). +- API endpoints and CLI for liveness (`/-/healthy`) and readiness (`/-/ready`) probes +- Parallel jobs optimized `Makefile` and various convenience scripts (see all targets and its description via `make help`). A full rebuild only takes seconds. +- Multi-staged `Dockerfile` (`development` -> `builder` -> `app`). ## Usage +> Please find more detailed information regarding the history, usage and other *whys?* of this project in our **[FAQ](https://github.com/allaboutapps/go-starter/wiki/FAQ)**. + +### Demo + +A demo go-starter service is deployed at **[https://go-starter.allaboutapps.at](https://go-starter.allaboutapps.at)** for you to play around with. + +Please visit our [FAQ](https://github.com/allaboutapps/go-starter/wiki/FAQ#what-are-the-limitations-of-your-demo-environment) to find out more about the limitations of this demo environment. + ### Requirements Requires the following local setup for development: @@ -49,47 +78,89 @@ Requires the following local setup for development: - [Docker Compose](https://docs.docker.com/compose/install/) (1.25 or above) - [VSCode Extension: Remote - Containers](https://code.visualstudio.com/docs/remote/containers) (`ms-vscode-remote.remote-containers`) -The project makes use of the [Remote - Containers extension](https://code.visualstudio.com/docs/remote/containers) provided by [Visual Studio Code](https://code.visualstudio.com/). A local installation of the Go toolchain is *no longer* required when using this setup. Please refer to the above official installation guide how this works for your host OS. +This project makes use of the [Remote - Containers extension](https://code.visualstudio.com/docs/remote/containers) provided by [Visual Studio Code](https://code.visualstudio.com/). A local installation of the Go tool-chain is **no longer required** when using this setup. + +Please refer to the [official installation guide](https://code.visualstudio.com/docs/remote/containers) how this works for your host OS and head to our [FAQ: How does our VSCode setup work?](https://github.com/allaboutapps/go-starter/wiki/FAQ#how-does-our-vscode-setup-work) if you encounter issues. ### Quickstart -> GitHub: Click on **[Use this template](https://github.com/allaboutapps/go-starter/generate)** to create your own project. -> Contributions and others: You will need to fork this repository. +Create a new git repository through the GitHub template repository feature ([use this template](https://github.com/allaboutapps/go-starter/generate)). You will then start with a **single initial commit** in your own repository. ```bash -# Easily start the docker-compose dev environment through our helper +# Clone your new repository, cd into it, then easily start the docker-compose dev environment through our helper ./docker-helper.sh --up +``` + +You should be inside the 'service' docker container with a bash shell. -# You should be inside the 'service' docker container with a bash shell. -# development@XXXXXXXXX:/app$ +```bash +development@94242c61cf2b:/app$ # inside your container... -# You may also work in VSCode's integrated terminal after connecting via CMD+SHIFT+P "Remote-Containers: Reopen in Container" +# Shortcut for make init, make build, make info and make test +make all # Print all available make targets make help ``` -### Set project module name +### Merge with the go-starter template repository to get future updates -After your `git clone` you may do the following: +> These steps are **not** necessary if you have a *"real"* fork. + +If your new project is generated from a template project (you have a **single commit**), you want to run the following command immediately and **before** applying any changes. Otherwise you won't be able to easily merge upstream go-starter changes into your own repository (see [GitHub Template Repositories](https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template), [Refusing to merge unrelated histories](https://www.educative.io/edpresso/the-fatal-refusing-to-merge-unrelated-histories-git-error) and [FAQ: I want to compare or update my project/fork to the latest go-starter master](https://github.com/allaboutapps/go-starter/wiki/FAQ#i-want-to-compare-or-update-my-projectfork-to-the-latest-go-starter-master)). + +```bash +make git-merge-go-starter +# Attempting to execute 'git merge --no-commit --no-ff go-starter/master' into your current HEAD. +# Are you sure? [y/N]y +# git merge --no-commit --no-ff --allow-unrelated-histories go-starter/master + +git commit -m "Initial merge of unrelated go-starter template history" +``` + +### Set project module name for your new project + +To replace all occurrences of `allaboutapps.dev/aw/go-stater` (our internal module name of this project) with your desired projects' module name, do the following: ```bash -# Change the go project module name and create a new README +development@94242c61cf2b:/app$ # inside your container... + +# Set a new go project module name. make set-module-name -# internal: allaboutapps.dev// -# others: github.com// +# allaboutapps.dev// (internal only) +# github.com// +# e.g. github.com/majodev/my-service +``` -# Finally move our license file away and create a new README.md for your project +The above command writes your new go module name to `tmp/.modulename`, `go.mod`. It actually sets it everywhere in `**/*` - thus this step is typically only required **once**. If you need to merge changes from the upstream go-starter later, we may want to run `make force-module-name` to set your own go module name everywhere again (especially relevant for new files / import paths). See our [FAQ](https://github.com/allaboutapps/go-starter/wiki/FAQ#i-want-to-compare-or-update-my-projectfork-to-the-latest-go-starter-master) for more information about this update flow. + +Optionally you may want to move the original `README.md` and `LICENSE` away: + +```bash +development@94242c61cf2b:/app$ # inside your container... + +# Optionally you may want to move our LICENSE and README.md away. mv README.md README-go-starter.md mv LICENSE LICENSE-go-starter + +# Optionally create a new README.md for your project. make get-module-name > README.md ``` -### Typical commands +### Visual Studio Code + +> If you are new to VSCode Remote - Containers feature, see our [FAQ: How does our VSCode setup work?](https://github.com/allaboutapps/go-starter/wiki/FAQ#how-does-our-vscode-setup-work). + +Run `CMD+SHIFT+P` `Go: Install/Update Tools` **after** attaching to the container with VSCode to auto-install all golang related vscode extensions. + + +### Building and testing Other useful commands while developing your service: ```bash +development@94242c61cf2b:/app$ # inside your container... + # Print all available make targets make help @@ -106,13 +177,21 @@ make make test ``` -### Running locally +### Running -To finally run the service locally you may: +To run the service locally you may: ```bash +development@94242c61cf2b:/app$ # inside your development container... + +# First ensure you have a fresh `app` executable available +make build + +# Check if all requirements for becoming are met (db is available, mnt path is writeable) +app probe readiness -v + # Migrate up the database -sql-migrate up +app db migrate # Seed the database (if you have any fixtures defined in `/internal/data/fixtures.go`) app db seed @@ -121,48 +200,18 @@ app db seed app server # Now available at http://127.0.0.1:8080 -``` - -### `./docker-helper.sh` - -Our `docker-helper.sh` script does its best to assist our `docker-compose`-based local dev workflow: -```bash -# --- - -# $local - -# you may attach to the development container through multiple shells, it's always the same command -./docker-helper.sh --up - -# if you ever need to halt the docker-compose env (without deleting your projects' images & volumes) -./docker-helper.sh --halt - -# if you ever change something in the Dockerfile and require a rebuild of the service image only -./docker-helper.sh --rebuild - -# if you ever need to wipe ALL docker traces (will delete your projects' images & volumes) -./docker-helper.sh --destroy +# You may also run all the above commands in a single command +app server --probe --migrate --seed # or `app server -pms` ``` -### PostgreSQL - -A PostgreSQL database is automatically started and exposed on `localhost:5432`. - -Feel free to connect with your preferred database client from your host maschine for debugging purposes or just issue `psql` within our development container. - -### SwaggerUI - -A Swagger-UI container was automatically started through our `docker-compose.yml` and is exposed on Port `8081`. Please visit [http://localhost:8081](http://localhost:8081/) to access it (it does not require a running `app server`). - -Regarding [Visual Studio Code](https://code.visualstudio.com/): Always develop *inside* the running `development` docker container, by attaching to this container. +### Uninstall -Run CMD+SHIFT+P `Go: Install/Update Tools` after starting vscode to autoinstall all golang vscode dependencies, then **reload your window**. +Simply run `./docker-helper --destroy` in your working directory (on your host machine) to wipe all docker related traces of this project (and its volumes!). ## Additional resources -* [Wiki](https://github.com/allaboutapps/go-starter/wiki) -* [FAQ](https://github.com/allaboutapps/go-starter/wiki/FAQ) +* **Please visit our [FAQ](https://github.com/allaboutapps/go-starter/wiki/FAQ)**. * [Random Training Material](https://github.com/allaboutapps/go-starter/wiki/Random-training-material) ## Contributing @@ -177,7 +226,8 @@ Please make sure to update tests as appropriate. - [Nick MÃŧller - @MorpheusXAUT](https://github.com/MorpheusXAUT) - [Mario Ranftl - @majodev](https://github.com/majodev) - [Manuel Wieser - @mwieser](https://github.com/mwieser) +- [Dominic Aschauer - @eldelto](https://github.com/eldelto) ## License -[MIT](LICENSE) Š 2020 aaa – all about apps GmbH | Michael Farkas | Nick MÃŧller | Mario Ranftl | Manuel Wieser and the "go-starter" project contributors +[MIT](LICENSE) Š 2021 aaa – all about apps GmbH | Michael Farkas | Nick MÃŧller | Mario Ranftl | Manuel Wieser and the "go-starter" project contributors diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..40d58cea --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Reporting a Vulnerability + +Please do not publicly open issues related to security concerns! + +To report a security issue, email `security [AT] allaboutapps [DOT] at` and include the word `SECURITY` in the subject line: +* Do not include anyone else on the disclosure email. +* Tell us what you found, how to reproduce it, and any concerns you have about it. + +We will do our best to get back to you ASAP. diff --git a/api/README.md b/api/README.md index 157b3a12..abc710f1 100644 --- a/api/README.md +++ b/api/README.md @@ -2,9 +2,21 @@ OpenAPI/Swagger specs, JSON schema files, protocol definition files. -https://github.com/golang-standards/project-layout/blob/master/api/README.md +Use `make swagger` do generate `/internal/types/**/*.go` from these specs. You may use `make watch-swagger` while writing these schemas to automatically execute `make swagger` every time there are changes (especially useful if you have `http://localhost:8081/` opened in your browser while mutating these specs). -Examples: +The final `/api/swagger.yml` is auto-generated from the following specs: +1. The skeleton spec at `/api/config/main.yml`, +2. all path specs living within `/api/paths/*.yml` and +3. any `/api/definitions/*.yml` referenced by in step 1 and 2. +Further reading: +* [Swagger v2 specification](https://swagger.io/specification/v2/) +* [go-swagger](https://github.com/go-swagger/go-swagger) +* [go-swagger: Schema Generation Rules](https://github.com/go-swagger/go-swagger/blob/master/docs/use/models/schemas.md) + + +Related examples regarding this project layout: + +* https://github.com/golang-standards/project-layout/blob/master/api/README.md * https://github.com/kubernetes/kubernetes/tree/master/api * https://github.com/moby/moby/tree/master/api \ No newline at end of file diff --git a/api/config/main.yml b/api/config/main.yml new file mode 100644 index 00000000..f5f7c87b --- /dev/null +++ b/api/config/main.yml @@ -0,0 +1,40 @@ +# This is our base swagger file and the primary mixin target. +# Everything in definitions|paths/*.yml will be mixed through +# and finally flattened into the actual swagger.yml in this dir. +consumes: + - application/json +produces: + - application/json +swagger: "2.0" +info: + title: allaboutapps.dev/aw/go-starter + version: 0.1.0 + description: API documentation +paths: {} +securityDefinitions: + Bearer: + type: apiKey + name: Authorization + in: header + description: |- + Access token for application access, **must** include "Bearer " prefix. + Example: `Bearer b4a94a42-3ea2-4af3-9699-8bcbfee6e6d2` + x-keyPrefix: "Bearer " + Management: + type: apiKey + in: query + description: Management secret, used for monitoring and infrastructure related calls + name: mgmt-secret +definitions: + # Any definitions that are not yet used within paths/*.yml are automatically removed from the resulting swagger.yml. + # You may reference some definitions that you *always* want to be included here. + # -- + # Always include nullables so we can test deserialization + nullables: + $ref: "../definitions/nullable.yml#/definitions/Nullables" + # Always include orderDir so we can test binding it to db queries directly. + orderDir: + type: string + enum: + - asc + - desc diff --git a/api/definitions/nullable.yml b/api/definitions/nullable.yml new file mode 100644 index 00000000..02d04191 --- /dev/null +++ b/api/definitions/nullable.yml @@ -0,0 +1,196 @@ +swagger: "2.0" +info: + title: allaboutapps.dev/aw/go-starter + version: 0.1.0 +paths: {} +definitions: + # https://github.com/allaboutapps/nullable + # Provides ability to determine if a json key has been set to null or not provided. + # These constructs are especially important for PATCH endpoints e.g. to explicity patch an *optional* field in payload to null + NullableBool: + type: boolean + example: true + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Bool + NullableBoolSlice: + type: array + items: + type: boolean + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: BoolSlice + NullableString: + type: string + example: example + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: String + NullableStringSlice: + type: array + items: + type: string + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: StringSlice + NullableInt: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int + NullableIntSlice: + type: array + items: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: IntSlice + NullableInt16: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int16 + NullableInt16Slice: + type: array + items: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int16Slice + NullableInt32: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int32 + NullableInt32Slice: + type: array + items: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int32Slice + NullableInt64: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int64 + NullableInt64Slice: + type: array + items: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int64Slice + NullableFloat: + type: number + format: float + example: 1.5 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float32 + NullableFloatSlice: + type: array + items: + type: number + format: float + example: 1.5 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float32Slice + NullableFloat32: + type: number + format: float + example: 1.5 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float32 + NullableFloat32Slice: + type: array + items: + type: number + format: float + example: 1.5 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float32Slice + NullableFloat64: + type: number + format: float + example: 1.5 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float64 + NullableFloat64Slice: + type: array + items: + type: number + format: float + example: 1.5 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float64Slice + Nullables: + type: object + properties: + nullableBool: + $ref: "#/definitions/NullableBool" + nullableBoolSlice: + $ref: "#/definitions/NullableBoolSlice" + nullableString: + $ref: "#/definitions/NullableString" + nullableStringSlice: + $ref: "#/definitions/NullableStringSlice" + nullableInt: + $ref: "#/definitions/NullableInt" + nullableIntSlice: + $ref: "#/definitions/NullableIntSlice" + nullableInt16: + $ref: "#/definitions/NullableInt16" + nullableInt16Slice: + $ref: "#/definitions/NullableInt16Slice" + nullableInt32: + $ref: "#/definitions/NullableInt32" + nullableInt32Slice: + $ref: "#/definitions/NullableInt32Slice" + nullableInt64: + $ref: "#/definitions/NullableInt64" + nullableInt64Slice: + $ref: "#/definitions/NullableInt64Slice" + nullableFloat: + $ref: "#/definitions/NullableFloat" + nullableFloatSlice: + $ref: "#/definitions/NullableFloatSlice" + nullableFloat32: + $ref: "#/definitions/NullableFloat32" + nullableFloat32Slice: + $ref: "#/definitions/NullableFloat32Slice" + nullableFloat64: + $ref: "#/definitions/NullableFloat64" + nullableFloat64Slice: + $ref: "#/definitions/NullableFloat64Slice" diff --git a/api/main.yml b/api/main.yml deleted file mode 100644 index 7ea8ac37..00000000 --- a/api/main.yml +++ /dev/null @@ -1,29 +0,0 @@ -# This is our base swagger file and the primary mixin target. -# Everything in definitions|paths/*.yml will be mixed through -# and finally flattened into the actual swagger.yml in this dir. -consumes: - - application/json -produces: - - application/json -swagger: "2.0" -info: - title: allaboutapps.dev/aw/go-starter - version: 0.1.0 - description: API documentation -paths: {} -definitions: - orderDir: - type: string - enum: - - asc - - desc -securityDefinitions: - Bearer: - type: apiKey - name: Authorization - in: header - x-keyPrefix: "Bearer " - Management: - type: apiKey - in: query - name: mgmt-secret diff --git a/api/paths/auth.yml b/api/paths/auth.yml index 6ea55942..e59f3c24 100644 --- a/api/paths/auth.yml +++ b/api/paths/auth.yml @@ -151,7 +151,7 @@ paths: /api/v1/auth/refresh: post: description: |- - Return a fresh set of access and refresh tokens if a valid refresh token was provided. + Returns a fresh set of access and refresh tokens if a valid refresh token was provided. The old refresh token used to authenticate the request will be invalidated. tags: - auth diff --git a/api/paths/common.yml b/api/paths/common.yml index ae71784f..62387207 100644 --- a/api/paths/common.yml +++ b/api/paths/common.yml @@ -19,12 +19,14 @@ paths: description: OK /-/ready: get: - summary: Get ready + summary: Get ready (readiness probe) operationId: GetReadyRoute produces: - text/plain description: |- - This endpoint returns 200 when the service is ready to serve traffic (i.e. respond to queries). + This endpoint returns 200 when the service is ready to serve traffic. + Does read-only probes apart from the general server ready state. + Note that /-/ready is typically public (and not shielded by a mgmt-secret), we thus prevent information leakage here and only return `"Ready."`. tags: - common responses: @@ -36,13 +38,15 @@ paths: get: security: - Management: [] - summary: Get healthy + summary: Get healthy (liveness probe) operationId: GetHealthyRoute produces: - text/plain description: |- This endpoint returns 200 when the service is healthy. - It performs additional checks to ensure other parts of the system are available. + Returns an human readable string about the current service status. + In addition to readiness probes, it performs actual write probes. + Note that /-/healthy is private (shielded by the mgmt-secret) as it may expose sensitive information about your service. tags: - common responses: @@ -50,3 +54,18 @@ paths: description: Ready. "521": description: Not ready. + /-/version: + get: + security: + - Management: [] + summary: Get version + operationId: GetVersionRoute + produces: + - text/plain + description: |- + This endpoint returns the module name, commit and build-date baked into the app binary. + tags: + - common + responses: + "200": + description: "ModuleName @ Commit (BuildDate)" diff --git a/api/swagger.yml b/api/swagger.yml index 49a7574d..a7a4a609 100644 --- a/api/swagger.yml +++ b/api/swagger.yml @@ -15,12 +15,14 @@ paths: - Management: [] description: |- This endpoint returns 200 when the service is healthy. - It performs additional checks to ensure other parts of the system are available. + Returns an human readable string about the current service status. + In addition to readiness probes, it performs actual write probes. + Note that /-/healthy is private (shielded by the mgmt-secret) as it may expose sensitive information about your service. produces: - text/plain tags: - common - summary: Get healthy + summary: Get healthy (liveness probe) operationId: GetHealthyRoute responses: "200": @@ -29,18 +31,36 @@ paths: description: Not ready. /-/ready: get: - description: This endpoint returns 200 when the service is ready to serve traffic (i.e. respond to queries). + description: |- + This endpoint returns 200 when the service is ready to serve traffic. + Does read-only probes apart from the general server ready state. + Note that /-/ready is typically public (and not shielded by a mgmt-secret), we thus prevent information leakage here and only return `"Ready."`. produces: - text/plain tags: - common - summary: Get ready + summary: Get ready (readiness probe) operationId: GetReadyRoute responses: "200": description: Ready. "521": description: Not ready. + /-/version: + get: + security: + - Management: [] + description: This endpoint returns the module name, commit and build-date baked + into the app binary. + produces: + - text/plain + tags: + - common + summary: Get version + operationId: GetVersionRoute + responses: + "200": + description: ModuleName @ Commit (BuildDate) /api/v1/auth/change-password: post: security: @@ -191,7 +211,7 @@ paths: /api/v1/auth/refresh: post: description: |- - Return a fresh set of access and refresh tokens if a valid refresh token was provided. + Returns a fresh set of access and refresh tokens if a valid refresh token was provided. The old refresh token used to authenticate the request will be invalidated. tags: - auth @@ -362,6 +382,193 @@ definitions: key: description: Key of field failing validation type: string + nullableBool: + type: boolean + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Bool + example: true + nullableBoolSlice: + type: array + items: + type: boolean + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: BoolSlice + nullableFloat: + type: number + format: float + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float32 + example: 1.5 + nullableFloat32: + type: number + format: float + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float32 + example: 1.5 + nullableFloat32Slice: + type: array + items: + type: number + format: float + example: 1.5 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float32Slice + nullableFloat64: + type: number + format: float + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float64 + example: 1.5 + nullableFloat64Slice: + type: array + items: + type: number + format: float + example: 1.5 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float64Slice + nullableFloatSlice: + type: array + items: + type: number + format: float + example: 1.5 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Float32Slice + nullableInt: + type: integer + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int + example: 1234 + nullableInt16: + type: integer + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int16 + example: 1234 + nullableInt16Slice: + type: array + items: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int16Slice + nullableInt32: + type: integer + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int32 + example: 1234 + nullableInt32Slice: + type: array + items: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int32Slice + nullableInt64: + type: integer + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int64 + example: 1234 + nullableInt64Slice: + type: array + items: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: Int64Slice + nullableIntSlice: + type: array + items: + type: integer + example: 1234 + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: IntSlice + nullableString: + type: string + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: String + example: example + nullableStringSlice: + type: array + items: + type: string + x-go-type: + import: + package: github.com/allaboutapps/nullable + type: StringSlice + nullables: + type: object + properties: + nullableBool: + $ref: '#/definitions/nullableBool' + nullableBoolSlice: + $ref: '#/definitions/nullableBoolSlice' + nullableFloat: + $ref: '#/definitions/nullableFloat' + nullableFloat32: + $ref: '#/definitions/nullableFloat32' + nullableFloat32Slice: + $ref: '#/definitions/nullableFloat32Slice' + nullableFloat64: + $ref: '#/definitions/nullableFloat64' + nullableFloat64Slice: + $ref: '#/definitions/nullableFloat64Slice' + nullableFloatSlice: + $ref: '#/definitions/nullableFloatSlice' + nullableInt: + $ref: '#/definitions/nullableInt' + nullableInt16: + $ref: '#/definitions/nullableInt16' + nullableInt16Slice: + $ref: '#/definitions/nullableInt16Slice' + nullableInt32: + $ref: '#/definitions/nullableInt32' + nullableInt32Slice: + $ref: '#/definitions/nullableInt32Slice' + nullableInt64: + $ref: '#/definitions/nullableInt64' + nullableInt64Slice: + $ref: '#/definitions/nullableInt64Slice' + nullableIntSlice: + $ref: '#/definitions/nullableIntSlice' + nullableString: + $ref: '#/definitions/nullableString' + nullableStringSlice: + $ref: '#/definitions/nullableStringSlice' orderDir: type: string enum: @@ -515,7 +722,8 @@ definitions: x-nullable: true example: 495179de-b771-48f0-aab2-8d23701b0f02 provider: - description: Identifier of the provider the token is for (eg. "fcm", "apn"). Currently only "fcm" is supported. + description: Identifier of the provider the token is for (eg. "fcm", "apn"). + Currently only "fcm" is supported. type: string maxLength: 500 example: fcm @@ -543,7 +751,8 @@ definitions: type: string example: Forbidden type: - description: Type of error returned, should be used for client-side error handling + description: Type of error returned, should be used for client-side error + handling type: string example: generic publicHttpValidationError: @@ -577,11 +786,16 @@ responses: $ref: '#/definitions/publicHttpValidationError' securityDefinitions: Bearer: + description: |- + Access token for application access, **must** include "Bearer " prefix. + Example: `Bearer b4a94a42-3ea2-4af3-9699-8bcbfee6e6d2` type: apiKey name: Authorization in: header x-keyPrefix: 'Bearer ' Management: + description: Management secret, used for monitoring and infrastructure related + calls type: apiKey name: mgmt-secret in: query diff --git a/api/templates/server/parameter.gotmpl b/api/templates/server/parameter.gotmpl index c21f8b38..5632427f 100644 --- a/api/templates/server/parameter.gotmpl +++ b/api/templates/server/parameter.gotmpl @@ -429,7 +429,7 @@ type {{ pascalize .Name }}Params struct { Collection Format: {{ .CollectionFormat }}{{ end }}{{ if .HasDefault }} Default: {{ printf "%#v" .Default }}{{ end }} */ - {{ if not .Schema }}{{ pascalize .ID }} {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}*{{ end }}{{.GoType}}{{ else }}{{ pascalize .Name }} {{ if and (not .Schema.IsBaseType) .IsNullable (not .Schema.IsStream) }}*{{ end }}{{.GoType}}{{ end }}{{/* BEGIN CUSTOM BY AAA */}}{{ if .IsQueryParam }}`query:"{{ .Name }}"`{{ end }}{{/* END CUSTOM BY AAA */}} + {{ if not .Schema }}{{ pascalize .ID }} {{ if and (not .IsArray) (not .HasDiscriminator) (not .IsInterface) (not .IsStream) .IsNullable }}*{{ end }}{{.GoType}}{{ else }}{{ pascalize .Name }} {{ if and (not .Schema.IsBaseType) .IsNullable (not .Schema.IsStream) }}*{{ end }}{{.GoType}}{{ end }}{{/* BEGIN CUSTOM BY AAA */}}{{ if .IsQueryParam }}`query:"{{ .Name }}"`{{ else if .IsPathParam }}`param:"{{ .Name }}"`{{ else if .IsFormParam }}`form:"{{ .Name }}"`{{ end }}{{/* END CUSTOM BY AAA */}} {{ end}} } diff --git a/cmd/db_migrate.go b/cmd/db_migrate.go new file mode 100644 index 00000000..2b93cc75 --- /dev/null +++ b/cmd/db_migrate.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "context" + "database/sql" + "fmt" + "os" + + "allaboutapps.dev/aw/go-starter/internal/config" + migrate "github.com/rubenv/sql-migrate" + "github.com/spf13/cobra" +) + +var migrateCmd = &cobra.Command{ + Use: "migrate", + Short: "Executes all migrations which are not yet applied.", + Run: migrateCmdFunc, +} + +func init() { + dbCmd.AddCommand(migrateCmd) + migrate.SetTable("migrations") +} + +func migrateCmdFunc(cmd *cobra.Command, args []string) { + n, err := applyMigrations() + if err != nil { + fmt.Printf("Error while applying migrations: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Applied %d migrations.\n", n) +} + +func applyMigrations() (int, error) { + ctx := context.Background() + config := config.DefaultServiceConfigFromEnv() + db, err := sql.Open("postgres", config.Database.ConnectionString()) + if err != nil { + return 0, err + } + defer db.Close() + + if err := db.PingContext(ctx); err != nil { + return 0, err + } + + // In case an old migration table exists we rename it to the new name equivalent + // to the settings in dbconfig.yml + if _, err := db.Exec("ALTER TABLE IF EXISTS gorp_migrations RENAME TO migrations;"); err != nil { + return 0, err + } + + migrations := &migrate.FileMigrationSource{ + Dir: "migrations", + } + n, err := migrate.Exec(db, "postgres", migrations, migrate.Up) + if err != nil { + return 0, err + } + + return n, nil +} diff --git a/cmd/db_seed.go b/cmd/db_seed.go index 360b9ab7..73b891b9 100644 --- a/cmd/db_seed.go +++ b/cmd/db_seed.go @@ -8,6 +8,7 @@ import ( "allaboutapps.dev/aw/go-starter/internal/config" "allaboutapps.dev/aw/go-starter/internal/data" + dbutil "allaboutapps.dev/aw/go-starter/internal/util/db" "github.com/spf13/cobra" "github.com/volatiletech/sqlboiler/v4/boil" ) @@ -17,20 +18,22 @@ var seedCmd = &cobra.Command{ Use: "seed", Short: "Inserts or updates fixtures to the database.", Long: `Uses upsert to add test data to the current environment.`, - Run: func(cmd *cobra.Command, args []string) { - if err := applyFixtures(); err != nil { - fmt.Printf("Error while applying fixtures: %v", err) - os.Exit(1) - } - - fmt.Println("Applied all fixtures") - }, + Run: seedCmdFunc, } func init() { dbCmd.AddCommand(seedCmd) } +func seedCmdFunc(cmd *cobra.Command, args []string) { + if err := applyFixtures(); err != nil { + fmt.Printf("Error while seeding fixtures: %v", err) + os.Exit(1) + } + + fmt.Println("Seeded all fixtures.") +} + func applyFixtures() error { ctx := context.Background() config := config.DefaultServiceConfigFromEnv() @@ -44,26 +47,20 @@ func applyFixtures() error { return err } - tx, err := db.BeginTx(ctx, nil) - if err != nil { - return err - } + // insert fixtures in an auto-managed db transaction + return dbutil.WithTransaction(ctx, db, func(tx boil.ContextExecutor) error { - fixtures := data.Upserts() + fixtures := data.Upserts() - for _, fixture := range fixtures { - if err := fixture.Upsert(ctx, db, true, nil, boil.Infer(), boil.Infer()); err != nil { - if err := tx.Rollback(); err != nil { + for _, fixture := range fixtures { + if err := fixture.Upsert(ctx, tx, true, nil, boil.Infer(), boil.Infer()); err != nil { + fmt.Printf("Failed to upsert fixture: %v\n", err) return err } - - return err } - } - if err := tx.Commit(); err != nil { - return err - } + fmt.Printf("Upserted %d fixtures.\n", len(fixtures)) + return nil - return nil + }) } diff --git a/cmd/env.go b/cmd/env.go new file mode 100644 index 00000000..b816032f --- /dev/null +++ b/cmd/env.go @@ -0,0 +1,41 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "allaboutapps.dev/aw/go-starter/internal/config" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +// envCmd represents the server command +var envCmd = &cobra.Command{ + Use: "env", + Short: "Prints the env", + Long: `Prints the currently applied env + +You may use this cmd to get an overview about how +your ENV_VARS are bound by the server config. +Please note that certain secrets are automatically +removed from this output.`, + Run: func(cmd *cobra.Command, args []string) { + runEnv() + }, +} + +func init() { + rootCmd.AddCommand(envCmd) +} + +func runEnv() { + config := config.DefaultServiceConfigFromEnv() + + c, err := json.MarshalIndent(config, "", " ") + + if err != nil { + log.Fatal().Err(err).Msg("Failed to marshal the env") + } + + fmt.Println(string(c)) +} diff --git a/cmd/probe.go b/cmd/probe.go new file mode 100644 index 00000000..4f574a96 --- /dev/null +++ b/cmd/probe.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +const ( + verboseFlag string = "verbose" +) + +// probeCmd represents the probe command +// see probe_*.go for sub_commands +var probeCmd = &cobra.Command{ + Use: "probe ", + Short: "Probe related subcommands", + Run: func(cmd *cobra.Command, args []string) { + if err := cmd.Help(); err != nil { + fmt.Println(err) + os.Exit(1) + } + os.Exit(0) + }, +} + +func init() { + rootCmd.AddCommand(probeCmd) +} diff --git a/cmd/probe_liveness.go b/cmd/probe_liveness.go new file mode 100644 index 00000000..6c72a9d6 --- /dev/null +++ b/cmd/probe_liveness.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "context" + "database/sql" + "fmt" + + "allaboutapps.dev/aw/go-starter/internal/api/handlers/common" + "allaboutapps.dev/aw/go-starter/internal/config" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +// livenessCmd represents the server command +var livenessCmd = &cobra.Command{ + Use: "liveness", + Short: "Runs liveness probes", + Long: `Runs connection livenesss probes + +This command triggers the same livenesss probes as in +/-/healthy (apart from the actual server.ready +probe) and prints the results to stdout. Fails with +non zero exitcode on encountered errors. + +A typical usecase of this command are liveness probes +to take action if dependant services (e.g. DB, NFS +mounts) become unstable. You may also use this to +ensure all requirements are fulfilled before starting +the app server.`, + Run: func(cmd *cobra.Command, args []string) { + + verbose, err := cmd.Flags().GetBool(verboseFlag) + if err != nil { + log.Fatal().Err(err).Msg("Failed to parse args") + } + runLiveness(verbose) + }, +} + +func init() { + probeCmd.AddCommand(livenessCmd) + livenessCmd.Flags().BoolP(verboseFlag, "v", false, "Show verbose output.") +} + +func runLiveness(verbose bool) { + config := config.DefaultServiceConfigFromEnv() + + db, err := sql.Open("postgres", config.Database.ConnectionString()) + if err != nil { + log.Fatal().Err(err).Msg("Failed to connect to the database") + } + defer db.Close() + + ctx, cancel := context.WithTimeout(context.Background(), config.Management.LivenessTimeout) + defer cancel() + + str, errs := common.ProbeLiveness(ctx, db, config.Management.ProbeWriteablePathsAbs, config.Management.ProbeWriteableTouchfile) + + if verbose { + fmt.Print(str) + } + + if len(errs) > 0 { + log.Fatal().Errs("errs", errs).Msg("Unhealthy.") + } +} diff --git a/cmd/probe_readiness.go b/cmd/probe_readiness.go new file mode 100644 index 00000000..11e352bb --- /dev/null +++ b/cmd/probe_readiness.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "context" + "database/sql" + "fmt" + + "allaboutapps.dev/aw/go-starter/internal/api/handlers/common" + "allaboutapps.dev/aw/go-starter/internal/config" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +// readinessCmd represents the server command +var readinessCmd = &cobra.Command{ + Use: "readiness", + Short: "Runs readiness probes", + Long: `Runs connection readinesss probes + +This command triggers the same readinesss probes as in +/-/ready (apart from the actual server.ready +probe) and prints the results to stdout. Fails with +non zero exitcode on encountered errors. + +A typical usecase of this command are readiness probes +to take action if dependant services (e.g. DB, NFS +mounts) become unstable. You may also use this to +ensure all requirements are fulfilled before starting +the app server.`, + Run: func(cmd *cobra.Command, args []string) { + + verbose, err := cmd.Flags().GetBool(verboseFlag) + if err != nil { + log.Fatal().Err(err).Msg("Failed to parse args") + } + runReadiness(verbose) + }, +} + +func init() { + probeCmd.AddCommand(readinessCmd) + readinessCmd.Flags().BoolP(verboseFlag, "v", false, "Show verbose output.") +} + +func runReadiness(verbose bool) { + config := config.DefaultServiceConfigFromEnv() + + db, err := sql.Open("postgres", config.Database.ConnectionString()) + if err != nil { + log.Fatal().Err(err).Msg("Failed to connect to the database") + } + defer db.Close() + + ctx, cancel := context.WithTimeout(context.Background(), config.Management.ReadinessTimeout) + defer cancel() + + str, errs := common.ProbeReadiness(ctx, db, config.Management.ProbeWriteablePathsAbs) + + if verbose { + fmt.Print(str) + } + + if len(errs) > 0 { + log.Fatal().Errs("errs", errs).Msg("Unhealthy.") + } +} diff --git a/cmd/root.go b/cmd/root.go index 866b8ca7..e1307aa4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,21 +4,19 @@ import ( "fmt" "os" + "allaboutapps.dev/aw/go-starter/internal/config" "github.com/spf13/cobra" ) // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ - Use: "app", - Short: "allaboutapps.dev/aw/go-starter", - Long: `A stateless RESTful JSON service written in Go. -Built on the shoulders of giants. + Version: config.GetFormattedBuildArgs(), + Use: "app", + Short: config.ModuleName, + Long: fmt.Sprintf(`%v -Requires configuration through ENV and -a fully migrated PostgreSQL database.`, - // Uncomment the following line if your bare application - // has an action associated with it: - // Run: func(cmd *cobra.Command, args []string) { }, +A stateless RESTful JSON service written in Go. +Requires configuration through ENV.`, config.ModuleName), } // Execute adds all child commands to the root command and sets flags appropriately. @@ -31,13 +29,5 @@ func Execute() { } func init() { - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // rootCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + rootCmd.SetVersionTemplate(`{{printf "%s\n" .Version}}`) } diff --git a/cmd/server.go b/cmd/server.go index 28e004ef..6841e17e 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -2,6 +2,7 @@ package cmd import ( "context" + "fmt" "net/http" "os" "os/signal" @@ -16,6 +17,12 @@ import ( "github.com/spf13/cobra" ) +const ( + probeFlag string = "probe" + migrateFlag string = "migrate" + seedFlag string = "seed" +) + // serverCmd represents the server command var serverCmd = &cobra.Command{ Use: "server", @@ -25,11 +32,45 @@ var serverCmd = &cobra.Command{ Requires configuration through ENV and and a fully migrated PostgreSQL database.`, Run: func(cmd *cobra.Command, args []string) { + + probeReadiness, err := cmd.Flags().GetBool(probeFlag) + if err != nil { + fmt.Printf("Error while parsing flags: %v\n", err) + os.Exit(1) + } + + applyMigrations, err := cmd.Flags().GetBool(migrateFlag) + if err != nil { + fmt.Printf("Error while parsing flags: %v\n", err) + os.Exit(1) + } + + seedFixtures, err := cmd.Flags().GetBool(seedFlag) + if err != nil { + fmt.Printf("Error while parsing flags: %v\n", err) + os.Exit(1) + } + + if probeReadiness { + runReadiness(true) + } + + if applyMigrations { + migrateCmdFunc(cmd, args) + } + + if seedFixtures { + seedCmdFunc(cmd, args) + } + runServer() }, } func init() { + serverCmd.Flags().BoolP(probeFlag, "p", false, "Probe readiness before startup.") + serverCmd.Flags().BoolP(migrateFlag, "m", false, "Apply migrations before startup.") + serverCmd.Flags().BoolP(seedFlag, "s", false, "Seed fixtures into database before startup.") rootCmd.AddCommand(serverCmd) } @@ -38,6 +79,11 @@ func runServer() { zerolog.TimeFieldFormat = time.RFC3339Nano zerolog.SetGlobalLevel(config.Logger.Level) + if config.Logger.PrettyPrintConsole { + log.Logger = log.Output(zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) { + w.TimeFormat = "15:04:05" + })) + } s := api.NewServer(config) @@ -60,7 +106,11 @@ func runServer() { go func() { if err := s.Start(); err != nil { - log.Fatal().Err(err).Msg("Failed to start server") + if err == http.ErrServerClosed { + log.Info().Msg("Server closed") + } else { + log.Fatal().Err(err).Msg("Failed to start server") + } } }() diff --git a/docker-compose.yml b/docker-compose.yml index fa813f8b..b8172fe3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,6 +52,14 @@ services: # optional: env for integresql client testing # INTEGRESQL_CLIENT_BASE_URL: "http://integresql:5000/api" + # optional: enable pretty print of log output + # intended use is for development and debugging purposes only + # not recommended to enable on production systems due to performance penalty and loss of parsing ability + SERVER_LOGGER_PRETTY_PRINT_CONSOLE: "true" + + # optional: static management secret to easily call http://localhost:8080/-/healthy?mgmt-secret=mgmtpass + SERVER_MANAGEMENT_SECRET: "mgmtpass" + # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. cap_add: - SYS_PTRACE @@ -62,7 +70,7 @@ services: command: /bin/sh -c "while sleep 1000; do :; done" postgres: - image: postgres:12.2-alpine # should be the same version as used in .drone.yml, Dockerfile and live + image: postgres:12.4-alpine # should be the same version as used in .drone.yml, .github/workflows, Dockerfile and live # ATTENTION # fsync=off, synchronous_commit=off and full_page_writes=off # gives us a major speed up during local development and testing (~30%), @@ -102,9 +110,7 @@ services: - "8025:8025" swaggerui: - image: swaggerapi/swagger-ui:v3.28.0 - ports: - - "8081:8080" + image: swaggerapi/swagger-ui:v3.46.0 environment: SWAGGER_JSON: "/api/swagger.yml" volumes: @@ -113,6 +119,14 @@ services: # mount overwritten translator.js (intercept requests port 8081 to our local service on port 8080) - ./api/config/swagger-ui-local-translator.js:/usr/share/nginx/configurator/translator.js:ro,delegated + swaggerui-browser-sync: + image: allaboutapps/browser-sync:v2.26.14 + command: start --proxy 'swaggerui:8080' --port 8081 --files "/api/*.yml" + volumes: + - ./api:/api:ro,consistent + ports: + - "8081:8081" + volumes: # postgresql: declare a named volume to persist DB data pgvolume: diff --git a/docs/schemacrawler/.gitignore b/docs/schemacrawler/.gitignore new file mode 100644 index 00000000..dc2dff26 --- /dev/null +++ b/docs/schemacrawler/.gitignore @@ -0,0 +1 @@ +schema.* \ No newline at end of file diff --git a/docs/schemacrawler/README.md b/docs/schemacrawler/README.md new file mode 100644 index 00000000..7d0f2674 --- /dev/null +++ b/docs/schemacrawler/README.md @@ -0,0 +1,30 @@ +# `/docs/schemacrawler` + +To locally (re-)generate a schemacrawler diagramm, execute any of the following commands from your **host** machine. + +```bash +# Note that the project must be already running within docker-compose (and the "spec" database should already be migrated via "make sql" or "make all"). +# First find out under which docker network the "allaboutapps.dev/aw/go-starter" project is available (as started via ./docker-helper.sh --up). +# Typically it's "_default". +docker network ls +# [...] +# go-starter_default + +# Ensure you are within the /docs/schemacrawler directory +cd docs/schemacrawler +pwd +# [...]/docs/schemacrawler + +# Generate a png (exchange --network="..." with your docker network before executing this command) +docker run --network=go-starter_default -v $(pwd):/home/schcrwlr/share -v $(pwd)/schemacrawler.config.properties:/opt/schemacrawler/config/schemacrawler.config.properties --entrypoint=/opt/schemacrawler/schemacrawler.sh schemacrawler/schemacrawler --server=postgresql --host=postgres --port=5432 --database=spec --schemas=public --user=dbuser --password=dbpass --info-level=standard --command=schema --portable-names --title "allaboutapps.dev/aw/go-starter" --output-format=png --output-file=/home/schcrwlr/share/schema.png + +# Generate a pdf (exchange --network="..." with your docker network before executing this command) +docker run --network=go-starter_default -v $(pwd):/home/schcrwlr/share -v $(pwd)/schemacrawler.config.properties:/opt/schemacrawler/config/schemacrawler.config.properties --entrypoint=/opt/schemacrawler/schemacrawler.sh schemacrawler/schemacrawler --server=postgresql --host=postgres --port=5432 --database=spec --schemas=public --user=dbuser --password=dbpass --info-level=standard --command=schema --portable-names --title "allaboutapps.dev/aw/go-starter" --output-format=pdf --output-file=/home/schcrwlr/share/schema.pdf + +# Feel free to override schemacrawler configuration settings in "./schemacrawler.config.properties". +``` + +For further information see: +- [SchemaCrawler Database Diagramming](https://www.schemacrawler.com/diagramming.html) (intro to most diagramming options) +- [Docker Image for SchemaCrawler](https://www.schemacrawler.com/docker-image.html) (about running schemacrawler in Docker) +- [DockerHub `schemacrawler/schemacrawler`](https://hub.docker.com/r/schemacrawler/schemacrawler/) (available version of this Docker image) \ No newline at end of file diff --git a/docs/schemacrawler/schemacrawler.config.properties b/docs/schemacrawler/schemacrawler.config.properties new file mode 100644 index 00000000..c49033c7 --- /dev/null +++ b/docs/schemacrawler/schemacrawler.config.properties @@ -0,0 +1,181 @@ +# --=----=----=----=----=----=----=----=----=----=----=----=----=----=----=----= +# - SchemaCrawler: Configuration Options +# --=----=----=----=----=----=----=----=----=----=----=----=----=----=----=----= +# +# - Metadata Retrieval Options +# ------------------------------------------------------------------------------ +# - Override the metadata retrieval strategy +# - This can affect speed, so they are commented out in order to use database +# - specific defaults +# - Default: Hard-coded into each database plugin, otherwise metadata +# - Possible values for each property are metadata or data_dictionary_all +#schemacrawler.schema.retrieval.strategy.typeinfo=metadata +#schemacrawler.schema.retrieval.strategy.tables=metadata +#schemacrawler.schema.retrieval.strategy.tablecolumns=metadata +#schemacrawler.schema.retrieval.strategy.primarykeys=metadata +#schemacrawler.schema.retrieval.strategy.indexes=metadata +#schemacrawler.schema.retrieval.strategy.foreignkeys=metadata +#schemacrawler.schema.retrieval.strategy.procedures=metadata +#schemacrawler.schema.retrieval.strategy.procedurecolumns=metadata +#schemacrawler.schema.retrieval.strategy.functions=metadata +#schemacrawler.schema.retrieval.strategy.functioncolumns=metadata +# +# - Limit Options - inclusion rules for database objects +# ------------------------------------------------------------------------------ +# - Regular expression schema pattern to filter +# - schema names +# - Default: .* for include, for exclude +# - IMPORTANT: Please uncomment the follow patterns only for +# - database that support schemas. SQLite for example does +# - not support schemas +#schemacrawler.schema.pattern.include=.* +#schemacrawler.schema.pattern.exclude= +# - Regular expression table and column name pattern to filter table +# - and column names +# - Column regular expression to match fully qualified column names, +# - in the form "CATALOGNAME.SCHEMANAME.TABLENAME.COLUMNNAME" +# - Default: .* for include, for exclude +#schemacrawler.table.pattern.include=.* +#schemacrawler.table.pattern.exclude= +#schemacrawler.column.pattern.include=.* +#schemacrawler.column.pattern.exclude= +# - Regular expression routine and routine parameter name pattern to filter +# - routine and routine parameter names +# - Default: .* for include, for exclude +#schemacrawler.routine.pattern.include= +#schemacrawler.routine.pattern.exclude=.* +#schemacrawler.routine.inout.pattern.include=.* +#schemacrawler.routine.inout.pattern.exclude= +# - Regular expression synonym pattern to filter +# - synonym names +# - Default: for include, .* for exclude +#schemacrawler.synonym.pattern.include= +#schemacrawler.synonym.pattern.exclude=.* +# - Regular expression sequence pattern to filter +# - sequence names +# - Default: for include, .* for exclude +#schemacrawler.sequence.pattern.include= +#schemacrawler.sequence.pattern.exclude=.* +# +# - Grep Options - inclusion rules +# ------------------------------------------------------------------------------ +# - Include patterns for table columns +# - Default: .* for include, for exclude +#schemacrawler.grep.column.pattern.include=.* +#schemacrawler.grep.column.pattern.exclude= +# - Include patterns for routine parameters +# - Default: .* for include, for exclude +#schemacrawler.grep.routine.inout.pattern.include=.* +#schemacrawler.grep.routine.inout.pattern.exclude= +# - Include patterns for table and routine definitions +# - Default: .* for include, for exclude +#schemacrawler.grep.definition.pattern.include=.* +#schemacrawler.grep.definition.pattern.exclude= +# +# - Sorting Options +# ------------------------------------------------------------------------------ +# - Sort orders for objects +#schemacrawler.format.sort_alphabetically.tables=true +#schemacrawler.format.sort_alphabetically.table_columns=false +#schemacrawler.format.sort_alphabetically.table_foreignkeys=false +#schemacrawler.format.sort_alphabetically.table_indexes=false +#schemacrawler.format.sort_alphabetically.routines=true +#schemacrawler.format.sort_alphabetically.routine_columns=false +# +# - Show Options - text output formatting +# ------------------------------------------------------------------------------ +# - Controls generation of the SchemaCrawler header and footer in output +# - Default: false +#schemacrawler.format.no_header=false +#schemacrawler.format.no_footer=false +schemacrawler.format.no_schemacrawler_info=true +schemacrawler.format.show_database_info=true +#schemacrawler.format.show_jdbc_driver_info=false +# - Controls display of remarks for tables and columns in output +# - Default: false +#schemacrawler.format.hide_remarks=false +# - Shows all object names with the catalog and schema names, for easier comparison +# - across different schemas +# - Default: false +#schemacrawler.format.show_unqualified_names=false +# - Shows standard column names instead of database specific column names +# - Default: false +#schemacrawler.format.show_standard_column_type_names=false +# - Shows ordinal numbers for columns +# - Default: false +#schemacrawler.format.show_ordinal_numbers=false +# - Shows table row counts - use with --info-level=maximum +# - Default: false +#schemacrawler.format.show_row_counts=false +# - If foreign key names, constraint names, trigger names, +# - specific names for routines, or index and primary key names +# - are not explicitly provided while creating a schema, most +# - database systems assign default names. These names can show +# - up as spurious diffs in SchemaCrawler output. +# - All of these are hidden with the --portable-names +# - command-line option. For more control, use the following +# - options. +# - Hides foreign key names, constraint names, trigger names, +# - specific names for routines, index and primary key names +# - Default: false +#schemacrawler.format.hide_primarykey_names=false +#schemacrawler.format.hide_foreignkey_names=false +#schemacrawler.format.hide_index_names=false +#schemacrawler.format.hide_trigger_names=false +#schemacrawler.format.hide_routine_specific_names=false +#schemacrawler.format.hide_constraint_names=false +#schemacrawler.format.show_weak_associations=false +# Specifies how to quote (delimit) database object names in text output +# Options are +# - quote_none - Do not quote any database object names +# - quote_all - Always quote database object names +# - quote_if_special_characters - Only quote database object names +# if they contain special characters +# - quote_if_special_characters_and_reserved_words - Quote database object names +# if they contain special characters or SQL 2003 reserved words +# - Default: quote_if_special_characters_and_reserved_words +#schemacrawler.format.identifier_quoting_strategy=quote_if_special_characters_and_reserved_words +# - Does not color-code catalog and schema names. +# - Default: false +#schemacrawler.format.no_schema_colors=false +# - Encoding of input files, such as Apache Velocity templates +# - Default: UTF-8 +#schemacrawler.encoding.input=UTF-8 +# - Encoding of SchemaCrawler output files +# - Default: UTF-8 +#schemacrawler.encoding.output=UTF-8 +# +# - Graphing Options +# - (some graphing options may be controlled by text formatting options) +# ------------------------------------------------------------------------------ +# - Show a crow's foot symbol to indicate cardinality +# - Default: true +#schemacrawler.graph.show.primarykey.cardinality=true +#schemacrawler.graph.show.foreignkey.cardinality=true +# +# - Graph attributes for Graphviz, supporting graph, node and edge +# - See https://www.graphviz.org/doc/info/attrs.html +schemacrawler.graph.graphviz.graph.rankdir=RL +schemacrawler.graph.graphviz.graph.labeljust=r +schemacrawler.graph.graphviz.graph.fontname=Helvetica +schemacrawler.graph.graphviz.node.fontname=Helvetica +schemacrawler.graph.graphviz.node.shape=none +schemacrawler.graph.graphviz.edge.fontname=Helvetica + +# schemacrawler.graph.graphviz.graph.splines=ortho + +# - Additional options for Graphviz, to control diagram generation +# - See https://www.graphviz.org/doc/info/command.html +# schemacrawler.graph.graphviz_opts=-Gdpi=150 +# - Data Output Options +# ------------------------------------------------------------------------------ +# - Whether to show data from CLOB and BLOB objects +# - Default: false +#schemacrawler.data.show_lobs=false +# --=----=----=----=----=----=----=----=----=----=----=----=----=----=----=----= +# Queries +# --=----=----=----=----=----=----=----=----=----=----=----=----=----=----=----= +# Define your own named queries, which then become SchemaCrawler command +hsqldb.tables=SELECT * FROM INFORMATION_SCHEMA.SYSTEM_TABLES +tables.select=SELECT ${columns} FROM ${table} ORDER BY ${columns} +tables.drop=DROP ${tabletype} ${table} diff --git a/go.mod b/go.mod index bf1bdac7..7f62c2fa 100644 --- a/go.mod +++ b/go.mod @@ -1,32 +1,115 @@ module allaboutapps.dev/aw/go-starter -go 1.15 +go 1.17 require ( github.com/allaboutapps/integresql-client-go v1.0.0 + github.com/allaboutapps/nullable v1.3.0 + github.com/davecgh/go-spew v1.1.1 github.com/friendsofgo/errors v0.9.2 - github.com/gabriel-vasile/mimetype v1.1.1 - github.com/go-openapi/errors v0.19.6 - github.com/go-openapi/runtime v0.19.20 - github.com/go-openapi/strfmt v0.19.5 - github.com/go-openapi/swag v0.19.9 - github.com/go-openapi/validate v0.19.10 - github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e + github.com/gabriel-vasile/mimetype v1.3.1 + github.com/go-openapi/errors v0.20.1 + github.com/go-openapi/runtime v0.19.31 + github.com/go-openapi/strfmt v0.20.2 + github.com/go-openapi/swag v0.19.15 + github.com/go-openapi/validate v0.20.2 + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 - github.com/labstack/echo/v4 v4.1.16 - github.com/lib/pq v1.8.0 + github.com/labstack/echo/v4 v4.6.1 + github.com/lib/pq v1.10.3 github.com/pkg/errors v0.9.1 - github.com/rogpeppe/go-internal v1.6.1 - github.com/rs/zerolog v1.19.0 - github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 - github.com/spf13/cobra v1.0.0 - github.com/spf13/viper v1.7.1 - github.com/stretchr/testify v1.6.1 - github.com/volatiletech/null/v8 v8.1.0 + github.com/pmezard/go-difflib v1.0.0 + github.com/rogpeppe/go-internal v1.8.0 + github.com/rs/zerolog v1.25.0 + github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc + github.com/spf13/cobra v1.2.1 + github.com/spf13/viper v1.9.0 + github.com/stretchr/testify v1.7.0 + github.com/volatiletech/null/v8 v8.1.2 github.com/volatiletech/randomize v0.0.1 - github.com/volatiletech/sqlboiler/v4 v4.2.0 + github.com/volatiletech/sqlboiler/v4 v4.6.0 github.com/volatiletech/strmangle v0.0.1 - golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de - golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed - google.golang.org/api v0.30.0 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 + golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 + google.golang.org/api v0.57.0 +) + +require ( + cloud.google.com/go v0.94.1 // indirect + github.com/Masterminds/goutils v1.1.0 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect + github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/denisenkom/go-mssqldb v0.10.0 // indirect + github.com/ericlagergren/decimal v0.0.0-20181231230500-73749d4874d5 // indirect + github.com/fatih/color v1.9.0 // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect + github.com/go-logfmt/logfmt v0.5.0 // indirect + github.com/go-openapi/analysis v0.20.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/loads v0.20.2 // indirect + github.com/go-openapi/spec v0.20.3 // indirect + github.com/go-sql-driver/mysql v1.5.0 // indirect + github.com/go-stack/stack v1.8.0 // indirect + github.com/godror/godror v0.24.2 // indirect + github.com/gofrs/uuid v3.2.0+incompatible // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/uuid v1.1.2 // indirect + github.com/googleapis/gax-go/v2 v2.1.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.11 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/labstack/gommon v0.3.0 // indirect + github.com/magiconair/properties v1.8.5 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-oci8 v0.1.1 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/mattn/go-sqlite3 v1.14.6 // indirect + github.com/mitchellh/cli v1.1.2 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.4.2 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pelletier/go-toml v1.9.4 // indirect + github.com/posener/complete v1.2.3 // indirect + github.com/spf13/afero v1.6.0 // indirect + github.com/spf13/cast v1.4.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.2.0 // indirect + github.com/subosito/gotenv v1.2.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.1 // indirect + github.com/volatiletech/inflect v0.0.1 // indirect + github.com/ziutek/mymysql v1.5.4 // indirect + go.mongodb.org/mongo-driver v1.5.1 // indirect + go.opencensus.io v0.23.0 // indirect + golang.org/x/net v0.0.0-20210913180222-943fd674d43e // indirect + golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 // indirect + google.golang.org/grpc v1.40.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/gorp.v1 v1.7.2 // indirect + gopkg.in/ini.v1 v1.63.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.not b/go.not new file mode 100644 index 00000000..78e30427 --- /dev/null +++ b/go.not @@ -0,0 +1,7 @@ +# Specifies go modules that should *never* be embedded in the final app executable (for testing only) +github.com/allaboutapps/integresql-client-go +github.com/davecgh/go-spew +github.com/pmezard/go-difflib +github.com/stretchr/testify +github.com/rogpeppe/go-internal +github.com/kat-co/vala \ No newline at end of file diff --git a/go.sum b/go.sum index d7f3f159..08b5e259 100644 --- a/go.sum +++ b/go.sum @@ -9,11 +9,22 @@ cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6T cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0 h1:WRz29PgAsVEyPSDHyk+0fpEkwEFyfhHn+JbksT6gIL4= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0 h1:RmDygqvj27Zf3fCQjQRtLyC7KwFcHkeJitcO0OoGOcA= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1 h1:DwuSvDZ1pTYGbXo8yOJevCTr3BoBlE+OVkHAKiYQUXc= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -23,6 +34,7 @@ cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM7 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -33,115 +45,102 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM= github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= +github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/allaboutapps/integresql-client-go v1.0.0 h1:sVsV2Z78BR5E9la+8TJ4fkzP832z+uHtfDOt7Mo3SKI= github.com/allaboutapps/integresql-client-go v1.0.0/go.mod h1:C5fz9y+Nnjidhj7Mc9h4qKXSmJanIXMa4nFeEOva0oA= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/allaboutapps/nullable v1.3.0 h1:1LTKtycnfmNZk7VL+am+eB5v11ePAL7RXThNgpqmq3s= +github.com/allaboutapps/nullable v1.3.0/go.mod h1:sfKLUs0wIWSPPWqFEXB+Ma4aun9jAeBAcffq7XQFu/g= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apmckinlay/gsuneido v0.0.0-20180907175622-1f10244968e3/go.mod h1:hJnaqxrCRgMCTWtpNz9XUFkBCREiQdlcyK6YNmOfroM= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= -github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= -github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= -github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef h1:46PFijGLmAjMPwCCCo7Jf0W6f9slllCkkv7vyc1yOSg= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= 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/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -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/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.0.0-20191001013358-cfbb681360f0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e h1:LzwWXEScfcTu7vUZNlDDWDARoSGEtvlDKK2BYHowNeE= -github.com/denisenkom/go-mssqldb v0.0.0-20200206145737-bbfc9a55622e/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= +github.com/denisenkom/go-mssqldb v0.10.0 h1:QykgLZBorFE95+gO3u9esLd0BmbvpWp0/waNNZfHBM8= +github.com/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ericlagergren/decimal v0.0.0-20181231230500-73749d4874d5 h1:HQGCJNlqt1dUs/BhtEKmqWd6LWS+DWYVxi9+Jo4r0jE= github.com/ericlagergren/decimal v0.0.0-20181231230500-73749d4874d5/go.mod h1:1yj25TwtUlJ+pfOu9apAVaM1RWfZGg+aFpd4hPQZekQ= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk= github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gabriel-vasile/mimetype v1.1.1 h1:qbN9MPuRf3bstHu9zkI9jDWNfH//9+9kHxr9oRBBBOA= -github.com/gabriel-vasile/mimetype v1.1.1/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/gabriel-vasile/mimetype v1.3.1 h1:qevA6c2MtE1RorlScnixeG0VA1H4xrXyhyX3oWBynNQ= +github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -149,9 +148,6 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= -github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= @@ -161,71 +157,95 @@ github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpR github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= github.com/go-openapi/analysis v0.19.4/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= -github.com/go-openapi/analysis v0.19.5 h1:8b2ZgKfKIUTVQpTb77MoRDIMEIwvDVw40o3aOXdfYzI= github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= -github.com/go-openapi/analysis v0.19.10 h1:5BHISBAXOc/aJK25irLZnx2D3s6WyYaY9D4gmuz9fdE= github.com/go-openapi/analysis v0.19.10/go.mod h1:qmhS3VNFxBlquFJ0RGoDtylO9y4pgTAUNE9AEEMdlJQ= +github.com/go-openapi/analysis v0.19.16/go.mod h1:GLInF007N83Ad3m8a/CbQ5TPzdnGT7workfHwuVjNVk= +github.com/go-openapi/analysis v0.20.0 h1:UN09o0kNhleunxW7LR+KnltD0YrJ8FF03pSqvAN3Vro= +github.com/go-openapi/analysis v0.20.0/go.mod h1:BMchjvaHDykmRMsK40iPtvyOfFdMMxlOmQr9FBZk+Og= github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= github.com/go-openapi/errors v0.19.3/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/errors v0.19.6 h1:xZMThgv5SQ7SMbWtKFkCf9bBdvR2iEyw9k3zGZONuys= github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.1 h1:j23mMDtRxMwIobkpId7sWh7Ddcx4ivaoqUbfXx5P+a8= +github.com/go-openapi/errors v0.20.1/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= github.com/go-openapi/loads v0.19.3/go.mod h1:YVfqhUCdahYwR3f3iiwQLhicVRvLlU/WO5WPaZvcvSI= -github.com/go-openapi/loads v0.19.5 h1:jZVYWawIQiA1NBnHla28ktg6hrcfTHsCE+3QLVRBIls= github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2es0x5/IbjY= +github.com/go-openapi/loads v0.19.6/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.19.7/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc= +github.com/go-openapi/loads v0.20.0/go.mod h1:2LhKquiE513rN5xC6Aan6lYOSddlL8Mp20AW9kpviM4= +github.com/go-openapi/loads v0.20.2 h1:z5p5Xf5wujMxS1y8aP+vxwW5qYT2zdJBbXKmQUG3lcc= +github.com/go-openapi/loads v0.20.2/go.mod h1:hTVUotJ+UonAMMZsvakEgmWKgtulweO9vYP2bQYKA/o= github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= -github.com/go-openapi/runtime v0.19.4 h1:csnOgcgAiuGoM/Po7PEpKDoNulCcF3FGbSnbHfxgjMI= github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= -github.com/go-openapi/runtime v0.19.15 h1:2GIefxs9Rx1vCDNghRtypRq+ig8KSLrjHbAYI/gCLCM= github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo= -github.com/go-openapi/runtime v0.19.20 h1:J/t+QIjbcoq8WJvjGxRKiFBhqUE8slS9SbmD0Oi/raQ= -github.com/go-openapi/runtime v0.19.20/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= +github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98= +github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= +github.com/go-openapi/runtime v0.19.26/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= +github.com/go-openapi/runtime v0.19.31 h1:GX+MgBxN12s/tQiHNJpvHDIoZiEXAz6j6Rqg0oJcnpg= +github.com/go-openapi/runtime v0.19.31/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= -github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/spec v0.19.6/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= -github.com/go-openapi/spec v0.19.8 h1:qAdZLh1r6QF/hI/gTq+TJTvsQUodZsM7KLqkAJdiJNg= github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/spec v0.19.15/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU= +github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ= +github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ= +github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= github.com/go-openapi/strfmt v0.19.2/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= -github.com/go-openapi/strfmt v0.19.5 h1:0utjKrw+BAh8s57XE9Xz8DUBsVvPmRUB6styvl9wWIM= github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= +github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc= +github.com/go-openapi/strfmt v0.20.2 h1:6XZL+fF4VZYFxKQGLAUB358hOrRh/wS51uWEtlONADE= +github.com/go-openapi/strfmt v0.20.2/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk= github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.7/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= -github.com/go-openapi/swag v0.19.9 h1:1IxuqvBUU3S2Bi4YC7tlP9SJF1gVpCvqN0T2Qof4azE= github.com/go-openapi/swag v0.19.9/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfTe5McPyhY= +github.com/go-openapi/swag v0.19.12/go.mod h1:eFdyEBkTdoAf/9RXBvj4cr1nH7GD8Kzo5HTt47gr72M= +github.com/go-openapi/swag v0.19.13/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= -github.com/go-openapi/validate v0.19.10 h1:tG3SZ5DC5KF4cyt7nqLVcQXGj5A7mpaYkAcNPlDK+Yk= github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbNMAuKvKB+IaGx8= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4= +github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9GA7monOmWBbeCI= +github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0= +github.com/go-openapi/validate v0.20.2 h1:AhqDegYV3J3iQkMPJSXkvzymHKMTw0BST3RK3hTT4ts= +github.com/go-openapi/validate v0.20.2/go.mod h1:e7OJoKNgd0twXZwIn0A43tHbvIcr/rZIVCbJBpTUoY0= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= @@ -235,8 +255,6 @@ github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQ github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8= -github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= @@ -249,31 +267,32 @@ github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxs github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= -github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg= -github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc= +github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= -github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= -github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= +github.com/gobuffalo/packd v1.0.0 h1:6ERZvJHfe24rfFmA9OaoKBdC7+c9sydrytMg8SdFGBM= +github.com/gobuffalo/packd v1.0.0/go.mod h1:6VTc4htmJRFB7u1m/4LeMTWjFoYrUiBkU9Fdec9hrhI= github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= -github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o= -github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= +github.com/gobuffalo/packr/v2 v2.8.1 h1:tkQpju6i3EtMXJ9uoF5GT6kB+LMTimDWD8Xvbz6zDVA= +github.com/gobuffalo/packr/v2 v2.8.1/go.mod h1:c/PLlOuTU+p3SybaJATW3H6lX/iK7xEz5OeMf+NnJpg= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= -github.com/godror/godror v0.13.3 h1:4A5GLGAJTSuELw1NThqY5bINYB+mqrln+kF5C2vuyCs= -github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godror/godror v0.24.2 h1:uxGAD7UdnNGjX5gf4NnEIGw0JAPTIFiqAyRBZTPKwXs= +github.com/godror/godror v0.24.2/go.mod h1:wZv/9vPiUib6tkoDl+AZ/QLf5YZgMravZ7jxH2eQWAE= github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -285,12 +304,14 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -298,24 +319,34 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -323,43 +354,47 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/googleapis/gax-go/v2 v2.1.0 h1:6DWmvNpomjL1+3liNSZbVns3zsYzzCjm6pRBO1tLeso= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -367,201 +402,189 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4U4laftpEHv+mvZBlr7UGimJXKEGQtg48= -github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/karrick/godirwalk v1.15.8 h1:7+rWAZPn9zuRxaIqqT8Ohs2Q2Ac0msBqwRdxNCr2VVs= +github.com/karrick/godirwalk v1.15.8/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 h1:DQVOxR9qdYEybJUr/c7ku34r3PfajaMYXZwgDM7KuSk= github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12/go.mod h1:u9MdXq/QageOOSGp7qG4XAQsYUMP+V5zEel/Vrl6OOc= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uFaWHIInc= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.1.16 h1:8swiwjE5Jkai3RPfZoahp8kjVCRNq+y7Q0hPji2Kz0o= -github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI= +github.com/labstack/echo/v4 v4.6.1 h1:OMVsrnNFzYlGSdaiYGHbgWQnr+JM7NG+B9suCPie14M= +github.com/labstack/echo/v4 v4.6.1/go.mod h1:RnjgMWNDB9g/HucVWhQYNQP9PvbYf6adqftqryo7s9k= github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.1-0.20191011153232-f91d3411e481/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= -github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= +github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.1 h1:mdxE1MF9o53iCb2Ghj1VfWvh7ZOwHpnVG/xwXrV90U8= github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= +github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= +github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-oci8 v0.0.7 h1:BBXYpvzPO43QNTLDEivPFteeFZ9nKA6JQ6eifpxOmio= -github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-sqlite3 v1.12.0 h1:u/x3mp++qUxvYfulZ4HKOvVO0JWhk7HtE8lWhbGz/Do= -github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM= +github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/cli v1.1.2 h1:PvH+lL2B7IQ101xQL63Of8yFS2y+aDlsFcsqNc+u/Kw= +github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.2 h1:sq53g+DWf0J6/ceFUHpQ0nAEb6WgM++fq16MZ91cS6o= -github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= -github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= -github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg= github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo= -github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= -github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5w= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.19.0 h1:hYz4ZVdUgjXTBUmrkrw55j1nHx68LfOKIQk5IYtyScg= -github.com/rs/zerolog v1.19.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= -github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351 h1:HXr/qUllAWv9riaI4zh2eXWKmCSDqVS/XH1MRHLKRwk= -github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II= +github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI= +github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc h1:BD7uZqkN8CpjJtN/tScAKiccBikU4dlqe/gNrkRaPY4= +github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc/go.mod h1:HFLT6i9iR4QBOF5rdCyjddC9t59ArqWJV2xx+jwcCMo= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -569,36 +592,33 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs= github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= -github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= -github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk= +github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= @@ -606,70 +626,77 @@ github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoH github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= -github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= github.com/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwvE8KsU= github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA= -github.com/volatiletech/null/v8 v8.1.0 h1:eAO3I31A5R04usY5SKMMfDcOCnEGyT/T4wRI0JVGp4U= -github.com/volatiletech/null/v8 v8.1.0/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g= +github.com/volatiletech/null/v8 v8.1.2 h1:kiTiX1PpwvuugKwfvUNX/SU/5A2KGZMXfGD0DUHdKEI= +github.com/volatiletech/null/v8 v8.1.2/go.mod h1:98DbwNoKEpRrYtGjWFctievIfm4n4MxG0A6EBUcoS5g= github.com/volatiletech/randomize v0.0.1 h1:eE5yajattWqTB2/eN8df4dw+8jwAzBtbdo5sbWC4nMk= github.com/volatiletech/randomize v0.0.1/go.mod h1:GN3U0QYqfZ9FOJ67bzax1cqZ5q2xuj2mXrXBjWaRTlY= -github.com/volatiletech/sqlboiler/v4 v4.2.0 h1:zNrDbkz8MAsaVd900ZIZlU5fY5uAJyZKX3WS60GgChg= -github.com/volatiletech/sqlboiler/v4 v4.2.0/go.mod h1:U0Z5K4y+twWgHxh364G45QyzyNssSbBqNWtXGHVTlgM= +github.com/volatiletech/sqlboiler/v4 v4.6.0 h1:LJtSRVf5R6kHFWmtZXC4GdFuq7e7QNPs/YAmHy8+R2c= +github.com/volatiletech/sqlboiler/v4 v4.6.0/go.mod h1:tBWGn0ZDYngQr2QUTRpwmjiDIPOUI3mVqo/g5qizcew= github.com/volatiletech/strmangle v0.0.1 h1:UKQoHmY6be/R3tSvD2nQYrH41k43OJkidwEiC74KIzk= github.com/volatiletech/strmangle v0.0.1/go.mod h1:F6RA6IkB5vq0yTG4GQ0UsbbRcl3ni9P76i+JrTBKFFg= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v0.0.0-20180714160509-73f8eece6fdc/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.3.0/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= -go.mongodb.org/mongo-driver v1.3.4 h1:zs/dKNwX0gYUtzwrN9lLiR15hCO0nDwQj5xXx+vjCdE= go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS3gZBapIE= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.4.6/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc= +go.mongodb.org/mongo-driver v1.5.1 h1:9nOVLGDfOaZ9R0tBumx/BcuqkbFpyTCU2r/Po7A2azI= +go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -679,13 +706,17 @@ golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -707,28 +738,28 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -741,53 +772,73 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8= +golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -798,53 +849,82 @@ golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed h1:WBkVNH1zd9jg/dK4HCM4lNANnmd12EHC9z+LmcCG4ns= -golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -863,12 +943,9 @@ golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -877,7 +954,6 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -887,22 +963,34 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4 h1:kDtqNkeBrZb8B+atrj50B5XLHpzXXqcCdZPP/ApQ5NY= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d h1:szSOL78iTCl0LF1AMjhSWJj8tIM0KixlUUnBtYXsmd8= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.3.1 h1:oJra/lMfmtm13/rgY/8i3MzjFWYXvQIAKjQ3HqofMk8= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -917,25 +1005,36 @@ google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0 h1:4t9zuDlHLcIx0ZEhmXEeFVCRsiOgpgn2QOH9N0MNjPI= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= @@ -951,35 +1050,69 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940 h1:MRHtG0U6SnaUb+s+LhNE1qt1FQ1wlhqr5E4usBKC0uA= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c h1:Lq4llNryJoaVFRmvrIwC/ZHH7tNt4tUYIu8+se2aayY= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 h1:3V2dxSZpz4zozWWUq36vUxXEKnSYitEH2LdsAx+RUmg= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -989,51 +1122,48 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c= +gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/internal/api/auth/authentication_result.go b/internal/api/auth/authentication_result.go new file mode 100644 index 00000000..b77f6ca1 --- /dev/null +++ b/internal/api/auth/authentication_result.go @@ -0,0 +1,14 @@ +package auth + +import ( + "time" + + "allaboutapps.dev/aw/go-starter/internal/models" +) + +type AuthenticationResult struct { + Token string + User *models.User + ValidUntil time.Time + Scopes []string +} diff --git a/internal/api/auth/context.go b/internal/api/auth/context.go index 6dc7d366..6a3a7281 100644 --- a/internal/api/auth/context.go +++ b/internal/api/auth/context.go @@ -5,30 +5,31 @@ import ( "allaboutapps.dev/aw/go-starter/internal/models" "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/go-openapi/swag" "github.com/labstack/echo/v4" ) // EnrichContextWithCredentials stores the provided credentials in the form of user and access token used for authentication // in the give context and updates the logger associated with ctx to include the user's ID. -func EnrichContextWithCredentials(ctx context.Context, user *models.User, accessToken *models.AccessToken) context.Context { +func EnrichContextWithCredentials(ctx context.Context, result AuthenticationResult) context.Context { // Retrieve current logger associated with context and extend it ID of authenticated user - l := util.LogFromContext(ctx).With().Str("user_id", user.ID).Logger() + l := util.LogFromContext(ctx).With().Str("user_id", result.User.ID).Logger() c := l.WithContext(ctx) // Store authenticated user's instance in context - c = context.WithValue(c, util.CTXKeyUser, user) + c = context.WithValue(c, util.CTXKeyUser, result.User) // Store access token used for authentication in context - c = context.WithValue(c, util.CTXKeyAccessToken, accessToken) + c = context.WithValue(c, util.CTXKeyAccessToken, result.Token) return c } // EnrichEchoContextWithCredentials stores the provided credentials in the form of user and access token user for authentication // in the given echo context's request and updates the logger associated with c to include the user's ID. -func EnrichEchoContextWithCredentials(c echo.Context, user *models.User, accessToken *models.AccessToken) echo.Context { +func EnrichEchoContextWithCredentials(c echo.Context, result AuthenticationResult) echo.Context { // Get current context and enrich it with credentials req := c.Request() - ctx := EnrichContextWithCredentials(req.Context(), user, accessToken) + ctx := EnrichContextWithCredentials(req.Context(), result) // Set updated request with enriched context in echo context c.SetRequest(req.WithContext(ctx)) @@ -60,22 +61,22 @@ func UserFromEchoContext(c echo.Context) *models.User { // AccessTokenFromContext returns the access token model of the token used to authentication from a context. If no authentication was // provided or the current context does not carry any access token information, nil will be returned instead. -func AccessTokenFromContext(ctx context.Context) *models.AccessToken { +func AccessTokenFromContext(ctx context.Context) *string { t := ctx.Value(util.CTXKeyAccessToken) if t == nil { return nil } - token, ok := t.(*models.AccessToken) + token, ok := t.(string) if !ok { return nil } - return token + return swag.String(token) } // AccessTokenFromEchoContext returns the access token model of the token used to authentication from an echo context. If no authentication // was provided or the current context does not carry any access token information, nil will be returned instead. -func AccessTokenFromEchoContext(c echo.Context) *models.AccessToken { +func AccessTokenFromEchoContext(c echo.Context) *string { return AccessTokenFromContext(c.Request().Context()) } diff --git a/internal/api/auth/scopes.go b/internal/api/auth/scopes.go new file mode 100644 index 00000000..99aa955a --- /dev/null +++ b/internal/api/auth/scopes.go @@ -0,0 +1,11 @@ +package auth + +type Scope string + +const ( + AuthScopeApp Scope = "app" +) + +func (s Scope) String() string { + return string(s) +} diff --git a/internal/api/handlers/auth/get_userinfo_test.go b/internal/api/handlers/auth/get_userinfo_test.go index abf47d01..7b42d152 100644 --- a/internal/api/handlers/auth/get_userinfo_test.go +++ b/internal/api/handlers/auth/get_userinfo_test.go @@ -15,8 +15,6 @@ import ( ) func TestGetUserInfo(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -29,6 +27,7 @@ func TestGetUserInfo(t *testing.T) { assert.Equal(t, fixtures.User1.ID, *response.Sub) assert.Equal(t, strfmt.Email(fixtures.User1.Username.String), response.Email) + test.Snapshoter.Skip([]string{"UpdatedAt"}).Save(t, response) for _, scope := range fixtures.User1.Scopes { assert.Contains(t, response.Scopes, scope) @@ -42,8 +41,6 @@ func TestGetUserInfo(t *testing.T) { } func TestGetUserInfoMinimal(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() diff --git a/internal/api/handlers/auth/post_change_password.go b/internal/api/handlers/auth/post_change_password.go index 3f447452..5305c321 100644 --- a/internal/api/handlers/auth/post_change_password.go +++ b/internal/api/handlers/auth/post_change_password.go @@ -31,7 +31,7 @@ func postChangePasswordHandler(s *api.Server) echo.HandlerFunc { log := util.LogFromContext(ctx) var body types.PostChangePasswordPayload - if err := util.BindAndValidate(c, &body); err != nil { + if err := util.BindAndValidateBody(c, &body); err != nil { return err } diff --git a/internal/api/handlers/auth/post_change_password_test.go b/internal/api/handlers/auth/post_change_password_test.go index e3cc6ad4..1b94da6c 100644 --- a/internal/api/handlers/auth/post_change_password_test.go +++ b/internal/api/handlers/auth/post_change_password_test.go @@ -19,8 +19,6 @@ import ( ) func TestPostChangePasswordSuccess(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -65,8 +63,6 @@ func TestPostChangePasswordSuccess(t *testing.T) { } func TestPostChangePasswordInvalidPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -99,8 +95,6 @@ func TestPostChangePasswordInvalidPassword(t *testing.T) { } func TestPostChangePasswordDeactivatedUser(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -133,8 +127,6 @@ func TestPostChangePasswordDeactivatedUser(t *testing.T) { } func TestPostChangePasswordUserWithoutPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -172,8 +164,6 @@ func TestPostChangePasswordUserWithoutPassword(t *testing.T) { } func TestPostChangePasswordMissingCurrentPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -209,8 +199,6 @@ func TestPostChangePasswordMissingCurrentPassword(t *testing.T) { } func TestPostChangePasswordMissingNewPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -245,8 +233,6 @@ func TestPostChangePasswordMissingNewPassword(t *testing.T) { } func TestPostChangePasswordEmptyCurrentPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -283,8 +269,6 @@ func TestPostChangePasswordEmptyCurrentPassword(t *testing.T) { } func TestPostChangePasswordEmptyNewPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() diff --git a/internal/api/handlers/auth/post_forgot_password.go b/internal/api/handlers/auth/post_forgot_password.go index cea3e914..18fae895 100644 --- a/internal/api/handlers/auth/post_forgot_password.go +++ b/internal/api/handlers/auth/post_forgot_password.go @@ -26,7 +26,7 @@ func postForgotPasswordHandler(s *api.Server) echo.HandlerFunc { ctx := c.Request().Context() var body types.PostForgotPasswordPayload - if err := util.BindAndValidate(c, &body); err != nil { + if err := util.BindAndValidateBody(c, &body); err != nil { return err } diff --git a/internal/api/handlers/auth/post_forgot_password_complete.go b/internal/api/handlers/auth/post_forgot_password_complete.go index d64f60e7..50fc2bd3 100644 --- a/internal/api/handlers/auth/post_forgot_password_complete.go +++ b/internal/api/handlers/auth/post_forgot_password_complete.go @@ -32,7 +32,7 @@ func postForgotPasswordCompleteHandler(s *api.Server) echo.HandlerFunc { log := util.LogFromContext(ctx) var body types.PostForgotPasswordCompletePayload - if err := util.BindAndValidate(c, &body); err != nil { + if err := util.BindAndValidateBody(c, &body); err != nil { return err } diff --git a/internal/api/handlers/auth/post_forgot_password_complete_test.go b/internal/api/handlers/auth/post_forgot_password_complete_test.go index 82d085a0..523014cb 100644 --- a/internal/api/handlers/auth/post_forgot_password_complete_test.go +++ b/internal/api/handlers/auth/post_forgot_password_complete_test.go @@ -21,8 +21,6 @@ import ( ) func TestPostForgotPasswordCompleteSuccess(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -54,6 +52,7 @@ func TestPostForgotPasswordCompleteSuccess(t *testing.T) { assert.NotEqual(t, fixtures.User1RefreshToken1.Token, *response.RefreshToken) assert.Equal(t, int64(s.Config.Auth.AccessTokenValidity.Seconds()), *response.ExpiresIn) assert.Equal(t, auth.TokenTypeBearer, *response.TokenType) + test.Snapshoter.Skip([]string{"AccessToken", "RefreshToken"}).Save(t, response) err = fixtures.User1AccessToken1.Reload(ctx, s.DB) assert.Equal(t, sql.ErrNoRows, err) @@ -75,8 +74,6 @@ func TestPostForgotPasswordCompleteSuccess(t *testing.T) { } func TestPostForgotPasswordCompleteUnknownToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -121,8 +118,6 @@ func TestPostForgotPasswordCompleteUnknownToken(t *testing.T) { } func TestPostForgotPasswordCompleteExpiredToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -175,8 +170,6 @@ func TestPostForgotPasswordCompleteExpiredToken(t *testing.T) { } func TestPostForgotPasswordCompleteDeactivatedUser(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -229,8 +222,6 @@ func TestPostForgotPasswordCompleteDeactivatedUser(t *testing.T) { } func TestPostForgotPasswordCompleteUserWithoutPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -288,8 +279,6 @@ func TestPostForgotPasswordCompleteUserWithoutPassword(t *testing.T) { } func TestPostForgotPasswordCompleteValidation(t *testing.T) { - t.Parallel() - tests := []struct { name string payload test.GenericPayload @@ -344,8 +333,6 @@ func TestPostForgotPasswordCompleteValidation(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - t.Parallel() - res := test.PerformRequest(t, s, "POST", "/api/v1/auth/forgot-password/complete", tt.payload, nil) assert.Equal(t, http.StatusBadRequest, res.Result().StatusCode) diff --git a/internal/api/handlers/auth/post_forgot_password_test.go b/internal/api/handlers/auth/post_forgot_password_test.go index ebdfbcd2..20875462 100644 --- a/internal/api/handlers/auth/post_forgot_password_test.go +++ b/internal/api/handlers/auth/post_forgot_password_test.go @@ -31,8 +31,6 @@ func getLastSentMail(t *testing.T, m *mailer.Mailer) *email.Email { } func TestPostForgotPasswordSuccess(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -54,8 +52,6 @@ func TestPostForgotPasswordSuccess(t *testing.T) { } func TestPostForgotPasswordUnknownUser(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() payload := test.GenericPayload{ @@ -76,8 +72,6 @@ func TestPostForgotPasswordUnknownUser(t *testing.T) { } func TestPostForgotPasswordDeactivatedUser(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -99,8 +93,6 @@ func TestPostForgotPasswordDeactivatedUser(t *testing.T) { } func TestPostForgotPasswordUserWithoutPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -127,8 +119,6 @@ func TestPostForgotPasswordUserWithoutPassword(t *testing.T) { } func TestPostForgotPasswordMissingUsername(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() payload := test.GenericPayload{} @@ -161,8 +151,6 @@ func TestPostForgotPasswordMissingUsername(t *testing.T) { } func TestPostForgotPasswordEmptyUsername(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() payload := test.GenericPayload{ @@ -197,8 +185,6 @@ func TestPostForgotPasswordEmptyUsername(t *testing.T) { } func TestPostForgotPasswordInvalidUsername(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() payload := test.GenericPayload{ diff --git a/internal/api/handlers/auth/post_login.go b/internal/api/handlers/auth/post_login.go index 1a69bf14..67e3e327 100644 --- a/internal/api/handlers/auth/post_login.go +++ b/internal/api/handlers/auth/post_login.go @@ -34,7 +34,7 @@ func postLoginHandler(s *api.Server) echo.HandlerFunc { log := util.LogFromContext(ctx) var body types.PostLoginPayload - if err := util.BindAndValidate(c, &body); err != nil { + if err := util.BindAndValidateBody(c, &body); err != nil { return err } diff --git a/internal/api/handlers/auth/post_login_test.go b/internal/api/handlers/auth/post_login_test.go index 9ba7b41e..2be80101 100644 --- a/internal/api/handlers/auth/post_login_test.go +++ b/internal/api/handlers/auth/post_login_test.go @@ -18,8 +18,6 @@ import ( ) func TestPostLoginSuccess(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { fixtures := test.Fixtures() payload := test.GenericPayload{ @@ -44,8 +42,6 @@ func TestPostLoginSuccess(t *testing.T) { } func TestPostLoginInvalidCredentials(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { fixtures := test.Fixtures() payload := test.GenericPayload{ @@ -70,8 +66,6 @@ func TestPostLoginInvalidCredentials(t *testing.T) { } func TestPostLoginUnknownUser(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { payload := test.GenericPayload{ "username": "definitelydoesnotexist@example.com", @@ -95,8 +89,6 @@ func TestPostLoginUnknownUser(t *testing.T) { } func TestPostLoginDeactivatedUser(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { fixtures := test.Fixtures() payload := test.GenericPayload{ @@ -121,8 +113,6 @@ func TestPostLoginDeactivatedUser(t *testing.T) { } func TestPostLoginUserWithoutPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { fixtures := test.Fixtures() payload := test.GenericPayload{ @@ -152,8 +142,6 @@ func TestPostLoginUserWithoutPassword(t *testing.T) { } func TestPostLoginInvalidUsername(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { payload := test.GenericPayload{ "username": "definitely not an email", @@ -181,8 +169,6 @@ func TestPostLoginInvalidUsername(t *testing.T) { } func TestPostLoginMissingUsername(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { payload := test.GenericPayload{ "password": test.PlainTestUserPassword, @@ -209,8 +195,6 @@ func TestPostLoginMissingUsername(t *testing.T) { } func TestPostLoginMissingPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { fixtures := test.Fixtures() payload := test.GenericPayload{ @@ -238,8 +222,6 @@ func TestPostLoginMissingPassword(t *testing.T) { } func TestPostLoginEmptyUsername(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { payload := test.GenericPayload{ "username": "", @@ -267,8 +249,6 @@ func TestPostLoginEmptyUsername(t *testing.T) { } func TestPostLoginEmptyPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { fixtures := test.Fixtures() payload := test.GenericPayload{ diff --git a/internal/api/handlers/auth/post_logout.go b/internal/api/handlers/auth/post_logout.go index 47d34537..1ac9f1e1 100644 --- a/internal/api/handlers/auth/post_logout.go +++ b/internal/api/handlers/auth/post_logout.go @@ -2,6 +2,7 @@ package auth import ( "database/sql" + "errors" "net/http" "allaboutapps.dev/aw/go-starter/internal/api" @@ -24,14 +25,14 @@ func postLogoutHandler(s *api.Server) echo.HandlerFunc { log := util.LogFromContext(ctx) var body types.PostLogoutPayload - if err := util.BindAndValidate(c, &body); err != nil { + if err := util.BindAndValidateBody(c, &body); err != nil { return err } - accessToken := auth.AccessTokenFromEchoContext(c) + token := auth.AccessTokenFromEchoContext(c) if err := db.WithTransaction(ctx, s.DB, func(tx boil.ContextExecutor) error { - if _, err := accessToken.Delete(ctx, tx); err != nil { + if _, err := models.AccessTokens(models.AccessTokenWhere.Token.EQ(*token)).DeleteAll(ctx, tx); err != nil { log.Debug().Err(err).Msg("Failed to delete access token") return err } @@ -39,7 +40,7 @@ func postLogoutHandler(s *api.Server) echo.HandlerFunc { if len(body.RefreshToken.String()) > 0 { refreshToken, err := models.FindRefreshToken(ctx, tx, body.RefreshToken.String()) if err != nil { - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { log.Debug().Msg("Did not find provided refresh token, ignoring") return nil } diff --git a/internal/api/handlers/auth/post_logout_test.go b/internal/api/handlers/auth/post_logout_test.go index 21f9d67b..044b9541 100644 --- a/internal/api/handlers/auth/post_logout_test.go +++ b/internal/api/handlers/auth/post_logout_test.go @@ -14,8 +14,6 @@ import ( ) func TestPostLogoutSuccess(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -33,8 +31,6 @@ func TestPostLogoutSuccess(t *testing.T) { } func TestPostLogoutSuccessWithRefreshToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -55,8 +51,6 @@ func TestPostLogoutSuccessWithRefreshToken(t *testing.T) { } func TestPostLogoutSuccessWithUnknownRefreshToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -77,8 +71,6 @@ func TestPostLogoutSuccessWithUnknownRefreshToken(t *testing.T) { } func TestPostLogoutInvalidRefreshToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -109,8 +101,6 @@ func TestPostLogoutInvalidRefreshToken(t *testing.T) { } func TestPostLogoutInvalidAuthToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { res := test.PerformRequest(t, s, "POST", "/api/v1/auth/logout", nil, test.HeadersWithAuth(t, "not my auth token")) @@ -129,8 +119,6 @@ func TestPostLogoutInvalidAuthToken(t *testing.T) { } func TestPostLogoutUnknownAuthToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { res := test.PerformRequest(t, s, "POST", "/api/v1/auth/logout", nil, test.HeadersWithAuth(t, "25e8630e-9a41-4f38-8339-373f0c203cef")) @@ -149,8 +137,6 @@ func TestPostLogoutUnknownAuthToken(t *testing.T) { } func TestPostLogoutMissingAuthToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { res := test.PerformRequest(t, s, "POST", "/api/v1/auth/logout", nil, nil) diff --git a/internal/api/handlers/auth/post_refresh.go b/internal/api/handlers/auth/post_refresh.go index d00ccda1..6f6de242 100644 --- a/internal/api/handlers/auth/post_refresh.go +++ b/internal/api/handlers/auth/post_refresh.go @@ -29,7 +29,7 @@ func postRefreshHandler(s *api.Server) echo.HandlerFunc { log := util.LogFromContext(ctx) var body types.PostRefreshPayload - if err := util.BindAndValidate(c, &body); err != nil { + if err := util.BindAndValidateBody(c, &body); err != nil { return err } diff --git a/internal/api/handlers/auth/post_refresh_test.go b/internal/api/handlers/auth/post_refresh_test.go index edc6416c..00794e81 100644 --- a/internal/api/handlers/auth/post_refresh_test.go +++ b/internal/api/handlers/auth/post_refresh_test.go @@ -16,8 +16,6 @@ import ( ) func TestPostRefreshSuccess(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -45,8 +43,6 @@ func TestPostRefreshSuccess(t *testing.T) { } func TestPostRefreshInvalidToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { payload := test.GenericPayload{ "refresh_token": "not my refresh token", @@ -69,8 +65,6 @@ func TestPostRefreshInvalidToken(t *testing.T) { } func TestPostRefreshUnknownToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { payload := test.GenericPayload{ "refresh_token": "c094e933-e5f0-4ece-9c10-914f3122cdb6", @@ -93,8 +87,6 @@ func TestPostRefreshUnknownToken(t *testing.T) { } func TestPostRefreshDeactivatedUser(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() @@ -122,8 +114,6 @@ func TestPostRefreshDeactivatedUser(t *testing.T) { } func TestPostRefreshMissingRefreshToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { payload := test.GenericPayload{} diff --git a/internal/api/handlers/auth/post_register.go b/internal/api/handlers/auth/post_register.go index a9218f7a..11bca06c 100644 --- a/internal/api/handlers/auth/post_register.go +++ b/internal/api/handlers/auth/post_register.go @@ -19,8 +19,6 @@ import ( "github.com/volatiletech/sqlboiler/v4/boil" ) -var () - func PostRegisterRoute(s *api.Server) *echo.Route { return s.Router.APIV1Auth.POST("/register", postRegisterHandler(s)) } @@ -31,7 +29,7 @@ func postRegisterHandler(s *api.Server) echo.HandlerFunc { log := util.LogFromContext(ctx) var body types.PostRegisterPayload - if err := util.BindAndValidate(c, &body); err != nil { + if err := util.BindAndValidateBody(c, &body); err != nil { return err } diff --git a/internal/api/handlers/auth/post_register_test.go b/internal/api/handlers/auth/post_register_test.go index fb38b096..59d2494f 100644 --- a/internal/api/handlers/auth/post_register_test.go +++ b/internal/api/handlers/auth/post_register_test.go @@ -20,8 +20,6 @@ import ( ) func TestPostRegisterSuccess(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() @@ -80,8 +78,6 @@ func TestPostRegisterSuccess(t *testing.T) { } func TestPostRegisterAlreadyExists(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() @@ -121,8 +117,6 @@ func TestPostRegisterAlreadyExists(t *testing.T) { } func TestPostRegisterMissingUsername(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { payload := test.GenericPayload{ "password": test.PlainTestUserPassword, @@ -149,8 +143,6 @@ func TestPostRegisterMissingUsername(t *testing.T) { } func TestPostRegisterMissingPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { fixtures := test.Fixtures() payload := test.GenericPayload{ @@ -178,8 +170,6 @@ func TestPostRegisterMissingPassword(t *testing.T) { } func TestPostRegisterInvalidUsername(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { payload := test.GenericPayload{ "username": "definitely not an email", @@ -207,8 +197,6 @@ func TestPostRegisterInvalidUsername(t *testing.T) { } func TestPostRegisterEmptyUsername(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { payload := test.GenericPayload{ "username": "", @@ -236,8 +224,6 @@ func TestPostRegisterEmptyUsername(t *testing.T) { } func TestPostRegisterEmptyPassword(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { payload := test.GenericPayload{ "username": "usernew@example.com", diff --git a/internal/api/handlers/common/get_healthy.go b/internal/api/handlers/common/get_healthy.go index f7c1b9a1..9637f9f0 100644 --- a/internal/api/handlers/common/get_healthy.go +++ b/internal/api/handlers/common/get_healthy.go @@ -1,75 +1,52 @@ package common import ( + "context" "fmt" "net/http" "strings" - "time" "allaboutapps.dev/aw/go-starter/internal/api" "github.com/labstack/echo/v4" - "golang.org/x/sys/unix" ) func GetHealthyRoute(s *api.Server) *echo.Route { return s.Router.Management.GET("/healthy", getHealthyHandler(s)) } -// Health check +// Heathly check (= liveness) // Returns an human readable string about the current service status. -// Does additional checks apart from the general server ready state +// In addition to readiness probes, it performs actual write probes. +// Note that /-/healthy is private (shielded by the mgmt-secret) as it may expose sensitive information about your service. // Structured upon https://prometheus.io/docs/prometheus/latest/management_api/ func getHealthyHandler(s *api.Server) echo.HandlerFunc { return func(c echo.Context) error { if !s.Ready() { + // We use 521 to indicate an error state + // same as Cloudflare: https://support.cloudflare.com/hc/en-us/articles/115003011431#521error return c.String(521, "Not ready.") } var str strings.Builder - - checksHaveErrored := false - fmt.Fprintln(&str, "Ready.") - // Check database is pingable... - dbPingStart := time.Now() - if err := s.DB.Ping(); err != nil { - checksHaveErrored = true - fmt.Fprintf(&str, "Database: Ping errored after %s, error=%v.\n", time.Since(dbPingStart), err.Error()) - } else { - fmt.Fprintf(&str, "Database: Ping succeeded in %s.\n", time.Since(dbPingStart)) - } - - // Check database is writable... - dbWriteStart := time.Now() - var seqVal int - if err := s.DB.QueryRow("SELECT nextval('seq_health');").Scan(&seqVal); err != nil { - checksHaveErrored = true - fmt.Fprintf(&str, "Database: Next health sequence errored after %s, error=%v.\n", time.Since(dbWriteStart), err.Error()) - } else { - fmt.Fprintf(&str, "Database: Next health sequence succeeded in %s, seq_health=%v.\n", time.Since(dbWriteStart), seqVal) - } - - // Check mount is writeable... - fsStart := time.Now() - if err := unix.Access(s.Config.Paths.MntBaseDirAbs, unix.W_OK); err != nil { - checksHaveErrored = true - fmt.Fprintf(&str, "Mount '%s': Errored after %s, error=%v.\n", s.Config.Paths.MntBaseDirAbs, time.Since(fsStart), err.Error()) - } else { - fmt.Fprintf(&str, "Mount '%s': Writeable check succeeded in %s.\n", s.Config.Paths.MntBaseDirAbs, time.Since(fsStart)) - } + // General Timeout and associated context. + ctx, cancel := context.WithTimeout(c.Request().Context(), s.Config.Management.LivenessTimeout) + defer cancel() - // Feel free to add additional checks here... + healthyStr, errs := ProbeLiveness(ctx, s.DB, s.Config.Management.ProbeWriteablePathsAbs, s.Config.Management.ProbeWriteableTouchfile) + str.WriteString(healthyStr) - if checksHaveErrored { - fmt.Fprintln(&str, "Not healthy.") + // Finally return the health status according to the seen states + if ctx.Err() != nil || len(errs) != 0 { + fmt.Fprintln(&str, "Probes failed.") // We use 521 to indicate this error state // same as Cloudflare: https://support.cloudflare.com/hc/en-us/articles/115003011431#521error return c.String(521, str.String()) } - fmt.Fprintln(&str, "Healthy.") + fmt.Fprintln(&str, "Probes succeeded.") return c.String(http.StatusOK, str.String()) } diff --git a/internal/api/handlers/common/get_healthy_test.go b/internal/api/handlers/common/get_healthy_test.go index 6b52a4e5..a45138a1 100644 --- a/internal/api/handlers/common/get_healthy_test.go +++ b/internal/api/handlers/common/get_healthy_test.go @@ -2,30 +2,64 @@ package common_test import ( "net/http" + "os" + "path" "testing" + "time" "allaboutapps.dev/aw/go-starter/internal/api" "allaboutapps.dev/aw/go-starter/internal/test" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGetHealthySuccess(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { + + // explicitly set touchfile that no other test has (so we can explicitly remove it beforehand.) + s.Config.Management.ProbeWriteableTouchfile = ".healthy-test" + + for _, writeablePath := range s.Config.Management.ProbeWriteablePathsAbs { + os.Remove(path.Join(writeablePath, s.Config.Management.ProbeWriteableTouchfile)) + + // also remove after test completion. + defer os.Remove(path.Join(writeablePath, s.Config.Management.ProbeWriteableTouchfile)) + } + res := test.PerformRequest(t, s, "GET", "/-/healthy?mgmt-secret="+s.Config.Management.Secret, nil, nil) + // fmt.Println(res.Body.String()) require.Equal(t, http.StatusOK, res.Result().StatusCode) require.Contains(t, res.Body.String(), "seq_health=1") + firstTouchTime := make([]time.Time, len(s.Config.Management.ProbeWriteablePathsAbs)) + + // expect a new touchfiles were written + for _, writeablePath := range s.Config.Management.ProbeWriteablePathsAbs { + filePath := path.Join(writeablePath, s.Config.Management.ProbeWriteableTouchfile) + stat, err := os.Stat(filePath) + require.NoErrorf(t, err, "Expected to have %v", filePath) + + firstTouchTime = append(firstTouchTime, stat.ModTime()) + } + res = test.PerformRequest(t, s, "GET", "/-/healthy?mgmt-secret="+s.Config.Management.Secret, nil, nil) require.Equal(t, http.StatusOK, res.Result().StatusCode) require.Contains(t, res.Body.String(), "seq_health=2") + + // expect touchfiles modTime was updated + for i, writeablePath := range s.Config.Management.ProbeWriteablePathsAbs { + filePath := path.Join(writeablePath, s.Config.Management.ProbeWriteableTouchfile) + stat, err := os.Stat(filePath) + require.NoErrorf(t, err, "Expected to have %v", filePath) + + assert.NotEqual(t, firstTouchTime[i], stat.ModTime()) + } + + // fmt.Println(res.Body.String()) }) } func TestGetHealthyNoAuth(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { res := test.PerformRequest(t, s, "GET", "/-/healthy", nil, nil) require.Equal(t, http.StatusBadRequest, res.Result().StatusCode) @@ -33,8 +67,6 @@ func TestGetHealthyNoAuth(t *testing.T) { } func TestGetHealthyWrongAuth(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { res := test.PerformRequest(t, s, "GET", "/-/healthy?mgmt-secret=i-have-no-idea-about-the-pass", nil, nil) require.Equal(t, http.StatusUnauthorized, res.Result().StatusCode) @@ -42,8 +74,6 @@ func TestGetHealthyWrongAuth(t *testing.T) { } func TestGetHealthyDBPingError(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { // forcefully close the DB @@ -55,8 +85,6 @@ func TestGetHealthyDBPingError(t *testing.T) { } func TestGetHealthyDBSeqError(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { // forcefully remove the sequence @@ -71,11 +99,9 @@ func TestGetHealthyDBSeqError(t *testing.T) { } func TestGetHealthyMountError(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { - s.Config.Paths.MntBaseDirAbs = "/this/path/does/not/exist" + s.Config.Management.ProbeWriteablePathsAbs = []string{"/this/path/does/not/exist"} res := test.PerformRequest(t, s, "GET", "/-/healthy?mgmt-secret="+s.Config.Management.Secret, nil, nil) require.Equal(t, 521, res.Result().StatusCode) @@ -83,8 +109,6 @@ func TestGetHealthyMountError(t *testing.T) { } func TestGetHealthyNotReady(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { // forcefully remove an initialized component to check if ready state works diff --git a/internal/api/handlers/common/get_ready.go b/internal/api/handlers/common/get_ready.go index 069858f6..aab73388 100644 --- a/internal/api/handlers/common/get_ready.go +++ b/internal/api/handlers/common/get_ready.go @@ -1,6 +1,7 @@ package common import ( + "context" "net/http" "allaboutapps.dev/aw/go-starter/internal/api" @@ -13,6 +14,8 @@ func GetReadyRoute(s *api.Server) *echo.Route { // Readiness check // This endpoint returns 200 when our Service is ready to serve traffic (i.e. respond to queries). +// Does read-only probes apart from the general server ready state. +// Note that /-/ready is typically public (and not shielded by a mgmt-secret), we thus prevent information leakage here and only return `"Ready."`. // Structured upon https://prometheus.io/docs/prometheus/latest/management_api/ func getReadyHandler(s *api.Server) echo.HandlerFunc { return func(c echo.Context) error { @@ -23,6 +26,19 @@ func getReadyHandler(s *api.Server) echo.HandlerFunc { return c.String(521, "Not ready.") } + // General Timeout and associated context. + ctx, cancel := context.WithTimeout(c.Request().Context(), s.Config.Management.ReadinessTimeout) + defer cancel() + + _, errs := ProbeReadiness(ctx, s.DB, s.Config.Management.ProbeWriteablePathsAbs) + + // Finally return the health status according to the seen states + if ctx.Err() != nil || len(errs) != 0 { + // We use 521 to indicate this error state + // same as Cloudflare: https://support.cloudflare.com/hc/en-us/articles/115003011431#521error + return c.String(521, "Not ready.") + } + return c.String(http.StatusOK, "Ready.") } } diff --git a/internal/api/handlers/common/get_ready_test.go b/internal/api/handlers/common/get_ready_test.go index f07ba5c5..96e5e9db 100644 --- a/internal/api/handlers/common/get_ready_test.go +++ b/internal/api/handlers/common/get_ready_test.go @@ -10,18 +10,14 @@ import ( ) func TestGetReadyReadiness(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { res := test.PerformRequest(t, s, "GET", "/-/ready", nil, nil) require.Equal(t, http.StatusOK, res.Result().StatusCode) - require.Equal(t, "Ready.", res.Body.String()) + require.Equal(t, res.Body.String(), "Ready.") }) } func TestGetReadyReadinessBroken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { // forcefully remove an initialized component to check if ready state works @@ -32,3 +28,16 @@ func TestGetReadyReadinessBroken(t *testing.T) { require.Equal(t, "Not ready.", res.Body.String()) }) } + +func TestGetReadyDBBrokenNotReady(t *testing.T) { + test.WithTestServer(t, func(s *api.Server) { + + // forcefully remove pg + err := s.DB.Close() + require.NoError(t, err) + + res := test.PerformRequest(t, s, "GET", "/-/ready", nil, nil) + require.Equal(t, 521, res.Result().StatusCode) + require.Equal(t, "Not ready.", res.Body.String()) + }) +} diff --git a/internal/api/handlers/common/get_swagger.go b/internal/api/handlers/common/get_swagger.go index c7b901f7..d41a8366 100644 --- a/internal/api/handlers/common/get_swagger.go +++ b/internal/api/handlers/common/get_swagger.go @@ -4,6 +4,7 @@ import ( "path/filepath" "allaboutapps.dev/aw/go-starter/internal/api" + "allaboutapps.dev/aw/go-starter/internal/api/middleware" "github.com/labstack/echo/v4" ) @@ -13,6 +14,6 @@ func GetSwaggerRoute(s *api.Server) *echo.Route { // hack: not attached to group - can go away after echo/group.go .File and .Static actually return the *echo.Route // see https://github.com/labstack/echo/issues/1595 // return s.Router.Root.File("swagger.yml", filepath.Join(s.Config.Echo.APIBaseDirAbs, "swagger.yml")) - - return s.Echo.File("/swagger.yml", filepath.Join(s.Config.Paths.APIBaseDirAbs, "swagger.yml")) + // we explicitly enforce a no-cache directive on any requests to it. + return s.Echo.File("/swagger.yml", filepath.Join(s.Config.Paths.APIBaseDirAbs, "swagger.yml"), middleware.NoCache()) } diff --git a/internal/api/handlers/common/get_swagger_test.go b/internal/api/handlers/common/get_swagger_test.go index a6f754af..40e5d111 100644 --- a/internal/api/handlers/common/get_swagger_test.go +++ b/internal/api/handlers/common/get_swagger_test.go @@ -6,14 +6,27 @@ import ( "allaboutapps.dev/aw/go-starter/internal/api" "allaboutapps.dev/aw/go-starter/internal/test" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSwaggerYAMLRetrieval(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { res := test.PerformRequest(t, s, "GET", "/swagger.yml", nil, nil) require.Equal(t, http.StatusOK, res.Result().StatusCode) + + // caching: ensure this call is always uncached for browsers + assert.Equal(t, "no-cache, private, max-age=0", res.Header().Get("Cache-Control")) + assert.Equal(t, "Thu, 01 Jan 1970 00:00:00 UTC", res.Header().Get("Expires")) + assert.Equal(t, "0", res.Header().Get("X-Accel-Expires")) + assert.Equal(t, "no-cache", res.Header().Get("Pragma")) + + // caching: unset + assert.Equal(t, "", res.Header().Get("ETag")) + assert.Equal(t, "", res.Header().Get("If-Modified-Since")) + assert.Equal(t, "", res.Header().Get("If-Match")) + assert.Equal(t, "", res.Header().Get("If-None-Match")) + assert.Equal(t, "", res.Header().Get("If-Range")) + assert.Equal(t, "", res.Header().Get("If-Unmodified-Since")) }) } diff --git a/internal/api/handlers/common/get_version.go b/internal/api/handlers/common/get_version.go new file mode 100644 index 00000000..9df50c02 --- /dev/null +++ b/internal/api/handlers/common/get_version.go @@ -0,0 +1,20 @@ +package common + +import ( + "net/http" + + "allaboutapps.dev/aw/go-starter/internal/api" + "allaboutapps.dev/aw/go-starter/internal/config" + "github.com/labstack/echo/v4" +) + +func GetVersionRoute(s *api.Server) *echo.Route { + return s.Router.Management.GET("/version", getVersionHandler(s)) +} + +// Returns the version and build date baked into the binary. +func getVersionHandler(s *api.Server) echo.HandlerFunc { + return func(c echo.Context) error { + return c.String(http.StatusOK, config.GetFormattedBuildArgs()) + } +} diff --git a/internal/api/handlers/common/get_version_test.go b/internal/api/handlers/common/get_version_test.go new file mode 100644 index 00000000..8cbd553b --- /dev/null +++ b/internal/api/handlers/common/get_version_test.go @@ -0,0 +1,33 @@ +package common_test + +import ( + "net/http" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/api" + "allaboutapps.dev/aw/go-starter/internal/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetVersion(t *testing.T) { + test.WithTestServer(t, func(s *api.Server) { + res := test.PerformRequest(t, s, "GET", "/-/version?mgmt-secret="+s.Config.Management.Secret, nil, nil) + require.Equal(t, http.StatusOK, res.Result().StatusCode) + require.Equal(t, "build.local/misses/ldflags @ < 40 chars git commit hash via ldflags > (1970-01-01T00:00:00+00:00)", res.Body.String()) // build args are not injected during test time. + + // caching: ensure this call is always uncached for browsers + assert.Equal(t, "no-cache, private, max-age=0", res.Header().Get("Cache-Control")) + assert.Equal(t, "Thu, 01 Jan 1970 00:00:00 UTC", res.Header().Get("Expires")) + assert.Equal(t, "0", res.Header().Get("X-Accel-Expires")) + assert.Equal(t, "no-cache", res.Header().Get("Pragma")) + + // caching: unset + assert.Equal(t, "", res.Header().Get("ETag")) + assert.Equal(t, "", res.Header().Get("If-Modified-Since")) + assert.Equal(t, "", res.Header().Get("If-Match")) + assert.Equal(t, "", res.Header().Get("If-None-Match")) + assert.Equal(t, "", res.Header().Get("If-Range")) + assert.Equal(t, "", res.Header().Get("If-Unmodified-Since")) + }) +} diff --git a/internal/api/handlers/common/probes.go b/internal/api/handlers/common/probes.go new file mode 100644 index 00000000..d3ff8323 --- /dev/null +++ b/internal/api/handlers/common/probes.go @@ -0,0 +1,228 @@ +package common + +import ( + "context" + "database/sql" + "fmt" + "path" + "strings" + "sync" + "time" + + "allaboutapps.dev/aw/go-starter/internal/util" + "golang.org/x/sys/unix" +) + +func ProbeReadiness(ctx context.Context, database *sql.DB, writeablePaths []string) (string, []error) { + var str strings.Builder + + // slice collects all errors from probes + errs := make([]error, 0, 1+len(writeablePaths)) + + // DB readable? + dbPingStr, dbPingErr := probeDatabasePingable(ctx, database) + str.WriteString(dbPingStr) + + if dbPingErr != nil { + errs = append(errs, dbPingErr) + } + + // FS (potentially) writeable? + for _, writeablePath := range writeablePaths { + + fsPermStr, fsPermErr := probePathWriteablePermission(ctx, writeablePath) + str.WriteString(fsPermStr) + + if fsPermErr != nil { + errs = append(errs, fsPermErr) + } + } + + // Feel free to add additional probes here... + + return str.String(), errs +} + +func ProbeLiveness(ctx context.Context, database *sql.DB, writeablePaths []string, touch string) (string, []error) { + + // fail immediately if any readiness probes above have already failed. + readinessProbeStr, readinessProbeErrs := ProbeReadiness(ctx, database, writeablePaths) + + if len(readinessProbeErrs) != 0 { + return readinessProbeStr, readinessProbeErrs + } + + var str strings.Builder + + // include previous readiness probe results in final string + str.WriteString(readinessProbeStr) + + // slice collects all errors from probes + errs := make([]error, 0, 1+len(writeablePaths)) + + // DB writeable? + dbHealthStr, dbHealthErr := probeDatabaseNextHealthSequence(ctx, database) + str.WriteString(dbHealthStr) + + if dbHealthErr != nil { + errs = append(errs, dbHealthErr) + } + + // FS writeable? + for _, writeablePath := range writeablePaths { + + fsTouchStr, fsTouchErr := probePathWriteableTouch(ctx, writeablePath, touch) + str.WriteString(fsTouchStr) + if fsTouchErr != nil { + errs = append(errs, fsTouchErr) + } + } + + // Feel free to add additional probes here... + + return str.String(), errs +} + +// FS (especially hard mounted NFS paths) or PostgreSQL calls may be blocking or running for too long and thus need to run detached +// We additionally want them to timeout (e.g. useful for hard mounted NFS paths) +// Typically a any context used here will already have a deadline associated +// If not we will explicitly return a short one here. +func ensureProbeDeadlineFromContext(ctx context.Context) time.Time { + ctxDeadline, hasDeadline := ctx.Deadline() + if !hasDeadline { + ctxDeadline = time.Now().Add(1 * time.Second) + } + + return ctxDeadline +} + +func probeDatabasePingable(ctx context.Context, database *sql.DB) (string, error) { + var str strings.Builder + ctxDeadline := ensureProbeDeadlineFromContext(ctx) + + dbPingStart := time.Now() + + var dbPingWg sync.WaitGroup + var dbErr error + + dbPingWg.Add(1) + go func() { + dbErr = database.PingContext(ctx) + dbPingWg.Done() + }() + + if err := util.WaitTimeout(&dbPingWg, time.Until(ctxDeadline)); err != nil { + fmt.Fprintf(&str, "Probe db: Ping deadline after %s, error=%v.\n", time.Since(dbPingStart), err.Error()) + return str.String(), err + } + + if dbErr != nil { + fmt.Fprintf(&str, "Probe db: Ping errored after %s, error=%v.\n", time.Since(dbPingStart), dbErr.Error()) + return str.String(), dbErr + } + + fmt.Fprintf(&str, "Probe db: Ping succeeded in %s.\n", time.Since(dbPingStart)) + + return str.String(), nil +} + +func probeDatabaseNextHealthSequence(ctx context.Context, database *sql.DB) (string, error) { + var str strings.Builder + ctxDeadline := ensureProbeDeadlineFromContext(ctx) + + dbWriteStart := time.Now() + + var seqVal int + var dbWriteWg sync.WaitGroup + var dbErr error + + dbWriteWg.Add(1) + go func() { + dbErr = database.QueryRowContext(ctx, "SELECT nextval('seq_health');").Scan(&seqVal) + dbWriteWg.Done() + }() + + if err := util.WaitTimeout(&dbWriteWg, time.Until(ctxDeadline)); err != nil { + fmt.Fprintf(&str, "Probe db: Next health sequence deadline after %s, error=%v.\n", time.Since(dbWriteStart), err.Error()) + return str.String(), err + } + + if dbErr != nil { + fmt.Fprintf(&str, "Probe db: Next health sequence errored after %s, error=%v.\n", time.Since(dbWriteStart), dbErr.Error()) + return str.String(), dbErr + } + + fmt.Fprintf(&str, "Probe db: Next health sequence succeeded in %s, seq_health=%v.\n", time.Since(dbWriteStart), seqVal) + + return str.String(), nil +} + +func probePathWriteablePermission(ctx context.Context, writeablePath string) (string, error) { + var str strings.Builder + ctxDeadline := ensureProbeDeadlineFromContext(ctx) + + fsWriteStart := time.Now() + + if ctx.Err() != nil { + fmt.Fprintf(&str, "Probe path '%s': W_OK check cancelled after %s, error=%v.\n", writeablePath, time.Since(fsWriteStart), ctx.Err()) + return str.String(), ctx.Err() + } + + var fsWriteWg sync.WaitGroup + var fsWriteErr error + fsWriteWg.Add(1) + go func(wp string) { + fsWriteErr = unix.Access(wp, unix.W_OK) + fsWriteWg.Done() + }(writeablePath) + + if err := util.WaitTimeout(&fsWriteWg, time.Until(ctxDeadline)); err != nil { + fmt.Fprintf(&str, "Probe path '%s': W_OK check deadline after %s, error=%v.\n", writeablePath, time.Since(fsWriteStart), err) + return str.String(), err + } + + if fsWriteErr != nil { + fmt.Fprintf(&str, "Probe path '%s': W_OK check errored after %s, error=%v.\n", writeablePath, time.Since(fsWriteStart), fsWriteErr.Error()) + return str.String(), fsWriteErr + } + + fmt.Fprintf(&str, "Probe path '%s': W_OK check succeeded in %s.\n", writeablePath, time.Since(fsWriteStart)) + + return str.String(), nil +} + +func probePathWriteableTouch(ctx context.Context, writeablePath string, touch string) (string, error) { + var str strings.Builder + ctxDeadline := ensureProbeDeadlineFromContext(ctx) + + fsTouchStart := time.Now() + fsTouchNameAbs := path.Join(writeablePath, touch) + + if ctx.Err() != nil { + fmt.Fprintf(&str, "Probe path '%s': Touch cancelled after %s, error=%v.\n", fsTouchNameAbs, time.Since(fsTouchStart), ctx.Err()) + return str.String(), ctx.Err() + } + + var fsTouchWg sync.WaitGroup + var fsTouchErr error + var fsTouchModTime time.Time + fsTouchWg.Add(1) + go func(tn string) { + fsTouchModTime, fsTouchErr = util.TouchFile(tn) + fsTouchWg.Done() + }(fsTouchNameAbs) + + if err := util.WaitTimeout(&fsTouchWg, time.Until(ctxDeadline)); err != nil { + fmt.Fprintf(&str, "Probe path '%s': Touch deadline after %s, error=%v.\n", fsTouchNameAbs, time.Since(fsTouchStart), err) + return str.String(), err + } + + if fsTouchErr != nil { + fmt.Fprintf(&str, "Probe path '%s': Touch errored after %s, error=%v.\n", fsTouchNameAbs, time.Since(fsTouchStart), fsTouchErr.Error()) + return str.String(), fsTouchErr + } + + fmt.Fprintf(&str, "Probe path '%s': Touch succeeded in %s, modTime=%v.\n", fsTouchNameAbs, time.Since(fsTouchStart), fsTouchModTime.Unix()) + + return str.String(), nil +} diff --git a/internal/api/handlers/common/probes_internal_test.go b/internal/api/handlers/common/probes_internal_test.go new file mode 100644 index 00000000..832f7d8d --- /dev/null +++ b/internal/api/handlers/common/probes_internal_test.go @@ -0,0 +1,67 @@ +package common + +import ( + "context" + "database/sql" + "os" + "testing" + "time" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/stretchr/testify/assert" +) + +func TestEnsureDeadline(t *testing.T) { + deadline := time.Now().Add(1 * time.Second) + + ctx, cancel := context.WithDeadline(context.Background(), deadline) + defer cancel() + + receivedDeadline := ensureProbeDeadlineFromContext(ctx) + assert.Equal(t, deadline, receivedDeadline) +} + +func TestDummyDeadlineWithinOneSec(t *testing.T) { + ctx := context.Background() + + receivedDeadline := ensureProbeDeadlineFromContext(ctx) + assert.WithinDuration(t, time.Now().Add(1*time.Second), receivedDeadline, 100*time.Millisecond) + +} + +func TestProbeDatabasePingableDeadline(t *testing.T) { + ctx, cancel := context.WithDeadline(context.Background(), time.Now()) + defer cancel() + + _, err := probeDatabasePingable(ctx, &sql.DB{}) + assert.Truef(t, err == util.ErrWaitTimeout || err == context.DeadlineExceeded, "err must be util.ErrWaitTimeout or context.DeadlineExceeded but is %v", err) +} + +func TestProbeDatabaseNextHealthSequenceDeadline(t *testing.T) { + ctx, cancel := context.WithDeadline(context.Background(), time.Now()) + defer cancel() + + _, err := probeDatabaseNextHealthSequence(ctx, &sql.DB{}) + assert.Truef(t, err == util.ErrWaitTimeout || err == context.DeadlineExceeded, "err must be util.ErrWaitTimeout or context.DeadlineExceeded but is %v", err) +} + +func TestProbePathWriteablePermissionContextDeadline(t *testing.T) { + ctx, cancel := context.WithDeadline(context.Background(), time.Now()) + defer cancel() + + _, err := probePathWriteablePermission(ctx, "/any/thing") + assert.Truef(t, err == util.ErrWaitTimeout || err == context.DeadlineExceeded, "err must be util.ErrWaitTimeout or context.DeadlineExceeded but is %v", err) +} + +func TestProbePathWriteableTouchContextDeadline(t *testing.T) { + ctx, cancel := context.WithDeadline(context.Background(), time.Now()) + defer cancel() + + _, err := probePathWriteableTouch(ctx, "/any/thing", ".touch") + assert.Truef(t, err == util.ErrWaitTimeout || err == context.DeadlineExceeded, "err must be util.ErrWaitTimeout or context.DeadlineExceeded but is %v", err) +} + +func TestProbePathWriteableTouchInaccessable(t *testing.T) { + _, err := probePathWriteableTouch(context.Background(), "/this/path/does/not/exist", ".touch") + assert.True(t, os.IsNotExist(err)) +} diff --git a/internal/api/handlers/handlers.go b/internal/api/handlers/handlers.go index 1eac32e4..4bdafa92 100644 --- a/internal/api/handlers/handlers.go +++ b/internal/api/handlers/handlers.go @@ -1,4 +1,4 @@ -// Code generated by go run scripts/handlers/gen_handlers.go; DO NOT EDIT. +// Code generated by go run -tags scripts scripts/handlers/gen_handlers.go; DO NOT EDIT. package handlers import ( @@ -23,6 +23,7 @@ func AttachAllRoutes(s *api.Server) { common.GetHealthyRoute(s), common.GetReadyRoute(s), common.GetSwaggerRoute(s), + common.GetVersionRoute(s), push.GetPushTestRoute(s), push.PostUpdatePushTokenRoute(s), } diff --git a/internal/api/handlers/push/get_test_push_test.go b/internal/api/handlers/push/get_test_push_test.go new file mode 100644 index 00000000..6ebf5aa7 --- /dev/null +++ b/internal/api/handlers/push/get_test_push_test.go @@ -0,0 +1,28 @@ +package push_test + +import ( + "net/http" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/api" + "allaboutapps.dev/aw/go-starter/internal/test" + "github.com/stretchr/testify/assert" +) + +func TestGetTestPush(t *testing.T) { + test.WithTestServer(t, func(s *api.Server) { + fixtures := test.Fixtures() + + res := test.PerformRequest(t, s, "GET", "/api/v1/push/test", nil, test.HeadersWithAuth(t, fixtures.User1AccessToken1.Token)) + + assert.Equal(t, http.StatusOK, res.Result().StatusCode) + }) +} + +func TestGetTestPushUnauthorized(t *testing.T) { + test.WithTestServer(t, func(s *api.Server) { + res := test.PerformRequest(t, s, "GET", "/api/v1/push/test", nil, nil) + + assert.Equal(t, http.StatusUnauthorized, res.Result().StatusCode) + }) +} diff --git a/internal/api/handlers/push/post_update_push_token.go b/internal/api/handlers/push/post_update_push_token.go index 1c2e4714..cbca57d6 100644 --- a/internal/api/handlers/push/post_update_push_token.go +++ b/internal/api/handlers/push/post_update_push_token.go @@ -16,8 +16,6 @@ import ( "github.com/volatiletech/sqlboiler/v4/boil" ) -var () - func PostUpdatePushTokenRoute(s *api.Server) *echo.Route { return s.Router.APIV1Push.PUT("/token", postUpdatePushTokenHandler(s)) } @@ -28,7 +26,7 @@ func postUpdatePushTokenHandler(s *api.Server) echo.HandlerFunc { log := util.LogFromContext(ctx) var body types.PostUpdatePushTokenPayload - if err := util.BindAndValidate(c, &body); err != nil { + if err := util.BindAndValidateBody(c, &body); err != nil { return err } diff --git a/internal/api/handlers/push/post_update_push_token_test.go b/internal/api/handlers/push/post_update_push_token_test.go index c001f98f..4c9004f5 100644 --- a/internal/api/handlers/push/post_update_push_token_test.go +++ b/internal/api/handlers/push/post_update_push_token_test.go @@ -16,12 +16,11 @@ import ( ) func TestPostUpdatePushTokenSuccess(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() + //nolint:gosec testToken := "869f6deb-73e6-4691-9d40-2a2a794006cf" testProvider := "fcm" @@ -44,12 +43,11 @@ func TestPostUpdatePushTokenSuccess(t *testing.T) { } func TestPostUpdatePushTokenSuccessWithOldToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() + //nolint:gosec oldToken := "6803ccb4-c91d-47b2-960e-291afa5e29cd" oldPushToken := models.PushToken{ @@ -60,6 +58,7 @@ func TestPostUpdatePushTokenSuccessWithOldToken(t *testing.T) { err := oldPushToken.Insert(ctx, s.DB, boil.Infer()) require.NoError(t, err) + //nolint:gosec testToken := "af55b6cf-1fb0-4bb7-960c-25268a5ce7c3" testProvider := "fcm" @@ -86,12 +85,11 @@ func TestPostUpdatePushTokenSuccessWithOldToken(t *testing.T) { } func TestPostUpdatePushTokenWithDuplicateToken(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() + //nolint:gosec oldToken := "6803ccb4-c91d-47b2-960e-291afa5e29cd" oldPushToken := models.PushToken{ @@ -136,12 +134,11 @@ func TestPostUpdatePushTokenWithDuplicateToken(t *testing.T) { } func TestPostUpdatePushTokenWithOldTokenNotfound(t *testing.T) { - t.Parallel() - test.WithTestServer(t, func(s *api.Server) { ctx := context.Background() fixtures := test.Fixtures() + //nolint:gosec oldToken := "cc08624a-b40d-4b8e-bbfe-f62aabb47592" oldPushToken := models.PushToken{ @@ -155,6 +152,7 @@ func TestPostUpdatePushTokenWithOldTokenNotfound(t *testing.T) { oldCnt, err := fixtures.User1.PushTokens().Count(ctx, s.DB) assert.NoError(t, err) + //nolint:gosec testToken := "8e4ad85f-cbb6-4ef3-a455-d9d8bd8917b3" testProvider := "fcm" diff --git a/internal/api/httperrors/error.go b/internal/api/httperrors/error.go index a384d817..28ea4fb6 100644 --- a/internal/api/httperrors/error.go +++ b/internal/api/httperrors/error.go @@ -10,6 +10,7 @@ import ( ) const ( + // HTTPErrorTypeGeneric represents the generic error type returned as default for all HTTP errors without a type defined. HTTPErrorTypeGeneric string = "generic" ) diff --git a/internal/api/httperrors/error_test.go b/internal/api/httperrors/error_test.go new file mode 100644 index 00000000..083d8fb2 --- /dev/null +++ b/internal/api/httperrors/error_test.go @@ -0,0 +1,80 @@ +package httperrors_test + +import ( + "database/sql" + "net/http" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/api/httperrors" + "allaboutapps.dev/aw/go-starter/internal/types" + "github.com/go-openapi/swag" + "github.com/stretchr/testify/require" +) + +func TestHTTPErrorSimple(t *testing.T) { + e := httperrors.NewHTTPError(http.StatusNotFound, httperrors.HTTPErrorTypeGeneric, http.StatusText(http.StatusNotFound)) + require.Equal(t, "HTTPError 404 (generic): Not Found", e.Error()) +} + +func TestHTTPErrorDetail(t *testing.T) { + e := httperrors.NewHTTPErrorWithDetail(http.StatusNotFound, httperrors.HTTPErrorTypeGeneric, http.StatusText(http.StatusNotFound), "ToS violation") + require.Equal(t, "HTTPError 404 (generic): Not Found - ToS violation", e.Error()) +} + +func TestHTTPErrorInternalError(t *testing.T) { + e := httperrors.NewHTTPError(http.StatusInternalServerError, httperrors.HTTPErrorTypeGeneric, http.StatusText(http.StatusInternalServerError)) + + e.Internal = sql.ErrConnDone + + require.Equal(t, "HTTPError 500 (generic): Internal Server Error, sql: connection is already closed", e.Error()) +} + +func TestHTTPErrorAdditionalData(t *testing.T) { + e := httperrors.NewHTTPError(http.StatusInternalServerError, httperrors.HTTPErrorTypeGeneric, http.StatusText(http.StatusInternalServerError)) + + e.AdditionalData = map[string]interface{}{ + "key1": "value1", + "key2": "value2", + } + + require.Equal(t, "HTTPError 500 (generic): Internal Server Error. Additional: key1=value1, key2=value2", e.Error()) +} + +var valErrs = append(make([]*types.HTTPValidationErrorDetail, 0, 2), &types.HTTPValidationErrorDetail{ + Key: swag.String("test1"), + In: swag.String("body.test1"), + Error: swag.String("ValidationError"), +}, &types.HTTPValidationErrorDetail{ + Key: swag.String("test2"), + In: swag.String("body.test2"), + Error: swag.String("Validation Error"), +}) + +func TestHTTPValidationErrorSimple(t *testing.T) { + e := httperrors.NewHTTPValidationError(http.StatusBadRequest, httperrors.HTTPErrorTypeGeneric, http.StatusText(http.StatusBadRequest), valErrs) + require.Equal(t, "HTTPValidationError 400 (generic): Bad Request - Validation: test1 (in body.test1): ValidationError, test2 (in body.test2): Validation Error", e.Error()) +} + +func TestHTTPValidationErrorDetail(t *testing.T) { + e := httperrors.NewHTTPValidationErrorWithDetail(http.StatusBadRequest, httperrors.HTTPErrorTypeGeneric, http.StatusText(http.StatusBadRequest), valErrs, "Did API spec change?") + require.Equal(t, "HTTPValidationError 400 (generic): Bad Request - Did API spec change? - Validation: test1 (in body.test1): ValidationError, test2 (in body.test2): Validation Error", e.Error()) +} + +func TestHTTPValidationErrorInternalError(t *testing.T) { + e := httperrors.NewHTTPValidationError(http.StatusBadRequest, httperrors.HTTPErrorTypeGeneric, http.StatusText(http.StatusBadRequest), valErrs) + + e.Internal = sql.ErrConnDone + + require.Equal(t, "HTTPValidationError 400 (generic): Bad Request, sql: connection is already closed - Validation: test1 (in body.test1): ValidationError, test2 (in body.test2): Validation Error", e.Error()) +} + +func TestHTTPValidationErrorAdditionalData(t *testing.T) { + e := httperrors.NewHTTPValidationError(http.StatusBadRequest, httperrors.HTTPErrorTypeGeneric, http.StatusText(http.StatusBadRequest), valErrs) + + e.AdditionalData = map[string]interface{}{ + "key1": "value1", + "key2": "value2", + } + + require.Equal(t, "HTTPValidationError 400 (generic): Bad Request. Additional: key1=value1, key2=value2 - Validation: test1 (in body.test1): ValidationError, test2 (in body.test2): Validation Error", e.Error()) +} diff --git a/internal/api/middleware/auth.go b/internal/api/middleware/auth.go index 1df3abaf..58663347 100644 --- a/internal/api/middleware/auth.go +++ b/internal/api/middleware/auth.go @@ -2,6 +2,7 @@ package middleware import ( "database/sql" + "errors" "fmt" "net/http" "time" @@ -14,6 +15,7 @@ import ( "github.com/go-openapi/strfmt" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "github.com/rs/zerolog/log" "github.com/volatiletech/sqlboiler/v4/queries/qm" ) @@ -22,9 +24,10 @@ var ( ErrUnauthorizedLastAuthenticatedAtExceeded = httperrors.NewHTTPError(http.StatusUnauthorized, "LAST_AUTHENTICATED_AT_EXCEEDED", "LastAuthenticatedAt timestamp exceeds threshold, re-authentication required") ErrForbiddenUserDeactivated = httperrors.NewHTTPError(http.StatusForbidden, "USER_DEACTIVATED", "User account is deactivated") ErrForbiddenMissingScopes = httperrors.NewHTTPError(http.StatusForbidden, "MISSING_SCOPES", "User is missing required scopes") + ErrAuthTokenValidationFailed = errors.New("auth token validation failed") ) -// Controls the type of authentication check performed for a specific route or group +// AuthMode controls the type of authentication check performed for a specific route or group type AuthMode int const ( @@ -95,7 +98,7 @@ const ( AuthTokenSourceHeader AuthTokenSource = iota // AuthTokenSourceQuery retrieves the auth token from a query parameter, specified by TokenSourceKey AuthTokenSourceQuery - // AuthTOkenSourceForm retrieves the auth token from a form parameter, specified by TokenSourceKey + // AuthTokenSourceForm retrieves the auth token from a form parameter, specified by TokenSourceKey AuthTokenSourceForm ) @@ -152,6 +155,30 @@ func DefaultAuthTokenFormatValidator(token string) bool { return strfmt.IsUUID4(token) } +type AuthTokenValidator func(c echo.Context, config AuthConfig, token string) (auth.AuthenticationResult, error) + +func DefaultAuthTokenValidator(c echo.Context, config AuthConfig, token string) (auth.AuthenticationResult, error) { + accessToken, err := models.AccessTokens( + models.AccessTokenWhere.Token.EQ(token), + qm.Load(models.AccessTokenRels.User), + ).One(c.Request().Context(), config.S.DB) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.Trace().Err(err).Msg("Access token not found in database") + return auth.AuthenticationResult{}, ErrAuthTokenValidationFailed + } + + log.Error().Err(err).Msg("Failed to query for access token in database, aborting request") + return auth.AuthenticationResult{}, echo.ErrInternalServerError + } + + return auth.AuthenticationResult{ + Token: accessToken.Token, + User: accessToken.R.User, + ValidUntil: accessToken.ValidUntil, + }, nil +} + var ( DefaultAuthConfig = AuthConfig{ Mode: AuthModeRequired, @@ -161,6 +188,8 @@ var ( Scheme: "Bearer", Skipper: middleware.DefaultSkipper, FormatValidator: DefaultAuthTokenFormatValidator, + TokenValidator: DefaultAuthTokenValidator, + Scopes: []string{auth.AuthScopeApp.String()}, } ) @@ -173,6 +202,7 @@ type AuthConfig struct { Scheme string // Sets required token scheme (default: "Bearer") Skipper middleware.Skipper // Controls skipping of certain routes (default: no skipped routes) FormatValidator AuthTokenFormatValidator // Validates the format of the token retrieved + TokenValidator AuthTokenValidator // Validates token retrieved and returns associated user (default: performs lookup in access_tokens table) Scopes []string // List of scopes required to access endpoint (default: none required) } @@ -188,7 +218,7 @@ func (c AuthConfig) CheckLastAuthenticatedAt(user *models.User) bool { return time.Since(user.LastAuthenticatedAt.Time).Seconds() <= c.S.Config.Auth.LastAuthenticatedAtThreshold.Seconds() } -func (c AuthConfig) CheckScopes(user *models.User) bool { +func (c AuthConfig) CheckUserScopes(user *models.User) bool { if len(c.Scopes) == 0 { return true } @@ -235,6 +265,10 @@ func AuthWithConfig(config AuthConfig) echo.MiddlewareFunc { config.FormatValidator = DefaultAuthConfig.FormatValidator } + if config.TokenValidator == nil { + config.TokenValidator = DefaultAuthConfig.TokenValidator + } + return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { log := util.LogFromEchoContext(c).With().Str("middleware", "auth").Str("auth_mode", config.Mode.String()).Logger() @@ -259,7 +293,7 @@ func AuthWithConfig(config AuthConfig) echo.MiddlewareFunc { return ErrUnauthorizedLastAuthenticatedAtExceeded } - if !config.CheckScopes(user) { + if !config.CheckUserScopes(user) { log.Trace(). Strs("scopes", config.Scopes). Strs("user_scopes", user.Scopes). @@ -292,35 +326,36 @@ func AuthWithConfig(config AuthConfig) echo.MiddlewareFunc { return next(c) } - accessToken, err := models.AccessTokens( - qm.Load(models.AccessTokenRels.User), - models.AccessTokenWhere.Token.EQ(token), - ).One(c.Request().Context(), config.S.DB) + res, err := config.TokenValidator(c, config, token) if err != nil { - if err == sql.ErrNoRows { + if errors.Is(err, ErrAuthTokenValidationFailed) { if config.Mode == AuthModeTry { - log.Trace().Msg("Access token not found in database, but auth mode permits access, allowing request") + log.Trace().Msg("Auth token validation failed, but auth mode permits access, allowing request") return next(c) } - log.Trace().Msg("Access token not found in database, rejecting request") + log.Trace().Msg("Auth token validation failed, rejecting request") return config.FailureMode.Error() } - log.Trace().Err(err).Msg("Failed to query for access token in database, aborting request") + log.Trace().Err(err).Msg("Failed to validate auth token, aborting request") return echo.ErrInternalServerError } - user = accessToken.R.User + user = res.User - if time.Now().After(accessToken.ValidUntil) { - if config.Mode == AuthModeTry { - log.Trace().Time("valid_until", accessToken.ValidUntil).Str("user_id", user.ID).Msg("Access token is expired, but auth mode permits access, allowing request") - return next(c) - } + if res.ValidUntil.IsZero() { + log.Trace().Str("user_id", user.ID).Msg("Auth token has no expiry, allowing request") + } else { + if time.Now().After(res.ValidUntil) { + if config.Mode == AuthModeTry { + log.Trace().Time("valid_until", res.ValidUntil).Str("user_id", user.ID).Msg("Auth token is expired, but auth mode permits access, allowing request") + return next(c) + } - log.Trace().Time("valid_until", accessToken.ValidUntil).Str("user_id", user.ID).Msg("Access token is expired, rejecting request") - return config.FailureMode.Error() + log.Trace().Time("valid_until", res.ValidUntil).Str("user_id", user.ID).Msg("Auth token is expired, rejecting request") + return config.FailureMode.Error() + } } // ! User has been explicitly deactivated - we do not allow access here, even with AuthModeTry @@ -337,17 +372,17 @@ func AuthWithConfig(config AuthConfig) echo.MiddlewareFunc { return ErrUnauthorizedLastAuthenticatedAtExceeded } - if !config.CheckScopes(user) { + if !config.CheckUserScopes(user) { log.Trace(). - Time("last_authenticated_at", user.LastAuthenticatedAt.Time). - Dur("last_authenticated_at_threshold", config.S.Config.Auth.LastAuthenticatedAtThreshold). - Msg("Authentication already performed, but last authenticated at time exceeds threshold, rejecting request") + Strs("scopes", config.Scopes). + Strs("user_scopes", user.Scopes). + Msg("Authentication already performed, but user does not have required scopes, rejecting request") return ErrForbiddenMissingScopes } - auth.EnrichEchoContextWithCredentials(c, user, accessToken) + auth.EnrichEchoContextWithCredentials(c, res) - log.Trace().Str("user_id", user.ID).Msg("Access token is valid, allowing request") + log.Trace().Str("user_id", user.ID).Msg("Auth token is valid, allowing request") return next(c) } diff --git a/internal/api/middleware/cache_control.go b/internal/api/middleware/cache_control.go new file mode 100644 index 00000000..e949f682 --- /dev/null +++ b/internal/api/middleware/cache_control.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "context" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +var ( + DefaultCacheControlConfig = CacheControlConfig{ + Skipper: middleware.DefaultSkipper, + } +) + +type CacheControlConfig struct { + Skipper middleware.Skipper +} + +func CacheControl() echo.MiddlewareFunc { + return CacheControlWithConfig(DefaultCacheControlConfig) +} + +func CacheControlWithConfig(config CacheControlConfig) echo.MiddlewareFunc { + if config.Skipper == nil { + config.Skipper = DefaultCacheControlConfig.Skipper + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if config.Skipper(c) { + return next(c) + } + + cacheControl := c.Request().Header.Get(util.HTTPHeaderCacheControl) + if len(cacheControl) > 0 { + directive := util.ParseCacheControlHeader(cacheControl) + + ctx := c.Request().Context() + + l := util.LogFromContext(ctx).With().Str("cacheControl", directive.String()).Logger() + ctx = l.WithContext(ctx) + + ctx = context.WithValue(ctx, util.CTXKeyCacheControl, directive) + + l.Trace().Msg("Setting cache control directive for request") + + c.SetRequest(c.Request().WithContext(ctx)) + } + + return next(c) + } + } +} diff --git a/internal/api/middleware/logger.go b/internal/api/middleware/logger.go index 1a3a10a8..6b5c5390 100644 --- a/internal/api/middleware/logger.go +++ b/internal/api/middleware/logger.go @@ -3,8 +3,8 @@ package middleware import ( "bufio" "bytes" + "context" "io" - "io/ioutil" "net" "net/http" "net/url" @@ -38,12 +38,12 @@ func DefaultRequestBodyLogSkipper(req *http.Request) bool { // ResponseBodyLogSkipper defines a function to skip logging certain response bodies. // Returning true skips logging the payload of the response. -type ResponseBodyLogSkipper func(res *echo.Response) bool +type ResponseBodyLogSkipper func(req *http.Request, res *echo.Response) bool // DefaultResponseBodyLogSkipper returns false for all responses with Content-Type // application/json, preventing logging for all other types of payloads as those // might contain binary or URL-encoded data unfit for logging purposes. -func DefaultResponseBodyLogSkipper(res *echo.Response) bool { +func DefaultResponseBodyLogSkipper(req *http.Request, res *echo.Response) bool { contentType := res.Header().Get(echo.HeaderContentType) switch { case strings.HasPrefix(contentType, echo.MIMEApplicationJSON): @@ -196,19 +196,19 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc { Str("bytes_in", in), ).Logger() le := l.WithLevel(config.Level) - req = req.WithContext(l.WithContext(req.Context())) + req = req.WithContext(l.WithContext(context.WithValue(req.Context(), util.CTXKeyRequestID, id))) if config.LogRequestBody && !config.RequestBodyLogSkipper(req) { - var reqBody []byte = nil + var reqBody []byte var err error if req.Body != nil { - reqBody, err = ioutil.ReadAll(req.Body) + reqBody, err = io.ReadAll(req.Body) if err != nil { l.Error().Err(err).Msg("Failed to read body while logging request") return err } - req.Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) + req.Body = io.NopCloser(bytes.NewBuffer(reqBody)) } le = le.Bytes("req_body", config.RequestBodyLogReplacer(reqBody)) @@ -258,7 +258,7 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc { Err(err), ) - if config.LogResponseBody && !config.ResponseBodyLogSkipper(res) { + if config.LogResponseBody && !config.ResponseBodyLogSkipper(req, res) { lle = lle.Bytes("res_body", config.ResponseBodyLogReplacer(resBody.Bytes())) } if config.LogResponseHeader { diff --git a/internal/api/middleware/no_cache.go b/internal/api/middleware/no_cache.go new file mode 100644 index 00000000..0759ecf9 --- /dev/null +++ b/internal/api/middleware/no_cache.go @@ -0,0 +1,90 @@ +package middleware + +// Based on https://github.com/LYY/echo-middleware (MIT License, https://github.com/LYY/echo-middleware/blob/master/LICENSE) +// Ported from Goji's middleware, source: +// https://github.com/zenazn/goji/tree/master/web/middleware (MIT License, https://github.com/zenazn/goji/blob/master/LICENSE) + +import ( + "time" + + "github.com/labstack/echo/v4" + middleware "github.com/labstack/echo/v4/middleware" +) + +type ( + // NoCacheConfig defines the config for nocache middleware. + NoCacheConfig struct { + // Skipper defines a function to skip middleware. + Skipper middleware.Skipper + } +) + +var ( + // Unix epoch time + epoch = time.Unix(0, 0).Format(time.RFC1123) + + // Taken from https://github.com/mytrile/nocache + noCacheHeaders = map[string]string{ + "Expires": epoch, + "Cache-Control": "no-cache, private, max-age=0", + "Pragma": "no-cache", + "X-Accel-Expires": "0", + } + etagHeaders = []string{ + "ETag", + "If-Modified-Since", + "If-Match", + "If-None-Match", + "If-Range", + "If-Unmodified-Since", + } + // DefaultNoCacheConfig is the default nocache middleware config. + DefaultNoCacheConfig = NoCacheConfig{ + Skipper: middleware.DefaultSkipper, + } +) + +// NoCache is a simple piece of middleware that sets a number of HTTP headers to prevent +// a router (or subrouter) from being cached by an upstream proxy and/or client. +// +// As per http://wiki.nginx.org/HttpProxyModule - NoCache sets: +// Expires: Thu, 01 Jan 1970 00:00:00 UTC +// Cache-Control: no-cache, private, max-age=0 +// X-Accel-Expires: 0 +// Pragma: no-cache (for HTTP/1.0 proxies/clients) +func NoCache() echo.MiddlewareFunc { + return NoCacheWithConfig(DefaultNoCacheConfig) +} + +// NoCacheWithConfig returns a nocache middleware with config. +func NoCacheWithConfig(config NoCacheConfig) echo.MiddlewareFunc { + // Defaults + if config.Skipper == nil { + config.Skipper = DefaultNoCacheConfig.Skipper + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) (err error) { + if config.Skipper(c) { + return next(c) + } + + req := c.Request() + + // Delete any ETag headers that may have been set + for _, v := range etagHeaders { + if req.Header.Get(v) != "" { + req.Header.Del(v) + } + } + + // Set our NoCache headers + res := c.Response() + for k, v := range noCacheHeaders { + res.Header().Set(k, v) + } + + return next(c) + } + } +} diff --git a/internal/api/middleware/noop.go b/internal/api/middleware/noop.go new file mode 100644 index 00000000..b6986e12 --- /dev/null +++ b/internal/api/middleware/noop.go @@ -0,0 +1,11 @@ +package middleware + +import "github.com/labstack/echo/v4" + +func Noop() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + return next(c) + } + } +} diff --git a/internal/api/router/router.go b/internal/api/router/router.go index af056851..695fb612 100644 --- a/internal/api/router/router.go +++ b/internal/api/router/router.go @@ -1,12 +1,19 @@ package router import ( + "net/http" + "runtime" + "strings" + "allaboutapps.dev/aw/go-starter/internal/api" "allaboutapps.dev/aw/go-starter/internal/api/handlers" "allaboutapps.dev/aw/go-starter/internal/api/middleware" "github.com/labstack/echo/v4" echoMiddleware "github.com/labstack/echo/v4/middleware" "github.com/rs/zerolog/log" + + // #nosec G108 - pprof handlers (conditionally made available via http.DefaultServeMux) + "net/http/pprof" ) func Init(s *api.Server) { @@ -22,19 +29,121 @@ func Init(s *api.Server) { // --- // General middleware - s.Echo.Pre(echoMiddleware.RemoveTrailingSlash()) - - s.Echo.Use(echoMiddleware.Recover()) - s.Echo.Use(echoMiddleware.RequestID()) - s.Echo.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ - Level: s.Config.Logger.RequestLevel, - LogRequestBody: s.Config.Logger.LogRequestBody, - LogRequestHeader: s.Config.Logger.LogRequestHeader, - LogRequestQuery: s.Config.Logger.LogRequestQuery, - LogResponseBody: s.Config.Logger.LogResponseBody, - LogResponseHeader: s.Config.Logger.LogResponseHeader, - })) - s.Echo.Use(echoMiddleware.CORS()) + if s.Config.Echo.EnableTrailingSlashMiddleware { + s.Echo.Pre(echoMiddleware.RemoveTrailingSlash()) + } else { + log.Warn().Msg("Disabling trailing slash middleware due to environment config") + } + + if s.Config.Echo.EnableRecoverMiddleware { + s.Echo.Use(echoMiddleware.Recover()) + } else { + log.Warn().Msg("Disabling recover middleware due to environment config") + } + + if s.Config.Echo.EnableSecureMiddleware { + s.Echo.Use(echoMiddleware.SecureWithConfig(echoMiddleware.SecureConfig{ + Skipper: echoMiddleware.DefaultSecureConfig.Skipper, + XSSProtection: s.Config.Echo.SecureMiddleware.XSSProtection, + ContentTypeNosniff: s.Config.Echo.SecureMiddleware.ContentTypeNosniff, + XFrameOptions: s.Config.Echo.SecureMiddleware.XFrameOptions, + HSTSMaxAge: s.Config.Echo.SecureMiddleware.HSTSMaxAge, + HSTSExcludeSubdomains: s.Config.Echo.SecureMiddleware.HSTSExcludeSubdomains, + ContentSecurityPolicy: s.Config.Echo.SecureMiddleware.ContentSecurityPolicy, + CSPReportOnly: s.Config.Echo.SecureMiddleware.CSPReportOnly, + HSTSPreloadEnabled: s.Config.Echo.SecureMiddleware.HSTSPreloadEnabled, + ReferrerPolicy: s.Config.Echo.SecureMiddleware.ReferrerPolicy, + })) + } else { + log.Warn().Msg("Disabling secure middleware due to environment config") + } + + if s.Config.Echo.EnableRequestIDMiddleware { + s.Echo.Use(echoMiddleware.RequestID()) + } else { + log.Warn().Msg("Disabling request ID middleware due to environment config") + } + + if s.Config.Echo.EnableLoggerMiddleware { + s.Echo.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ + Level: s.Config.Logger.RequestLevel, + LogRequestBody: s.Config.Logger.LogRequestBody, + LogRequestHeader: s.Config.Logger.LogRequestHeader, + LogRequestQuery: s.Config.Logger.LogRequestQuery, + LogResponseBody: s.Config.Logger.LogResponseBody, + LogResponseHeader: s.Config.Logger.LogResponseHeader, + RequestBodyLogSkipper: func(req *http.Request) bool { + // We skip all body logging for auth endpoints as these might contain credentials + if strings.HasPrefix(req.URL.Path, "/api/v1/auth") { + return true + } + + return middleware.DefaultRequestBodyLogSkipper(req) + }, + ResponseBodyLogSkipper: func(req *http.Request, res *echo.Response) bool { + // We skip all body logging for auth endpoints as these might contain credentials + if strings.HasPrefix(req.URL.Path, "/api/v1/auth") { + return true + } + + return middleware.DefaultResponseBodyLogSkipper(req, res) + }, + Skipper: func(c echo.Context) bool { + // We skip logging of readiness and liveness endpoints + switch c.Path() { + case "/-/ready", "/-/healthy": + return true + } + return false + }, + })) + } else { + log.Warn().Msg("Disabling logger middleware due to environment config") + } + + if s.Config.Echo.EnableCORSMiddleware { + s.Echo.Use(echoMiddleware.CORS()) + } else { + log.Warn().Msg("Disabling CORS middleware due to environment config") + } + + if s.Config.Echo.EnableCacheControlMiddleware { + s.Echo.Use(middleware.CacheControl()) + } else { + log.Warn().Msg("Disabling cache control middleware due to environment config") + } + + if s.Config.Pprof.Enable { + + pprofAuthMiddleware := middleware.Noop() + + if s.Config.Pprof.EnableManagementKeyAuth { + pprofAuthMiddleware = echoMiddleware.KeyAuthWithConfig(echoMiddleware.KeyAuthConfig{ + KeyLookup: "query:mgmt-secret", + Validator: func(key string, c echo.Context) (bool, error) { + return key == s.Config.Management.Secret, nil + }, + }) + } + + s.Echo.GET("/debug/pprof", echo.WrapHandler(http.HandlerFunc(pprof.Index)), pprofAuthMiddleware) + s.Echo.Any("/debug/pprof/*", echo.WrapHandler(http.DefaultServeMux), pprofAuthMiddleware) + + log.Warn().Bool("EnableManagementKeyAuth", s.Config.Pprof.EnableManagementKeyAuth).Msg("Pprof http handlers are available at /debug/pprof") + + if s.Config.Pprof.RuntimeBlockProfileRate != 0 { + runtime.SetBlockProfileRate(s.Config.Pprof.RuntimeBlockProfileRate) + log.Warn().Int("RuntimeBlockProfileRate", s.Config.Pprof.RuntimeBlockProfileRate).Msg("Pprof runtime.SetBlockProfileRate") + } + + if s.Config.Pprof.RuntimeMutexProfileFraction != 0 { + runtime.SetMutexProfileFraction(s.Config.Pprof.RuntimeMutexProfileFraction) + log.Warn().Int("RuntimeMutexProfileFraction", s.Config.Pprof.RuntimeMutexProfileFraction).Msg("Pprof runtime.SetMutexProfileFraction") + } + } + + // Add your custom / additional middlewares here. + // see https://echo.labstack.com/middleware // --- // Initialize our general groups and set middleware to use above them @@ -44,7 +153,7 @@ func Init(s *api.Server) { // Unsecured base group available at /** Root: s.Echo.Group(""), - // Management endpoints, secured by key auth (query param), available at /-/** + // Management endpoints, uncacheable, secured by key auth (query param), available at /-/** Management: s.Echo.Group("/-", echoMiddleware.KeyAuthWithConfig(echoMiddleware.KeyAuthConfig{ KeyLookup: "query:mgmt-secret", Validator: func(key string, c echo.Context) (bool, error) { @@ -57,7 +166,7 @@ func Init(s *api.Server) { } return false }, - })), + }), middleware.NoCache()), // OAuth2, unsecured or secured by bearer auth, available at /api/v1/auth/** APIV1Auth: s.Echo.Group("/api/v1/auth", middleware.AuthWithConfig(middleware.AuthConfig{ diff --git a/internal/api/router/router_test.go b/internal/api/router/router_test.go new file mode 100644 index 00000000..329a03b6 --- /dev/null +++ b/internal/api/router/router_test.go @@ -0,0 +1,70 @@ +package router_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/api" + "allaboutapps.dev/aw/go-starter/internal/config" + "allaboutapps.dev/aw/go-starter/internal/test" + "github.com/stretchr/testify/require" +) + +func TestPprofEnabled(t *testing.T) { + config := config.DefaultServiceConfigFromEnv() + + // these are typically our default values, however we force set them here to ensure those are set while test execution. + config.Pprof.Enable = true + config.Pprof.EnableManagementKeyAuth = true + + test.WithTestServerConfigurable(t, config, func(s *api.Server) { + // heap (test any) + res := test.PerformRequest(t, s, "GET", "/debug/pprof/heap?mgmt-secret="+s.Config.Management.Secret, nil, nil) + require.Equal(t, 200, res.Result().StatusCode) + + // index + res = test.PerformRequest(t, s, "GET", "/debug/pprof?mgmt-secret="+s.Config.Management.Secret, nil, nil) + require.Equal(t, 200, res.Result().StatusCode) + + res = test.PerformRequest(t, s, "GET", "/debug/pprof/heap?mgmt-secret=wrongsecret", nil, nil) + require.Equal(t, 401, res.Result().StatusCode) + }) +} + +func TestPprofEnabledNoAuth(t *testing.T) { + config := config.DefaultServiceConfigFromEnv() + + // these are typically our default values, however we force set them here to ensure those are set while test execution. + config.Pprof.Enable = true + config.Pprof.EnableManagementKeyAuth = false + + test.WithTestServerConfigurable(t, config, func(s *api.Server) { + res := test.PerformRequest(t, s, "GET", "/debug/pprof/heap?", nil, nil) + require.Equal(t, 200, res.Result().StatusCode) + }) +} + +func TestPprofDisabled(t *testing.T) { + config := config.DefaultServiceConfigFromEnv() + config.Pprof.Enable = false + + test.WithTestServerConfigurable(t, config, func(s *api.Server) { + res := test.PerformRequest(t, s, "GET", "/debug/pprof/heap?mgmt-secret="+s.Config.Management.Secret, nil, nil) + require.Equal(t, 404, res.Result().StatusCode) + }) +} + +func TestMiddlewaresDisabled(t *testing.T) { + // disable all + config := config.DefaultServiceConfigFromEnv() + config.Echo.EnableCORSMiddleware = false + config.Echo.EnableLoggerMiddleware = false + config.Echo.EnableRecoverMiddleware = false + config.Echo.EnableRequestIDMiddleware = false + config.Echo.EnableSecureMiddleware = false + config.Echo.EnableTrailingSlashMiddleware = false + + test.WithTestServerConfigurable(t, config, func(s *api.Server) { + res := test.PerformRequest(t, s, "GET", "/-/ready", nil, nil) + require.Equal(t, 200, res.Result().StatusCode) + }) +} diff --git a/internal/api/server.go b/internal/api/server.go index d73cdea9..19eeb78a 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "fmt" "allaboutapps.dev/aw/go-starter/internal/config" "allaboutapps.dev/aw/go-starter/internal/mailer" @@ -80,12 +81,15 @@ func (s *Server) InitDB(ctx context.Context) error { return nil } -func (s *Server) InitMailer(mock ...bool) error { - if len(mock) > 0 && mock[0] { +func (s *Server) InitMailer() error { + switch config.MailerTransporter(s.Config.Mailer.Transporter) { + case config.MailerTransporterMock: log.Warn().Msg("Initializing mock mailer") s.Mailer = mailer.New(s.Config.Mailer, transport.NewMock()) - } else { + case config.MailerTransporterSMTP: s.Mailer = mailer.New(s.Config.Mailer, transport.NewSMTP(s.Config.SMTP)) + default: + return fmt.Errorf("Unsupported mail transporter: %s", s.Config.Mailer.Transporter) } return s.Mailer.ParseTemplates() @@ -103,7 +107,7 @@ func (s *Server) InitPush() error { } if s.Config.Push.UseMockProvider { - log.Warn().Msg("Initializing push mock provider") + log.Warn().Msg("Initializing mock push provider") mockProvider := provider.NewMock(push.ProviderTypeFCM) s.Push.RegisterProvider(mockProvider) } @@ -124,7 +128,7 @@ func (s *Server) Start() error { } func (s *Server) Shutdown(ctx context.Context) error { - log.Info().Msg("Shutting down server") + log.Warn().Msg("Shutting down server") if s.DB != nil { log.Debug().Msg("Closing database connection") diff --git a/internal/config/build_args.go b/internal/config/build_args.go new file mode 100644 index 00000000..7972bc23 --- /dev/null +++ b/internal/config/build_args.go @@ -0,0 +1,18 @@ +package config + +import "fmt" + +// The following vars are automatically injected via -ldflags. +// See Makefile target "make go-build" and make var $(LDFLAGS). +// No need to change them here. +// https://www.digitalocean.com/community/tutorials/using-ldflags-to-set-version-information-for-go-applications +var ( + ModuleName = "build.local/misses/ldflags" // e.g. "allaboutapps.dev/aw/go-starter" + Commit = "< 40 chars git commit hash via ldflags >" // e.g. "59cb7684dd0b0f38d68cd7db657cb614feba8f7e" + BuildDate = "1970-01-01T00:00:00+00:00" // e.g. "1970-01-01T00:00:00+00:00" +) + +// GetFormattedBuildArgs returns string representation of buildsargs set via ldflags " @ ()" +func GetFormattedBuildArgs() string { + return fmt.Sprintf("%v @ %v (%v)", ModuleName, Commit, BuildDate) +} diff --git a/internal/config/db_config.go b/internal/config/db_config.go index 5dbdd1bb..25486ae7 100644 --- a/internal/config/db_config.go +++ b/internal/config/db_config.go @@ -8,18 +8,18 @@ import ( ) type Database struct { - Host string `json:"host"` - Port int `json:"port"` - Username string `json:"username"` - Password string `json:"password"` - Database string `json:"database"` - AdditionalParams map[string]string `json:"additionalParams,omitempty"` // Optional additional connection parameters mapped into the connection string - MaxOpenConns int `json:"maxOpenConns"` - MaxIdleConns int `json:"maxIdleConns"` - ConnMaxLifetime time.Duration `json:"connMaxLifetime"` + Host string + Port int + Username string + Password string `json:"-"` // sensitive + Database string + AdditionalParams map[string]string `json:",omitempty"` // Optional additional connection parameters mapped into the connection string + MaxOpenConns int + MaxIdleConns int + ConnMaxLifetime time.Duration } -// Generates a connection string to be passed to sql.Open or equivalents, assuming Postgres syntax +// ConnectionString generates a connection string to be passed to sql.Open or equivalents, assuming Postgres syntax func (c Database) ConnectionString() string { var b strings.Builder b.WriteString(fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", c.Host, c.Port, c.Username, c.Password, c.Database)) diff --git a/internal/config/db_config_test.go b/internal/config/db_config_test.go new file mode 100644 index 00000000..089cdbef --- /dev/null +++ b/internal/config/db_config_test.go @@ -0,0 +1,69 @@ +package config_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/config" +) + +// via https://github.com/allaboutapps/integresql/blob/master/pkg/manager/database_config_test.go +func TestDatabaseConnectionString(t *testing.T) { + tests := []struct { + name string + config config.Database + want string + }{ + { + name: "Simple", + config: config.Database{ + Host: "localhost", + Port: 5432, + Username: "simple", + Password: "database_config", + Database: "simple_database_config", + }, + want: "host=localhost port=5432 user=simple password=database_config dbname=simple_database_config sslmode=disable", + }, + { + name: "SSLMode", + config: config.Database{ + Host: "localhost", + Port: 5432, + Username: "simple", + Password: "database_config", + Database: "simple_database_config", + AdditionalParams: map[string]string{ + "sslmode": "prefer", + }, + }, + want: "host=localhost port=5432 user=simple password=database_config dbname=simple_database_config sslmode=prefer", + }, + { + name: "Complex", + config: config.Database{ + Host: "localhost", + Port: 5432, + Username: "simple", + Password: "database_config", + Database: "simple_database_config", + AdditionalParams: map[string]string{ + "connect_timeout": "10", + "sslmode": "verify-full", + "sslcert": "/app/certs/pg.pem", + "sslkey": "/app/certs/pg.key", + "sslrootcert": "/app/certs/pg_root.pem", + }, + }, + want: "host=localhost port=5432 user=simple password=database_config dbname=simple_database_config connect_timeout=10 sslcert=/app/certs/pg.pem sslkey=/app/certs/pg.key sslmode=verify-full sslrootcert=/app/certs/pg_root.pem", + }, + } + + for _, tt := range tests { + tt := tt // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables + t.Run(tt.name, func(t *testing.T) { + if got := tt.config.ConnectionString(); got != tt.want { + t.Errorf("invalid connection string, got %q, want %q", got, tt.want) + } + }) + } +} diff --git a/internal/config/mailer_config.go b/internal/config/mailer_config.go index 461619e9..ce4c5800 100644 --- a/internal/config/mailer_config.go +++ b/internal/config/mailer_config.go @@ -1,7 +1,19 @@ package config +type MailerTransporter string + +var ( + MailerTransporterMock MailerTransporter = "mock" + MailerTransporterSMTP MailerTransporter = "SMTP" +) + +func (m MailerTransporter) String() string { + return string(m) +} + type Mailer struct { DefaultSender string Send bool WebTemplatesEmailBaseDirAbs string + Transporter string } diff --git a/internal/config/server_config.go b/internal/config/server_config.go index 6f0c144b..d4886786 100644 --- a/internal/config/server_config.go +++ b/internal/config/server_config.go @@ -3,6 +3,7 @@ package config import ( "path/filepath" "runtime" + "sync" "time" "allaboutapps.dev/aw/go-starter/internal/mailer/transport" @@ -11,11 +12,45 @@ import ( "github.com/rs/zerolog" ) +var ( + config Server + configOnce sync.Once +) + type EchoServer struct { Debug bool ListenAddress string HideInternalServerErrorDetails bool BaseURL string + EnableCORSMiddleware bool + EnableLoggerMiddleware bool + EnableRecoverMiddleware bool + EnableRequestIDMiddleware bool + EnableTrailingSlashMiddleware bool + EnableSecureMiddleware bool + EnableCacheControlMiddleware bool + SecureMiddleware EchoServerSecureMiddleware +} + +type PprofServer struct { + Enable bool + EnableManagementKeyAuth bool + RuntimeBlockProfileRate int + RuntimeMutexProfileFraction int +} + +// EchoServerSecureMiddleware represents a subset of echo's secure middleware config relevant to the app server. +// https://github.com/labstack/echo/blob/master/middleware/secure.go +type EchoServerSecureMiddleware struct { + XSSProtection string + ContentTypeNosniff string + XFrameOptions string + HSTSMaxAge int + HSTSExcludeSubdomains bool + ContentSecurityPolicy string + CSPReportOnly bool + HSTSPreloadEnabled bool + ReferrerPolicy string } type AuthServer struct { @@ -31,7 +66,11 @@ type PathsServer struct { } type ManagementServer struct { - Secret string + Secret string `json:"-"` // sensitive + ReadinessTimeout time.Duration + LivenessTimeout time.Duration + ProbeWriteablePathsAbs []string + ProbeWriteableTouchfile string } type FrontendServer struct { @@ -40,18 +79,20 @@ type FrontendServer struct { } type LoggerServer struct { - Level zerolog.Level - RequestLevel zerolog.Level - LogRequestBody bool - LogRequestHeader bool - LogRequestQuery bool - LogResponseBody bool - LogResponseHeader bool + Level zerolog.Level + RequestLevel zerolog.Level + LogRequestBody bool + LogRequestHeader bool + LogRequestQuery bool + LogResponseBody bool + LogResponseHeader bool + PrettyPrintConsole bool } type Server struct { Database Database Echo EchoServer + Pprof PprofServer Paths PathsServer Auth AuthServer Management ManagementServer @@ -63,76 +104,121 @@ type Server struct { FCMConfig provider.FCMConfig } +// DefaultServiceConfigFromEnv returns the server config as parsed from environment variables +// and their respective defaults defined below. +// We don't expect that ENV_VARs change while we are running our application or our tests +// (and it would be a bad thing to do anyways with parallel testing). +// Do NOT use os.Setenv / os.Unsetenv in tests utilizing DefaultServiceConfigFromEnv()! +// We can optimize here to do ENV_VAR parsing only once. func DefaultServiceConfigFromEnv() Server { - return Server{ - Database: Database{ - Host: util.GetEnv("PGHOST", "postgres"), - Port: util.GetEnvAsInt("PGPORT", 5432), - Database: util.GetEnv("PGDATABASE", "development"), - Username: util.GetEnv("PGUSER", "dbuser"), - Password: util.GetEnv("PGPASSWORD", ""), - AdditionalParams: map[string]string{ - "sslmode": util.GetEnv("PGSSLMODE", "disable"), + configOnce.Do(func() { + config = Server{ + Database: Database{ + Host: util.GetEnv("PGHOST", "postgres"), + Port: util.GetEnvAsInt("PGPORT", 5432), + Database: util.GetEnv("PGDATABASE", "development"), + Username: util.GetEnv("PGUSER", "dbuser"), + Password: util.GetEnv("PGPASSWORD", ""), + AdditionalParams: map[string]string{ + "sslmode": util.GetEnv("PGSSLMODE", "disable"), + }, + MaxOpenConns: util.GetEnvAsInt("DB_MAX_OPEN_CONNS", runtime.NumCPU()*2), + MaxIdleConns: util.GetEnvAsInt("DB_MAX_IDLE_CONNS", 1), + ConnMaxLifetime: time.Second * time.Duration(util.GetEnvAsInt("DB_CONN_MAX_LIFETIME_SEC", 60)), + }, + Echo: EchoServer{ + Debug: util.GetEnvAsBool("SERVER_ECHO_DEBUG", false), + ListenAddress: util.GetEnv("SERVER_ECHO_LISTEN_ADDRESS", ":8080"), + HideInternalServerErrorDetails: util.GetEnvAsBool("SERVER_ECHO_HIDE_INTERNAL_SERVER_ERROR_DETAILS", true), + BaseURL: util.GetEnv("SERVER_ECHO_BASE_URL", "http://localhost:8080"), + EnableCORSMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_CORS_MIDDLEWARE", true), + EnableLoggerMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_LOGGER_MIDDLEWARE", true), + EnableRecoverMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_RECOVER_MIDDLEWARE", true), + EnableRequestIDMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_REQUEST_ID_MIDDLEWARE", true), + EnableTrailingSlashMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_TRAILING_SLASH_MIDDLEWARE", true), + EnableSecureMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_SECURE_MIDDLEWARE", true), + EnableCacheControlMiddleware: util.GetEnvAsBool("SERVER_ECHO_ENABLE_CACHE_CONTROL_MIDDLEWARE", true), + // see https://echo.labstack.com/middleware/secure + // see https://github.com/labstack/echo/blob/master/middleware/secure.go + SecureMiddleware: EchoServerSecureMiddleware{ + XSSProtection: util.GetEnv("SERVER_ECHO_SECURE_MIDDLEWARE_XSS_PROTECTION", "1; mode=block"), + ContentTypeNosniff: util.GetEnv("SERVER_ECHO_SECURE_MIDDLEWARE_CONTENT_TYPE_NOSNIFF", "nosniff"), + XFrameOptions: util.GetEnv("SERVER_ECHO_SECURE_MIDDLEWARE_X_FRAME_OPTIONS", "SAMEORIGIN"), + HSTSMaxAge: util.GetEnvAsInt("SERVER_ECHO_SECURE_MIDDLEWARE_HSTS_MAX_AGE", 0), + HSTSExcludeSubdomains: util.GetEnvAsBool("SERVER_ECHO_SECURE_MIDDLEWARE_HSTS_EXCLUDE_SUBDOMAINS", false), + ContentSecurityPolicy: util.GetEnv("SERVER_ECHO_SECURE_MIDDLEWARE_CONTENT_SECURITY_POLICY", ""), + CSPReportOnly: util.GetEnvAsBool("SERVER_ECHO_SECURE_MIDDLEWARE_CSP_REPORT_ONLY", false), + HSTSPreloadEnabled: util.GetEnvAsBool("SERVER_ECHO_SECURE_MIDDLEWARE_HSTS_PRELOAD_ENABLED", false), + ReferrerPolicy: util.GetEnv("SERVER_ECHO_SECURE_MIDDLEWARE_REFERRER_POLICY", ""), + }, + }, + Pprof: PprofServer{ + // https://golang.org/pkg/net/http/pprof/ + Enable: util.GetEnvAsBool("SERVER_PPROF_ENABLE", false), + EnableManagementKeyAuth: util.GetEnvAsBool("SERVER_PPROF_ENABLE_MANAGEMENT_KEY_AUTH", true), + RuntimeBlockProfileRate: util.GetEnvAsInt("SERVER_PPROF_RUNTIME_BLOCK_PROFILE_RATE", 0), + RuntimeMutexProfileFraction: util.GetEnvAsInt("SERVER_PPROF_RUNTIME_MUTEX_PROFILE_FRACTION", 0), + }, + Paths: PathsServer{ + // Please ALWAYS work with ABSOLUTE (ABS) paths from ENV_VARS (however you may resolve a project-relative to absolute for the default value) + APIBaseDirAbs: util.GetEnv("SERVER_PATHS_API_BASE_DIR_ABS", filepath.Join(util.GetProjectRootDir(), "/api")), // /app/api (swagger.yml) + MntBaseDirAbs: util.GetEnv("SERVER_PATHS_MNT_BASE_DIR_ABS", filepath.Join(util.GetProjectRootDir(), "/assets/mnt")), // /app/assets/mnt (user-generated content) + }, + Auth: AuthServer{ + AccessTokenValidity: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_ACCESS_TOKEN_VALIDITY", 86400)), + PasswordResetTokenValidity: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_PASSWORD_RESET_TOKEN_VALIDITY", 900)), + DefaultUserScopes: util.GetEnvAsStringArr("SERVER_AUTH_DEFAULT_USER_SCOPES", []string{"app"}), + LastAuthenticatedAtThreshold: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_LAST_AUTHENTICATED_AT_THRESHOLD", 900)), }, - MaxOpenConns: util.GetEnvAsInt("DB_MAX_OPEN_CONNS", runtime.NumCPU()*2), - MaxIdleConns: util.GetEnvAsInt("DB_MAX_IDLE_CONNS", 1), - ConnMaxLifetime: time.Second * time.Duration(util.GetEnvAsInt("DB_CONN_MAX_LIFETIME_SEC", 60)), - }, - Echo: EchoServer{ - Debug: util.GetEnvAsBool("SERVER_ECHO_DEBUG", false), - ListenAddress: util.GetEnv("SERVER_ECHO_LISTEN_ADDRESS", ":8080"), - HideInternalServerErrorDetails: util.GetEnvAsBool("SERVER_ECHO_HIDE_INTERNAL_SERVER_ERROR_DETAILS", true), - BaseURL: util.GetEnv("SERVER_ECHO_BASE_URL", "http://localhost:8080"), - }, - Paths: PathsServer{ - // Please ALWAYS work with ABSOLUTE (ABS) paths from ENV_VARS (however you may resolve a project-relative to absolute for the default value) - APIBaseDirAbs: util.GetEnv("SERVER_PATHS_API_BASE_DIR_ABS", filepath.Join(util.GetProjectRootDir(), "/api")), // /app/api (swagger.yml) - MntBaseDirAbs: util.GetEnv("SERVER_PATHS_MNT_BASE_DIR_ABS", filepath.Join(util.GetProjectRootDir(), "/assets/mnt")), // /app/assets/mnt (user-generated content) - }, - Auth: AuthServer{ - AccessTokenValidity: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_ACCESS_TOKEN_VALIDITY", 86400)), - PasswordResetTokenValidity: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_PASSWORD_RESET_TOKEN_VALIDITY", 900)), - DefaultUserScopes: util.GetEnvAsStringArr("SERVER_AUTH_DEFAULT_USER_SCOPES", []string{"app"}), - LastAuthenticatedAtThreshold: time.Second * time.Duration(util.GetEnvAsInt("SERVER_AUTH_LAST_AUTHENTICATED_AT_THRESHOLD", 900)), - }, - Management: ManagementServer{ - Secret: util.GetEnv("SERVER_MANAGEMENT_SECRET", "mgmt-pass"), - }, - Mailer: Mailer{ - DefaultSender: util.GetEnv("SERVER_MAILER_DEFAULT_SENDER", "go-starter@example.com"), - Send: util.GetEnvAsBool("SERVER_MAILER_SEND", true), - WebTemplatesEmailBaseDirAbs: util.GetEnv("SERVER_MAILER_WEB_TEMPLATES_EMAIL_BASE_DIR_ABS", filepath.Join(util.GetProjectRootDir(), "/web/templates/email")), // /app/web/templates/email - }, - SMTP: transport.SMTPMailTransportConfig{ - Host: util.GetEnv("SERVER_SMTP_HOST", "mailhog"), - Port: util.GetEnvAsInt("SERVER_SMTP_PORT", 1025), - Username: util.GetEnv("SERVER_SMTP_USERNAME", ""), - Password: util.GetEnv("SERVER_SMTP_PASSWORD", ""), - AuthType: transport.SMTPAuthTypeFromString(util.GetEnv("SERVER_SMTP_AUTH_TYPE", transport.SMTPAuthTypeNone.String())), - UseTLS: util.GetEnvAsBool("SERVER_SMTP_USE_TLS", false), - TLSConfig: nil, - }, - Frontend: FrontendServer{ - BaseURL: util.GetEnv("SERVER_FRONTEND_BASE_URL", "http://localhost:3000"), - PasswordResetEndpoint: util.GetEnv("SERVER_FRONTEND_PASSWORD_RESET_ENDPOINT", "/set-new-password"), - }, - Logger: LoggerServer{ - Level: util.LogLevelFromString(util.GetEnv("SERVER_LOGGER_LEVEL", zerolog.DebugLevel.String())), - RequestLevel: util.LogLevelFromString(util.GetEnv("SERVER_LOGGER_REQUEST_LEVEL", zerolog.DebugLevel.String())), - LogRequestBody: util.GetEnvAsBool("SERVER_LOGGER_LOG_REQUEST_BODY", false), - LogRequestHeader: util.GetEnvAsBool("SERVER_LOGGER_LOG_REQUEST_HEADER", false), - LogRequestQuery: util.GetEnvAsBool("SERVER_LOGGER_LOG_REQUEST_QUERY", false), - LogResponseBody: util.GetEnvAsBool("SERVER_LOGGER_LOG_RESPONSE_BODY", false), - LogResponseHeader: util.GetEnvAsBool("SERVER_LOGGER_LOG_RESPONSE_HEADER", false), - }, - Push: PushService{ - UseFCMProvider: util.GetEnvAsBool("SERVER_PUSH_USE_FCM", false), - UseMockProvider: util.GetEnvAsBool("SERVER_PUSH_USE_MOCK", true), - }, - FCMConfig: provider.FCMConfig{ - GoogleApplicationCredentials: util.GetEnv("GOOGLE_APPLICATION_CREDENTIALS", ""), - ProjectID: util.GetEnv("SERVER_FCM_PROJECT_ID", "no-fcm-project-id-set"), - ValidateOnly: util.GetEnvAsBool("SERVER_FCM_VALIDATE_ONLY", true), - }, - } + Management: ManagementServer{ + Secret: util.GetMgmtSecret("SERVER_MANAGEMENT_SECRET"), + ReadinessTimeout: time.Second * time.Duration(util.GetEnvAsInt("SERVER_MANAGEMENT_READINESS_TIMEOUT_SEC", 4)), + LivenessTimeout: time.Second * time.Duration(util.GetEnvAsInt("SERVER_MANAGEMENT_LIVENESS_TIMEOUT_SEC", 9)), + ProbeWriteablePathsAbs: util.GetEnvAsStringArr("SERVER_MANAGEMENT_PROBE_WRITEABLE_PATHS_ABS", []string{ + filepath.Join(util.GetProjectRootDir(), "/assets/mnt")}, ","), + ProbeWriteableTouchfile: util.GetEnv("SERVER_MANAGEMENT_PROBE_WRITEABLE_TOUCHFILE", ".healthy"), + }, + Mailer: Mailer{ + DefaultSender: util.GetEnv("SERVER_MAILER_DEFAULT_SENDER", "go-starter@example.com"), + Send: util.GetEnvAsBool("SERVER_MAILER_SEND", true), + WebTemplatesEmailBaseDirAbs: util.GetEnv("SERVER_MAILER_WEB_TEMPLATES_EMAIL_BASE_DIR_ABS", filepath.Join(util.GetProjectRootDir(), "/web/templates/email")), // /app/web/templates/email + Transporter: util.GetEnvEnum("SERVER_MAILER_TRANSPORTER", MailerTransporterMock.String(), []string{MailerTransporterSMTP.String(), MailerTransporterMock.String()}), + }, + SMTP: transport.SMTPMailTransportConfig{ + Host: util.GetEnv("SERVER_SMTP_HOST", "mailhog"), + Port: util.GetEnvAsInt("SERVER_SMTP_PORT", 1025), + Username: util.GetEnv("SERVER_SMTP_USERNAME", ""), + Password: util.GetEnv("SERVER_SMTP_PASSWORD", ""), + AuthType: transport.SMTPAuthTypeFromString(util.GetEnv("SERVER_SMTP_AUTH_TYPE", transport.SMTPAuthTypeNone.String())), + UseTLS: util.GetEnvAsBool("SERVER_SMTP_USE_TLS", false), + TLSConfig: nil, + }, + Frontend: FrontendServer{ + BaseURL: util.GetEnv("SERVER_FRONTEND_BASE_URL", "http://localhost:3000"), + PasswordResetEndpoint: util.GetEnv("SERVER_FRONTEND_PASSWORD_RESET_ENDPOINT", "/set-new-password"), + }, + Logger: LoggerServer{ + Level: util.LogLevelFromString(util.GetEnv("SERVER_LOGGER_LEVEL", zerolog.DebugLevel.String())), + RequestLevel: util.LogLevelFromString(util.GetEnv("SERVER_LOGGER_REQUEST_LEVEL", zerolog.DebugLevel.String())), + LogRequestBody: util.GetEnvAsBool("SERVER_LOGGER_LOG_REQUEST_BODY", false), + LogRequestHeader: util.GetEnvAsBool("SERVER_LOGGER_LOG_REQUEST_HEADER", false), + LogRequestQuery: util.GetEnvAsBool("SERVER_LOGGER_LOG_REQUEST_QUERY", false), + LogResponseBody: util.GetEnvAsBool("SERVER_LOGGER_LOG_RESPONSE_BODY", false), + LogResponseHeader: util.GetEnvAsBool("SERVER_LOGGER_LOG_RESPONSE_HEADER", false), + PrettyPrintConsole: util.GetEnvAsBool("SERVER_LOGGER_PRETTY_PRINT_CONSOLE", false), + }, + Push: PushService{ + UseFCMProvider: util.GetEnvAsBool("SERVER_PUSH_USE_FCM", false), + UseMockProvider: util.GetEnvAsBool("SERVER_PUSH_USE_MOCK", true), + }, + FCMConfig: provider.FCMConfig{ + GoogleApplicationCredentials: util.GetEnv("GOOGLE_APPLICATION_CREDENTIALS", ""), + ProjectID: util.GetEnv("SERVER_FCM_PROJECT_ID", "no-fcm-project-id-set"), + ValidateOnly: util.GetEnvAsBool("SERVER_FCM_VALIDATE_ONLY", true), + }, + } + + }) + + return config } diff --git a/internal/config/server_config_test.go b/internal/config/server_config_test.go new file mode 100644 index 00000000..9ef95a1e --- /dev/null +++ b/internal/config/server_config_test.go @@ -0,0 +1,17 @@ +package config_test + +import ( + "encoding/json" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/config" +) + +func TestPrintServiceEnv(t *testing.T) { + config := config.DefaultServiceConfigFromEnv() + _, err := json.MarshalIndent(config, "", " ") + + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go index cdb1498c..87a0070a 100644 --- a/internal/mailer/mailer.go +++ b/internal/mailer/mailer.go @@ -5,7 +5,7 @@ import ( "context" "errors" "html/template" - "io/ioutil" + "os" "path/filepath" "allaboutapps.dev/aw/go-starter/internal/config" @@ -35,7 +35,7 @@ func New(config config.Mailer, transport transport.MailTransporter) *Mailer { } func (m *Mailer) ParseTemplates() error { - files, err := ioutil.ReadDir(m.Config.WebTemplatesEmailBaseDirAbs) + files, err := os.ReadDir(m.Config.WebTemplatesEmailBaseDirAbs) if err != nil { log.Error().Str("dir", m.Config.WebTemplatesEmailBaseDirAbs).Err(err).Msg("Failed to read email templates directory while parsing templates") return err diff --git a/internal/mailer/mailer_test.go b/internal/mailer/mailer_test.go index ba6fbef9..c879f816 100644 --- a/internal/mailer/mailer_test.go +++ b/internal/mailer/mailer_test.go @@ -4,25 +4,24 @@ import ( "context" "testing" + "allaboutapps.dev/aw/go-starter/internal/api" + "allaboutapps.dev/aw/go-starter/internal/config" "allaboutapps.dev/aw/go-starter/internal/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestMailerSendPasswordReset(t *testing.T) { - t.Parallel() - ctx := context.Background() fixtures := test.Fixtures() m := test.NewTestMailer(t) - + //nolint:gosec passwordResetLink := "http://localhost/password/reset/12345" err := m.SendPasswordReset(ctx, fixtures.User1.Username.String, passwordResetLink) require.NoError(t, err) mt := test.GetTestMailerMockTransport(t, m) - mail := mt.GetLastSentMail() mails := mt.GetSentMails() require.NotNil(t, mail) @@ -34,3 +33,31 @@ func TestMailerSendPasswordReset(t *testing.T) { assert.Equal(t, "Password reset", mail.Subject) assert.Contains(t, string(mail.HTML), passwordResetLink) } + +func SkipTestMailerSendPasswordResetWithMailhog(t *testing.T) { + t.Skip() + ctx := context.Background() + fixtures := test.Fixtures() + + m := test.NewSMTPMailerFromDefaultEnv(t) + + //nolint:gosec + passwordResetLink := "http://localhost/password/reset/12345" + err := m.SendPasswordReset(ctx, fixtures.User1.Username.String, passwordResetLink) + require.NoError(t, err) +} + +func SkipTestMailerSendPasswordResetWithMailhogAndServer(t *testing.T) { + t.Skip() + ctx := context.Background() + fixtures := test.Fixtures() + + defaultConfig := config.DefaultServiceConfigFromEnv() + defaultConfig.Mailer.Transporter = config.MailerTransporterSMTP.String() + test.WithTestServerConfigurable(t, defaultConfig, func(s *api.Server) { + //nolint:gosec + passwordResetLink := "http://localhost/password/reset/12345" + err := s.Mailer.SendPasswordReset(ctx, fixtures.User1.Username.String, passwordResetLink) + require.NoError(t, err) + }) +} diff --git a/internal/mailer/transport/smtp_config.go b/internal/mailer/transport/smtp_config.go index d0482c18..16483993 100644 --- a/internal/mailer/transport/smtp_config.go +++ b/internal/mailer/transport/smtp_config.go @@ -9,9 +9,13 @@ import ( type SMTPAuthType int const ( + // SMTPAuthTypeNone indicates no SMTP authentication should be performed. SMTPAuthTypeNone SMTPAuthType = iota + // SMTPAuthTypePlain indicates SMTP authentication should be performed using the "AUTH PLAIN" protocol. SMTPAuthTypePlain + // SMTPAuthTypeCRAMMD5 indicates SMTP authentication should be performed using the "CRAM-MD5" protocol. SMTPAuthTypeCRAMMD5 + // SMTPAuthTypeLogin indicates SMTP authentication should be performed using the "LOGIN" protocol. SMTPAuthTypeLogin ) @@ -46,9 +50,9 @@ func SMTPAuthTypeFromString(s string) SMTPAuthType { type SMTPMailTransportConfig struct { Host string Port int - AuthType SMTPAuthType + AuthType SMTPAuthType `json:"-"` // iota Username string - Password string + Password string `json:"-"` // sensitive UseTLS bool - TLSConfig *tls.Config + TLSConfig *tls.Config `json:"-"` // pointer } diff --git a/internal/models/access_tokens.go b/internal/models/access_tokens.go index 260ae316..0fc67aaf 100644 --- a/internal/models/access_tokens.go +++ b/internal/models/access_tokens.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -47,6 +47,20 @@ var AccessTokenColumns = struct { UpdatedAt: "updated_at", } +var AccessTokenTableColumns = struct { + Token string + ValidUntil string + UserID string + CreatedAt string + UpdatedAt string +}{ + Token: "access_tokens.token", + ValidUntil: "access_tokens.valid_until", + UserID: "access_tokens.user_id", + CreatedAt: "access_tokens.created_at", + UpdatedAt: "access_tokens.updated_at", +} + // Generated where type whereHelperstring struct{ field string } @@ -136,7 +150,7 @@ var ( type ( // AccessTokenSlice is an alias for a slice of pointers to AccessToken. - // This should generally be used opposed to []AccessToken. + // This should almost always be used instead of []AccessToken. AccessTokenSlice []*AccessToken accessTokenQuery struct { diff --git a/internal/models/access_tokens_test.go b/internal/models/access_tokens_test.go index 9796d268..4be435d9 100644 --- a/internal/models/access_tokens_test.go +++ b/internal/models/access_tokens_test.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/app_user_profiles.go b/internal/models/app_user_profiles.go index ba4d6a08..1d9935e5 100644 --- a/internal/models/app_user_profiles.go +++ b/internal/models/app_user_profiles.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -45,6 +45,18 @@ var AppUserProfileColumns = struct { UpdatedAt: "updated_at", } +var AppUserProfileTableColumns = struct { + UserID string + LegalAcceptedAt string + CreatedAt string + UpdatedAt string +}{ + UserID: "app_user_profiles.user_id", + LegalAcceptedAt: "app_user_profiles.legal_accepted_at", + CreatedAt: "app_user_profiles.created_at", + UpdatedAt: "app_user_profiles.updated_at", +} + // Generated where type whereHelpernull_Time struct{ field string } @@ -111,7 +123,7 @@ var ( type ( // AppUserProfileSlice is an alias for a slice of pointers to AppUserProfile. - // This should generally be used opposed to []AppUserProfile. + // This should almost always be used instead of []AppUserProfile. AppUserProfileSlice []*AppUserProfile appUserProfileQuery struct { diff --git a/internal/models/app_user_profiles_test.go b/internal/models/app_user_profiles_test.go index 54403531..445bdfd2 100644 --- a/internal/models/app_user_profiles_test.go +++ b/internal/models/app_user_profiles_test.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/boil_main_test.go b/internal/models/boil_main_test.go index d5c10b8d..83a9a077 100644 --- a/internal/models/boil_main_test.go +++ b/internal/models/boil_main_test.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/boil_queries.go b/internal/models/boil_queries.go index 15a8511a..ec64c52c 100644 --- a/internal/models/boil_queries.go +++ b/internal/models/boil_queries.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/boil_queries_test.go b/internal/models/boil_queries_test.go index 22341988..e3738131 100644 --- a/internal/models/boil_queries_test.go +++ b/internal/models/boil_queries_test.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/boil_suites_test.go b/internal/models/boil_suites_test.go index 0bac9c7b..c3866a98 100644 --- a/internal/models/boil_suites_test.go +++ b/internal/models/boil_suites_test.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/boil_table_names.go b/internal/models/boil_table_names.go index df27af57..8f79392c 100644 --- a/internal/models/boil_table_names.go +++ b/internal/models/boil_table_names.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/boil_types.go b/internal/models/boil_types.go index 98545849..d2054546 100644 --- a/internal/models/boil_types.go +++ b/internal/models/boil_types.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/password_reset_tokens.go b/internal/models/password_reset_tokens.go index b525eb80..45023ce4 100644 --- a/internal/models/password_reset_tokens.go +++ b/internal/models/password_reset_tokens.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -47,6 +47,20 @@ var PasswordResetTokenColumns = struct { UpdatedAt: "updated_at", } +var PasswordResetTokenTableColumns = struct { + Token string + ValidUntil string + UserID string + CreatedAt string + UpdatedAt string +}{ + Token: "password_reset_tokens.token", + ValidUntil: "password_reset_tokens.valid_until", + UserID: "password_reset_tokens.user_id", + CreatedAt: "password_reset_tokens.created_at", + UpdatedAt: "password_reset_tokens.updated_at", +} + // Generated where var PasswordResetTokenWhere = struct { @@ -92,7 +106,7 @@ var ( type ( // PasswordResetTokenSlice is an alias for a slice of pointers to PasswordResetToken. - // This should generally be used opposed to []PasswordResetToken. + // This should almost always be used instead of []PasswordResetToken. PasswordResetTokenSlice []*PasswordResetToken passwordResetTokenQuery struct { diff --git a/internal/models/password_reset_tokens_test.go b/internal/models/password_reset_tokens_test.go index 47181996..a17e69ec 100644 --- a/internal/models/password_reset_tokens_test.go +++ b/internal/models/password_reset_tokens_test.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/psql_main_test.go b/internal/models/psql_main_test.go index 72fd4d45..a48c18c2 100644 --- a/internal/models/psql_main_test.go +++ b/internal/models/psql_main_test.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/psql_suites_test.go b/internal/models/psql_suites_test.go index bda7597b..e228a5cb 100644 --- a/internal/models/psql_suites_test.go +++ b/internal/models/psql_suites_test.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/psql_upsert.go b/internal/models/psql_upsert.go index c7116f93..7877945f 100644 --- a/internal/models/psql_upsert.go +++ b/internal/models/psql_upsert.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/push_tokens.go b/internal/models/push_tokens.go index 4dcd2429..1bcf6a92 100644 --- a/internal/models/push_tokens.go +++ b/internal/models/push_tokens.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -50,6 +50,22 @@ var PushTokenColumns = struct { UpdatedAt: "updated_at", } +var PushTokenTableColumns = struct { + ID string + Token string + Provider string + UserID string + CreatedAt string + UpdatedAt string +}{ + ID: "push_tokens.id", + Token: "push_tokens.token", + Provider: "push_tokens.provider", + UserID: "push_tokens.user_id", + CreatedAt: "push_tokens.created_at", + UpdatedAt: "push_tokens.updated_at", +} + // Generated where var PushTokenWhere = struct { @@ -97,7 +113,7 @@ var ( type ( // PushTokenSlice is an alias for a slice of pointers to PushToken. - // This should generally be used opposed to []PushToken. + // This should almost always be used instead of []PushToken. PushTokenSlice []*PushToken pushTokenQuery struct { diff --git a/internal/models/push_tokens_test.go b/internal/models/push_tokens_test.go index 135626d0..f4288845 100644 --- a/internal/models/push_tokens_test.go +++ b/internal/models/push_tokens_test.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/refresh_tokens.go b/internal/models/refresh_tokens.go index 26a45b79..4b156791 100644 --- a/internal/models/refresh_tokens.go +++ b/internal/models/refresh_tokens.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -44,6 +44,18 @@ var RefreshTokenColumns = struct { UpdatedAt: "updated_at", } +var RefreshTokenTableColumns = struct { + Token string + UserID string + CreatedAt string + UpdatedAt string +}{ + Token: "refresh_tokens.token", + UserID: "refresh_tokens.user_id", + CreatedAt: "refresh_tokens.created_at", + UpdatedAt: "refresh_tokens.updated_at", +} + // Generated where var RefreshTokenWhere = struct { @@ -87,7 +99,7 @@ var ( type ( // RefreshTokenSlice is an alias for a slice of pointers to RefreshToken. - // This should generally be used opposed to []RefreshToken. + // This should almost always be used instead of []RefreshToken. RefreshTokenSlice []*RefreshToken refreshTokenQuery struct { diff --git a/internal/models/refresh_tokens_test.go b/internal/models/refresh_tokens_test.go index e2d7e18f..c3f1b656 100644 --- a/internal/models/refresh_tokens_test.go +++ b/internal/models/refresh_tokens_test.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/models/users.go b/internal/models/users.go index d8c63b79..ce98f60e 100644 --- a/internal/models/users.go +++ b/internal/models/users.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models @@ -58,6 +58,26 @@ var UserColumns = struct { UpdatedAt: "updated_at", } +var UserTableColumns = struct { + ID string + Username string + Password string + IsActive string + Scopes string + LastAuthenticatedAt string + CreatedAt string + UpdatedAt string +}{ + ID: "users.id", + Username: "users.username", + Password: "users.password", + IsActive: "users.is_active", + Scopes: "users.scopes", + LastAuthenticatedAt: "users.last_authenticated_at", + CreatedAt: "users.created_at", + UpdatedAt: "users.updated_at", +} + // Generated where type whereHelpernull_String struct{ field string } @@ -174,7 +194,7 @@ var ( type ( // UserSlice is an alias for a slice of pointers to User. - // This should generally be used opposed to []User. + // This should almost always be used instead of []User. UserSlice []*User userQuery struct { diff --git a/internal/models/users_test.go b/internal/models/users_test.go index fcb770e7..581e8bdd 100644 --- a/internal/models/users_test.go +++ b/internal/models/users_test.go @@ -1,4 +1,4 @@ -// Code generated by SQLBoiler 4.2.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// Code generated by SQLBoiler 4.6.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. // This file is meant to be re-generated in place and/or deleted at any time. package models diff --git a/internal/push/provider/fcm_provider.go b/internal/push/provider/fcm_provider.go index 8f910461..22aafeaa 100644 --- a/internal/push/provider/fcm_provider.go +++ b/internal/push/provider/fcm_provider.go @@ -16,7 +16,7 @@ type FCM struct { } type FCMConfig struct { - GoogleApplicationCredentials string + GoogleApplicationCredentials string `json:"-"` // sensitive ProjectID string ValidateOnly bool } diff --git a/internal/push/service_test.go b/internal/push/service_test.go index 1313a25c..72763c6a 100644 --- a/internal/push/service_test.go +++ b/internal/push/service_test.go @@ -15,8 +15,6 @@ import ( ) func TestSendMessageSuccess(t *testing.T) { - t.Parallel() - test.WithTestPusher(t, func(p *push.Service, db *sql.DB) { ctx := context.Background() fixtures := test.Fixtures() @@ -33,8 +31,6 @@ func TestSendMessageSuccess(t *testing.T) { } func TestSendMessageSuccessWithGenericError(t *testing.T) { - t.Parallel() - test.WithTestPusher(t, func(p *push.Service, db *sql.DB) { ctx := context.Background() fixtures := test.Fixtures() @@ -52,8 +48,6 @@ func TestSendMessageSuccessWithGenericError(t *testing.T) { } func TestSendMessageWithInvalidToken(t *testing.T) { - t.Parallel() - test.WithTestPusher(t, func(p *push.Service, db *sql.DB) { ctx := context.Background() fixtures := test.Fixtures() @@ -82,8 +76,6 @@ func TestSendMessageWithInvalidToken(t *testing.T) { } func TestSendMessageWithNoProvider(t *testing.T) { - t.Parallel() - test.WithTestPusher(t, func(p *push.Service, db *sql.DB) { ctx := context.Background() @@ -104,8 +96,6 @@ func TestSendMessageWithNoProvider(t *testing.T) { } func TestSendMessageWithMultipleProvider(t *testing.T) { - t.Parallel() - test.WithTestPusher(t, func(p *push.Service, db *sql.DB) { ctx := context.Background() fixtures := test.Fixtures() diff --git a/internal/test/README.md b/internal/test/README.md index ae85cacc..b30a7976 100644 --- a/internal/test/README.md +++ b/internal/test/README.md @@ -16,6 +16,12 @@ This are your global db test fixtures, that are only available while testing. Ho Please use this convention to specify test only utility functions. +### Regarding snapshot testing + +Golden files can be created by using the `Snapshoter.Save(t TestingT, data ...interface{})` method. The snapshot can be configured to force an update, use a different replacer function or to set a different file location and suffix for the snaphot. + +A snapshot can be updated by either calling the `Update(true)` method, or by using the global override by setting the environment variable `TEST_UPDATE_GOLDEN` to `true`. To update all snapshots the call might look like this: `TEST_UPDATE_GOLDEN=true make test`. + ### `testdata` and `.` or `_` prefixed files Note that Go will ignore directories or files that begin with "." or "_", so you have more flexibility in terms of how you name your test data directory. diff --git a/internal/test/fixtures.go b/internal/test/fixtures.go index f2283054..4385bb36 100644 --- a/internal/test/fixtures.go +++ b/internal/test/fixtures.go @@ -11,15 +11,15 @@ import ( const ( PlainTestUserPassword = "password" - HashedTestUserPassword = "$argon2id$v=19$m=65536,t=1,p=4$RFO8ulg2c2zloG0029pAUQ$2Po6NUIhVCMm9vivVDuzo7k5KVWfZzJJfeXzC+n+row" + HashedTestUserPassword = "$argon2id$v=19$m=65536,t=1,p=4$RFO8ulg2c2zloG0029pAUQ$2Po6NUIhVCMm9vivVDuzo7k5KVWfZzJJfeXzC+n+row" //nolint:gosec ) -// A common interface for all model instances so they may be inserted via the Inserts() func +// Insertable represents a common interface for all model instances so they may be inserted via the Inserts() func type Insertable interface { Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error } -// The main definition which fixtures are available though Fixtures() +// FixtureMap represents the main definition which fixtures are available though Fixtures() type FixtureMap struct { User1 *models.User User1AppUserProfile *models.AppUserProfile @@ -37,8 +37,8 @@ type FixtureMap struct { User1PushTokenAPN *models.PushToken } -// We return a function wrapping our fixtures, tests are allowed to manipulate those -// each test (which may run concurrently) can use a fresh copy +// Fixtures returns a function wrapping our fixtures, which tests are allowed to manipulate. +// Each test (which may run concurrently) receives a fresh copy, preventing side effects between test runs. func Fixtures() FixtureMap { now := time.Now() f := FixtureMap{} @@ -132,7 +132,7 @@ func Fixtures() FixtureMap { return f } -// This function defines the order in which the fixtures will be inserted +// Inserts defines the order in which the fixtures will be inserted // into the test database func Inserts() []Insertable { fixtures := Fixtures() diff --git a/internal/test/fixtures_test.go b/internal/test/fixtures_test.go index 9f6d9799..16a64bde 100644 --- a/internal/test/fixtures_test.go +++ b/internal/test/fixtures_test.go @@ -1,4 +1,4 @@ -package test +package test_test import ( "context" @@ -6,17 +6,15 @@ import ( "testing" "allaboutapps.dev/aw/go-starter/internal/models" + "allaboutapps.dev/aw/go-starter/internal/test" _ "github.com/lib/pq" "github.com/volatiletech/null/v8" "github.com/volatiletech/sqlboiler/v4/boil" ) func TestFixturesReload(t *testing.T) { - - t.Parallel() - - WithTestDatabase(t, func(db *sql.DB) { - err := Fixtures().User1.Reload(context.Background(), db) + test.WithTestDatabase(t, func(db *sql.DB) { + err := test.Fixtures().User1.Reload(context.Background(), db) if err != nil { t.Error("failed to reload") @@ -28,10 +26,7 @@ func TestFixturesReload(t *testing.T) { } func TestInsert(t *testing.T) { - - t.Parallel() - - WithTestDatabase(t, func(db *sql.DB) { + test.WithTestDatabase(t, func(db *sql.DB) { userNew := models.User{ ID: "6d00d09b-fab3-43d8-a163-279fe7ba533e", @@ -51,12 +46,9 @@ func TestInsert(t *testing.T) { } func TestUpdate(t *testing.T) { + test.WithTestDatabase(t, func(db *sql.DB) { - t.Parallel() - - WithTestDatabase(t, func(db *sql.DB) { - - originalUser1 := Fixtures().User1 + originalUser1 := test.Fixtures().User1 updatedUser1 := models.User(*originalUser1) @@ -86,9 +78,9 @@ func TestUpdate(t *testing.T) { }) // with another testdatabase: - WithTestDatabase(t, func(db *sql.DB) { + test.WithTestDatabase(t, func(db *sql.DB) { - originalUser1 := Fixtures().User1 + originalUser1 := test.Fixtures().User1 // ensure our fixture is the same again! if originalUser1.Username != null.StringFrom("user1@example.com") { diff --git a/internal/test/helper_compare.go b/internal/test/helper_compare.go index 6254717a..abb13108 100644 --- a/internal/test/helper_compare.go +++ b/internal/test/helper_compare.go @@ -49,6 +49,8 @@ func CompareAllPayload(t *testing.T, base map[string]interface{}, toCheck map[st } strV := fmt.Sprintf("%v", v) + //revive:disable-next-line:var-naming + //nolint:revive kConv := keyFunc(k) compareStrV := fmt.Sprintf("%v", toCheck[kConv]) diff --git a/internal/test/helper_compare_test.go b/internal/test/helper_compare_test.go new file mode 100644 index 00000000..5d989705 --- /dev/null +++ b/internal/test/helper_compare_test.go @@ -0,0 +1,99 @@ +package test_test + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "allaboutapps.dev/aw/go-starter/internal/test" + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/strfmt/conv" + "github.com/stretchr/testify/require" +) + +func TestCompareFileHashes(t *testing.T) { + tmpDir := t.TempDir() + newFilePath := tmpDir + "example2.jpg" + filePath := filepath.Join(util.GetProjectRootDir(), "test", "testdata", "example.jpg") + + in, err := os.Open(filePath) + require.NoError(t, err) + defer in.Close() + + out, err := os.Create(newFilePath) + require.NoError(t, err) + defer out.Close() + + _, err = io.Copy(out, in) + require.NoError(t, err) + require.FileExists(t, newFilePath) + + test.CompareFileHashes(t, filePath, newFilePath) +} + +func TestCompareAllPayload(t *testing.T) { + payload := test.GenericPayload{ + "A": 1, + "B": "b", + "C": 2.3, + "D": true, + "E": "2020-02-01", + "F": conv.UUID4(strfmt.UUID4("0862573e-6ccb-4684-847d-276d3364e91e")), + "X_Y": "skiped", + } + response := map[string]string{ + "A": "1", + "B": "b", + "C": "2.3", + "D": "true", + "E": util.Date(2020, 2, 1, time.UTC).String(), + "F": "0862573e-6ccb-4684-847d-276d3364e91e", + } + + toSkip := map[string]bool{ + "X_Y": true, + } + test.CompareAllPayload(t, payload, response, toSkip) + + payload = test.GenericPayload{ + "a": 1, + "B": "b", + "C": 2.3, + "d": true, + "e": "2020-02-01", + "F": conv.UUID4(strfmt.UUID4("0862573e-6ccb-4684-847d-276d3364e91e")), + "X_Y": "skiped", + } + test.CompareAllPayload(t, payload, response, toSkip, func(s string) string { + return strings.ToUpper(s) + }) +} + +func TestCompareAll(t *testing.T) { + payload := map[string]string{ + "A": "1", + "B": "b", + "C": "2.3", + "D": "true", + "E": "2020-02-01", + "F": strfmt.UUID4("0862573e-6ccb-4684-847d-276d3364e91e").String(), + "X_Y": "skiped", + } + response := map[string]string{ + "A": "1", + "B": "b", + "C": "2.3", + "D": "true", + "E": util.Date(2020, 2, 1, time.UTC).String(), + "F": "0862573e-6ccb-4684-847d-276d3364e91e", + } + + toSkip := map[string]bool{ + "X_Y": true, + } + test.CompareAll(t, payload, response, toSkip) +} diff --git a/internal/test/helper_mappers.go b/internal/test/helper_mappers.go index 80e0bad0..8af6bcf1 100644 --- a/internal/test/helper_mappers.go +++ b/internal/test/helper_mappers.go @@ -6,7 +6,7 @@ import ( "strings" ) -// Returns a map of a given struct using a tag name as key and +// GetMapFromStructByTag returns a map of a given struct using a tag name as key and // the string of the property value as value. // inspired by: https://stackoverflow.com/questions/55879028/golang-get-structs-field-name-by-json-tag func GetMapFromStructByTag(tag string, s interface{}) map[string]string { @@ -22,6 +22,9 @@ func GetMapFromStructByTag(tag string, s interface{}) map[string]string { for i := 0; i < rt.NumField(); i++ { f := rt.Field(i) v := strings.Split(f.Tag.Get(tag), ",")[0] // use split to ignore tag "options" like omitempty, etc. + if v == "" { + continue + } fs := rv.Field(i) if fs.Kind() == reflect.Ptr { if !fs.IsNil() { diff --git a/internal/test/helper_mappers_test.go b/internal/test/helper_mappers_test.go new file mode 100644 index 00000000..5ca8dae3 --- /dev/null +++ b/internal/test/helper_mappers_test.go @@ -0,0 +1,84 @@ +package test_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/test" + "github.com/stretchr/testify/assert" +) + +func TestGetMapFromStruct(t *testing.T) { + type tmp struct { + A string + B int + C interface{} + D float32 + E bool + F *string + G *int + } + + f := "stringPtr" + g := 5 + x := tmp{ + A: "string", + B: 1, + C: tmp{ + A: "string2", + }, + D: 2.3, + E: true, + F: &f, + G: &g, + } + + xMap := test.GetMapFromStruct(x) + assert.Len(t, xMap, 7) + assert.Equal(t, "string", xMap["A"]) + assert.Equal(t, "1", xMap["B"]) + assert.Contains(t, xMap["C"], "string2") + assert.Equal(t, "2.3", xMap["D"]) + assert.Equal(t, "true", xMap["E"]) + assert.Equal(t, "stringPtr", xMap["F"]) + assert.Equal(t, "5", xMap["G"]) +} + +func TestGetMapFromStructByTag(t *testing.T) { + type tmp struct { + A string `x:"1,omitempty" y:"2"` + B int `x:"3"` + C interface{} `x:"12"` + D float32 `x:"2"` + E bool `x:"5"` + F *string `x:"4"` + G *int `x:"6"` + } + + f := "stringPtr" + g := 5 + x := tmp{ + A: "string", + B: 1, + C: tmp{ + A: "string2", + }, + D: 2.3, + E: true, + F: &f, + G: &g, + } + + xMap := test.GetMapFromStructByTag("x", x) + assert.Len(t, xMap, 7) + assert.Equal(t, "string", xMap["1"]) + assert.Equal(t, "1", xMap["3"]) + assert.Contains(t, xMap["12"], "string2") + assert.Equal(t, "2.3", xMap["2"]) + assert.Equal(t, "true", xMap["5"]) + assert.Equal(t, "stringPtr", xMap["4"]) + assert.Equal(t, "5", xMap["6"]) + + yMap := test.GetMapFromStructByTag("y", x) + assert.Len(t, yMap, 1) + assert.Equal(t, "string", yMap["2"]) +} diff --git a/internal/test/helper_request.go b/internal/test/helper_request.go index 5cff1a83..b1d2a189 100644 --- a/internal/test/helper_request.go +++ b/internal/test/helper_request.go @@ -16,6 +16,7 @@ import ( ) type GenericPayload map[string]interface{} +type GenericArrayPayload []interface{} func (g GenericPayload) Reader(t *testing.T) *bytes.Reader { t.Helper() @@ -28,6 +29,17 @@ func (g GenericPayload) Reader(t *testing.T) *bytes.Reader { return bytes.NewReader(b) } +func (g GenericArrayPayload) Reader(t *testing.T) *bytes.Reader { + t.Helper() + + b, err := json.Marshal(g) + if err != nil { + t.Fatalf("failed to serialize payload: %v", err) + } + + return bytes.NewReader(b) +} + func PerformRequestWithParams(t *testing.T, s *api.Server, method string, path string, body GenericPayload, headers http.Header, queryParams map[string]string) *httptest.ResponseRecorder { t.Helper() @@ -38,6 +50,16 @@ func PerformRequestWithParams(t *testing.T, s *api.Server, method string, path s return PerformRequestWithRawBody(t, s, method, path, body.Reader(t), headers, queryParams) } +func PerformRequestWithArrayAndParams(t *testing.T, s *api.Server, method string, path string, body GenericArrayPayload, headers http.Header, queryParams map[string]string) *httptest.ResponseRecorder { + t.Helper() + + if body == nil { + return PerformRequestWithRawBody(t, s, method, path, nil, headers, queryParams) + } + + return PerformRequestWithRawBody(t, s, method, path, body.Reader(t), headers, queryParams) +} + func PerformRequestWithRawBody(t *testing.T, s *api.Server, method string, path string, body io.Reader, headers http.Header, queryParams map[string]string) *httptest.ResponseRecorder { t.Helper() @@ -72,6 +94,12 @@ func PerformRequest(t *testing.T, s *api.Server, method string, path string, bod return PerformRequestWithParams(t, s, method, path, body, headers, nil) } +func PerformRequestWithArray(t *testing.T, s *api.Server, method string, path string, body GenericArrayPayload, headers http.Header) *httptest.ResponseRecorder { + t.Helper() + + return PerformRequestWithArrayAndParams(t, s, method, path, body, headers, nil) +} + func ParseResponseBody(t *testing.T, res *httptest.ResponseRecorder, v interface{}) { t.Helper() @@ -93,8 +121,14 @@ func ParseResponseAndValidate(t *testing.T, res *httptest.ResponseRecorder, v ru func HeadersWithAuth(t *testing.T, token string) http.Header { t.Helper() + return HeadersWithConfigurableAuth(t, "Bearer", token) +} + +func HeadersWithConfigurableAuth(t *testing.T, scheme string, token string) http.Header { + t.Helper() + headers := http.Header{} - headers.Set(echo.HeaderAuthorization, fmt.Sprintf("Bearer %s", token)) + headers.Set(echo.HeaderAuthorization, fmt.Sprintf("%s %s", scheme, token)) return headers } diff --git a/internal/test/helper_snapshot.go b/internal/test/helper_snapshot.go new file mode 100644 index 00000000..5eba3c3c --- /dev/null +++ b/internal/test/helper_snapshot.go @@ -0,0 +1,154 @@ +package test + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/davecgh/go-spew/spew" + "github.com/pmezard/go-difflib/difflib" +) + +var ( + DefaultSnapshotDirPathAbs = filepath.Join(util.GetProjectRootDir(), "/test/testdata/snapshots") + UpdateGoldenGlobal = util.GetEnvAsBool("TEST_UPDATE_GOLDEN", false) +) + +var defaultReplacer = func(s string) string { + return s +} + +var spewConfig = spew.ConfigState{ + Indent: " ", + SortKeys: true, // maps should be spewed in a deterministic order + DisablePointerAddresses: true, // don't spew the addresses of pointers + DisableCapacities: true, // don't spew capacities of collections + SpewKeys: true, // if unable to sort map keys then spew keys to strings and sort those +} + +type snapshoter struct { + update bool + label string + replacer func(s string) string + location string +} + +var Snapshoter = snapshoter{ + update: false, + label: "", + replacer: defaultReplacer, + location: DefaultSnapshotDirPathAbs, +} + +// Save creates a formatted dump of the given data. +// It will fail the test if the dump is different from the saved dump. +// It will also fail if it is the creation or an update of the snapshot. +// vastly inspired by https://github.com/bradleyjkemp/cupaloy +// main reason for self implementation is the replacer function and general flexibility +func (s snapshoter) Save(t TestingT, data ...interface{}) { + t.Helper() + err := os.MkdirAll(s.location, os.ModePerm) + if err != nil { + t.Fatal(err) + } + + dump := s.replacer(spewConfig.Sdump(data...)) + snapshotName := fmt.Sprintf("%s%s", strings.Replace(t.Name(), "/", "-", -1), s.label) + snapshotAbsPath := filepath.Join(s.location, fmt.Sprintf("%s.golden", snapshotName)) + + if s.update || UpdateGoldenGlobal { + err := writeSnapshot(snapshotAbsPath, dump) + if err != nil { + t.Fatal(err) + } + + t.Errorf("Updating snapshot: '%s'", snapshotName) + } + + prevSnapBytes, err := os.ReadFile(snapshotAbsPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + err = writeSnapshot(snapshotAbsPath, dump) + if err != nil { + t.Fatal(err) + } + + t.Fatalf("No snapshot exists for name: '%s'. Creating new snapshot", snapshotName) + } + + t.Fatal(err) + } + + prevSnap := string(prevSnapBytes) + if prevSnap != dump { + diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(prevSnap), + B: difflib.SplitLines(dump), + FromFile: "Previous", + ToFile: "Current", + Context: 1, + }) + if err != nil { + t.Fatal(err) + } + + t.Error(diff) + } +} + +// SaveU is a short version for .Update(true).Save(...) +func (s snapshoter) SaveU(t TestingT, data ...interface{}) { + s.Update(true).Save(t, data...) +} + +// Skip creates a custom replace function using a regex, this will replace any +// replacer function set in the Snapshoter. +// Each line of the formatted dump is matched against the property name defined in skip and +// the value will be replaced to deal with generated values that change each test. +func (s snapshoter) Skip(skip []string) snapshoter { + s.replacer = func(s string) string { + skipString := strings.Join(skip, "|") + re, err := regexp.Compile(fmt.Sprintf("(%s): .*", skipString)) + if err != nil { + panic(err) + } + + // replace lines with property name + + return re.ReplaceAllString(s, "$1: ,") + } + + return s +} + +// Upadte is used to force an update for the snapshot. Will fail the test. +func (s snapshoter) Update(update bool) snapshoter { + s.update = update + return s +} + +// Label is used to add a suffix to the snapshots golden file. +func (s snapshoter) Label(label string) snapshoter { + s.label = label + return s +} + +// Replacer is used to define a custom replace function in order to replace +// generated values (e.g. IDs). +func (s snapshoter) Replacer(replacer func(s string) string) snapshoter { + s.replacer = replacer + return s +} + +// Location is used to save the golden file to a different location. +func (s snapshoter) Location(location string) snapshoter { + s.location = location + return s +} + +func writeSnapshot(absPath string, dump string) error { + return os.WriteFile(absPath, []byte(dump), os.FileMode(0644)) +} diff --git a/internal/test/helper_snapshot_test.go b/internal/test/helper_snapshot_test.go new file mode 100644 index 00000000..ed56b9b0 --- /dev/null +++ b/internal/test/helper_snapshot_test.go @@ -0,0 +1,219 @@ +package test_test + +import ( + "os" + "path/filepath" + "regexp" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/test" + "allaboutapps.dev/aw/go-starter/internal/test/mocks" + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/go-openapi/swag" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestSnapshot(t *testing.T) { + if test.UpdateGoldenGlobal { + t.Skip() + } + a := struct { + A string + B int + C bool + D *string + }{ + A: "foo", + B: 1, + C: true, + D: swag.String("bar"), + } + + b := "Hello World!" + + test.Snapshoter.Save(t, a, b) +} + +func TestSnapshotWithReplacer(t *testing.T) { + if test.UpdateGoldenGlobal { + t.Skip() + } + randID, err := util.GenerateRandomBase64String(20) + require.NoError(t, err) + a := struct { + ID string + A string + B int + C bool + D *string + }{ + ID: randID, + A: "foo", + B: 1, + C: true, + D: swag.String("bar"), + } + + replacer := func(s string) string { + re, err := regexp.Compile(`ID:.*"(.*)",`) + require.NoError(t, err) + return re.ReplaceAllString(s, "ID: ,") + } + test.Snapshoter.Replacer(replacer).Save(t, a) +} + +func TestSnapshotShouldFail(t *testing.T) { + if test.UpdateGoldenGlobal { + t.Skip() + } + a := struct { + A string + B int + C bool + D *string + }{ + A: "fo", + B: 1, + C: true, + D: swag.String("bar"), + } + + b := "Hello World!" + + tMock := new(mocks.TestingT) + tMock.On("Helper").Return() + tMock.On("Name").Return("TestSnapshotShouldFail") + tMock.On("Error", mock.Anything).Return() + test.Snapshoter.Save(tMock, a, b) + tMock.AssertNotCalled(t, "Fatal") + tMock.AssertNotCalled(t, "Fatalf") + tMock.AssertCalled(t, "Error", mock.Anything) +} + +func TestSnapshotWithUpdate(t *testing.T) { + if test.UpdateGoldenGlobal { + t.Skip() + } + a := struct { + A string + B int + C bool + D *string + }{ + A: "fo", + B: 1, + C: true, + D: swag.String("bar"), + } + + b := "Hello World!" + + tMock := new(mocks.TestingT) + tMock.On("Helper").Return() + tMock.On("Name").Return("TestSnapshotWithUpdate") + tMock.On("Errorf", mock.Anything, mock.Anything).Return() + test.Snapshoter.Update(true).Save(tMock, a, b) + tMock.AssertNotCalled(t, "Error") + tMock.AssertNotCalled(t, "Fatal") + tMock.AssertCalled(t, "Errorf", mock.Anything, mock.Anything) +} + +func TestSnapshotNotExists(t *testing.T) { + if test.UpdateGoldenGlobal { + t.Skip() + } + a := struct { + A string + B int + C bool + D *string + }{ + A: "foo", + B: 1, + C: true, + D: swag.String("bar"), + } + + b := "Hello World!" + + defer func() { + os.Remove(filepath.Join(test.DefaultSnapshotDirPathAbs, "TestSnapshotNotExists.golden")) + }() + + tMock := new(mocks.TestingT) + tMock.On("Helper").Return() + tMock.On("Name").Return("TestSnapshotNotExists") + tMock.On("Fatalf", mock.Anything, mock.Anything).Return() + tMock.On("Fatal", mock.Anything).Return() + tMock.On("Error", mock.Anything).Return() + test.Snapshoter.Save(tMock, a, b) + tMock.AssertNotCalled(t, "Error") + tMock.AssertNotCalled(t, "Fatalf") + tMock.AssertCalled(t, "Fatalf", mock.Anything, mock.Anything) +} + +func TestSnapshotSkipFields(t *testing.T) { + if test.UpdateGoldenGlobal { + t.Skip() + } + randID, err := util.GenerateRandomBase64String(20) + require.NoError(t, err) + a := struct { + ID string + A string + B int + C bool + D *string + }{ + ID: randID, + A: "foo", + B: 1, + C: true, + D: swag.String("bar"), + } + + test.Snapshoter.Skip([]string{"ID"}).Save(t, a) +} + +func TestSnapshotWithLabel(t *testing.T) { + if test.UpdateGoldenGlobal { + t.Skip() + } + a := struct { + A string + B int + C bool + D *string + }{ + A: "foo", + B: 1, + C: true, + D: swag.String("bar"), + } + + b := "Hello World!" + + test.Snapshoter.Label("_A").Save(t, a) + test.Snapshoter.Label("_B").Save(t, b) +} + +func TestSnapshotWithLocation(t *testing.T) { + if test.UpdateGoldenGlobal { + t.Skip() + } + a := struct { + A string + B int + C bool + D *string + }{ + A: "foo", + B: 1, + C: true, + D: swag.String("bar"), + } + + location := filepath.Join(util.GetProjectRootDir(), "/internal/test/testdata") + test.Snapshoter.Location(location).Save(t, a) +} diff --git a/internal/test/mocks/TestingT.go b/internal/test/mocks/TestingT.go new file mode 100644 index 00000000..257d91f9 --- /dev/null +++ b/internal/test/mocks/TestingT.go @@ -0,0 +1,153 @@ +// Initial code generated by mockery using a local installation. +// The methods for the testing.T interface will not change that often so no +// automatic generation of the mock is required. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// TestingT is an autogenerated mock type for the TestingT type +type TestingT struct { + mock.Mock +} + +// Cleanup provides a mock function with given fields: _a0 +func (_m *TestingT) Cleanup(_a0 func()) { + _m.Called(_a0) +} + +// Error provides a mock function with given fields: args +func (_m *TestingT) Error(args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, args...) + _m.Called(_ca...) +} + +// Errorf provides a mock function with given fields: format, args +func (_m *TestingT) Errorf(format string, args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, format) + _ca = append(_ca, args...) + _m.Called(_ca...) +} + +// Fail provides a mock function with given fields: +func (_m *TestingT) Fail() { + _m.Called() +} + +// FailNow provides a mock function with given fields: +func (_m *TestingT) FailNow() { + _m.Called() +} + +// Failed provides a mock function with given fields: +func (_m *TestingT) Failed() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// Fatal provides a mock function with given fields: args +func (_m *TestingT) Fatal(args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, args...) + _m.Called(_ca...) +} + +// Fatalf provides a mock function with given fields: format, args +func (_m *TestingT) Fatalf(format string, args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, format) + _ca = append(_ca, args...) + _m.Called(_ca...) +} + +// Helper provides a mock function with given fields: +func (_m *TestingT) Helper() { + _m.Called() +} + +// Log provides a mock function with given fields: args +func (_m *TestingT) Log(args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, args...) + _m.Called(_ca...) +} + +// Logf provides a mock function with given fields: format, args +func (_m *TestingT) Logf(format string, args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, format) + _ca = append(_ca, args...) + _m.Called(_ca...) +} + +// Name provides a mock function with given fields: +func (_m *TestingT) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Skip provides a mock function with given fields: args +func (_m *TestingT) Skip(args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, args...) + _m.Called(_ca...) +} + +// SkipNow provides a mock function with given fields: +func (_m *TestingT) SkipNow() { + _m.Called() +} + +// Skipf provides a mock function with given fields: format, args +func (_m *TestingT) Skipf(format string, args ...interface{}) { + var _ca []interface{} + _ca = append(_ca, format) + _ca = append(_ca, args...) + _m.Called(_ca...) +} + +// Skipped provides a mock function with given fields: +func (_m *TestingT) Skipped() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// TempDir provides a mock function with given fields: +func (_m *TestingT) TempDir() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} diff --git a/internal/test/test_database.go b/internal/test/test_database.go index d62344e1..a038055f 100644 --- a/internal/test/test_database.go +++ b/internal/test/test_database.go @@ -2,179 +2,348 @@ package test import ( "context" + "crypto/md5" //nolint:gosec "database/sql" + "fmt" + "os" + "os/exec" "path/filepath" + "strings" "sync" "testing" pUtil "allaboutapps.dev/aw/go-starter/internal/util" + dbutil "allaboutapps.dev/aw/go-starter/internal/util/db" "github.com/allaboutapps/integresql-client-go" "github.com/allaboutapps/integresql-client-go/pkg/util" + "github.com/pkg/errors" migrate "github.com/rubenv/sql-migrate" "github.com/volatiletech/sqlboiler/v4/boil" ) var ( client *integresql.Client - hash string - // tracks template testDatabase initialization - doOnce sync.Once + // tracks IntegreSQL template initialization and hash relookup (to reidentify the pool from a precomputed poolID) + poolInitMap = &sync.Map{} // "poolID" -> *sync.Once + poolHashMap = &sync.Map{} // "poolID" -> "poolHash" - migDir = filepath.Join(pUtil.GetProjectRootDir(), "/migrations") - fixFile = filepath.Join(pUtil.GetProjectRootDir(), "/internal/test/fixtures.go") + // we will compute a db template hash over the following dirs/files + migDir = filepath.Join(pUtil.GetProjectRootDir(), "/migrations") + fixFile = filepath.Join(pUtil.GetProjectRootDir(), "/internal/test/fixtures.go") + selfFile = filepath.Join(pUtil.GetProjectRootDir(), "/internal/test/test_database.go") + defaultPoolPaths = []string{migDir, fixFile, selfFile} ) -// Use this utility func to test with an isolated test database -func WithTestDatabase(t *testing.T, closure func(db *sql.DB)) { +func init() { + // autoinitialize IntegreSQL client + c, err := integresql.DefaultClientFromEnv() + if err != nil { + panic(errors.Wrap(err, "Failed to create new integresql-client")) + } + client = c +} +// WithTestDatabase returns an isolated test database based on the current migrations and fixtures. +func WithTestDatabase(t *testing.T, closure func(db *sql.DB)) { t.Helper() - - // new context derived from background ctx := context.Background() + WithTestDatabaseContext(ctx, t, closure) +} + +// WithTestDatabaseContext returns an isolated test database based on the current migrations and fixtures. +// The provided context will be used during setup (instead of the default background context). +func WithTestDatabaseContext(ctx context.Context, t *testing.T, closure func(db *sql.DB)) { + t.Helper() - doOnce.Do(func() { + poolID := strings.Join(defaultPoolPaths[:], ",") + // Get a hold of the &sync.Once{} for this integresql pool in this pkg scope... + doOnce, _ := poolInitMap.LoadOrStore(poolID, &sync.Once{}) + doOnce.(*sync.Once).Do(func() { t.Helper() - initializeTestDatabaseTemplate(ctx, t) + + // compute and store poolID -> poolHash map (computes hash of all files/dirs specified) + poolHash := storePoolHash(t, poolID, defaultPoolPaths) + + // properly build up the template database once + execClosureNewIntegresTemplate(ctx, t, poolHash, func(db *sql.DB) error { + t.Helper() + + countMigrations, err := ApplyMigrations(t, db) + if err != nil { + t.Fatalf("Failed to apply migrations for %q: %v\n", poolHash, err) + return err + } + t.Logf("Applied %d migrations for hash %q", countMigrations, poolHash) + + countFixtures, err := ApplyTestFixtures(ctx, t, db) + if err != nil { + t.Fatalf("Failed to apply test fixtures for %q: %v\n", poolHash, err) + return err + } + t.Logf("Applied %d test fixtures for hash %q", countFixtures, poolHash) + + return nil + }) }) - testDatabase, err := client.GetTestDatabase(ctx, hash) + // execute closure in a new IntegreSQL database build from above template + execClosureNewIntegresDatabase(ctx, t, getPoolHash(t, poolID), "WithTestDatabase", closure) +} - if err != nil { - t.Fatalf("Failed to obtain test database: %v", err) - } +type DatabaseDumpConfig struct { + DumpFile string // required, absolute path to dump file + ApplyMigrations bool // optional, default false + ApplyTestFixtures bool // optional, default false +} - connectionString := testDatabase.Config.ConnectionString() +// WithTestDatabaseFromDump returns an isolated test database based on a dump file. +func WithTestDatabaseFromDump(t *testing.T, config DatabaseDumpConfig, closure func(db *sql.DB)) { + t.Helper() + ctx := context.Background() + WithTestDatabaseFromDumpContext(ctx, t, config, closure) +} - db, err := sql.Open("postgres", connectionString) +// WithTestDatabaseFromDumpContext returns an isolated test database based on a dump file. +// The provided context will be used during setup (instead of the default background context). +func WithTestDatabaseFromDumpContext(ctx context.Context, t *testing.T, config DatabaseDumpConfig, closure func(db *sql.DB)) { + t.Helper() - if err != nil { - t.Fatalf("Failed to setup test database for connectionString %q: %v", connectionString, err) + // DumpFile is mandadory. + if config.DumpFile == "" { + t.Fatal("DatabaseDumpConfig.DumpFile is mandadory and cannot be ''") } - if err := db.PingContext(ctx); err != nil { - t.Fatalf("Failed to ping test database for connectionString %q: %v", connectionString, err) - } + // poolID must incorporate additional config args in the final hash + fragments := fmt.Sprintf("?migrations=%v&fixtures=%v", config.ApplyMigrations, config.ApplyTestFixtures) + poolID := strings.Join([]string{config.DumpFile, selfFile}[:], ",") + fragments - t.Logf("WithTestDatabase: %q", testDatabase.Config.Database) + // Get a hold of the &sync.Once{} for this integresql pool in this pkg scope... + doOnce, _ := poolInitMap.LoadOrStore(poolID, &sync.Once{}) + doOnce.(*sync.Once).Do(func() { + t.Helper() - closure(db) + // compute and store poolID -> poolHash map (computes hash of all files/dirs specified) + poolHash := storePoolHash(t, poolID, []string{config.DumpFile, selfFile}, fragments) - // this database object is managed and should close automatically after running the test - if err := db.Close(); err != nil { - t.Fatalf("Failed to close db %q: %v", connectionString, err) - } + // properly build up the template database once + execClosureNewIntegresTemplate(ctx, t, poolHash, func(db *sql.DB) error { + t.Helper() - // disallow any further refs to managed object after running the test - db = nil + if err := ApplyDump(ctx, t, db, config.DumpFile); err != nil { + t.Fatalf("Failed to apply dumps for %q: %v\n", poolHash, err) + return err + } + t.Logf("Applied dump for hash %q", poolHash) + + if config.ApplyMigrations { + countMigrations, err := ApplyMigrations(t, db) + if err != nil { + t.Fatalf("Failed to apply migrations for %q: %v\n", poolHash, err) + return err + } + t.Logf("Applied %d migrations for hash %q", countMigrations, poolHash) + } + + if config.ApplyTestFixtures { + countFixtures, err := ApplyTestFixtures(ctx, t, db) + if err != nil { + t.Fatalf("Failed to apply test fixtures for %q: %v\n", poolHash, err) + return err + } + t.Logf("Applied %d test fixtures for hash %q", countFixtures, poolHash) + } + + return nil + }) + }) + + // execute closure in a new IntegreSQL database build from above template + execClosureNewIntegresDatabase(ctx, t, getPoolHash(t, poolID), "WithTestDatabaseFromDump", closure) } -// main private function to properly build up the template database -// ensure it is called once once per pkg scope. -func initializeTestDatabaseTemplate(ctx context.Context, t *testing.T) { +// WithTestDatabaseEmpty returns an isolated test database with no migrations applied or fixtures inserted. +func WithTestDatabaseEmpty(t *testing.T, closure func(db *sql.DB)) { + t.Helper() + ctx := context.Background() + WithTestDatabaseEmptyContext(ctx, t, closure) +} +// WithTestDatabaseEmptyContext returns an isolated test database with no migrations applied or fixtures inserted. +// The provided context will be used during setup (instead of the default background context). +func WithTestDatabaseEmptyContext(ctx context.Context, t *testing.T, closure func(db *sql.DB)) { t.Helper() - initTestDatabaseHash(t) + poolID := selfFile - initIntegresClient(t) + // Get a hold of the &sync.Once{} for this integresql pool in this pkg scope... + doOnce, _ := poolInitMap.LoadOrStore(poolID, &sync.Once{}) + doOnce.(*sync.Once).Do(func() { + t.Helper() - if err := client.SetupTemplateWithDBClient(ctx, hash, func(db *sql.DB) error { + // compute and store poolID -> poolHash map (computes hash of all files/dirs specified) + poolHash := storePoolHash(t, poolID, []string{selfFile}) - t.Helper() + // properly build up the template database once (noop) + execClosureNewIntegresTemplate(ctx, t, poolHash, func(db *sql.DB) error { + t.Helper() + return nil + }) + }) - err := applyMigrations(t, db) + // execute closure in a new IntegreSQL database build from above template + execClosureNewIntegresDatabase(ctx, t, getPoolHash(t, poolID), "WithTestDatabaseEmpty", closure) +} - if err != nil { - return err - } +// Adds poolID to poolHashMap pointing to the final integresql hash +// Expects hashPaths to be absolute paths to actual files or directories (its contents will be md5 hashed) +// Optional fragments can be used to further enhance the computed md5 +func storePoolHash(t *testing.T, poolID string, hashPaths []string, fragments ...string) string { + t.Helper() - err = insertFixtures(ctx, t, db) + // compute a new integreSQL pool hash + poolHash, err := util.GetTemplateHash(hashPaths...) + if err != nil { + t.Fatalf("Failed to create template hash for %v: %#v", poolID, err) + } - return err - }); err != nil { + // update the hash with optional provided fragments + if len(fragments) > 0 { + poolHash = fmt.Sprintf("%x", md5.Sum([]byte(poolHash+strings.Join(fragments, ",")))) //nolint:gosec + } + + // and point poolID to it (sideffect synchronized store!) + poolHashMap.Store(poolID, poolHash) // save it for all runners + + return poolHash +} + +// Gets precomputed integresql hash via poolID identifier from our synchronized map (see storePoolHash) +func getPoolHash(t *testing.T, poolID string) string { + poolHash, ok := poolHashMap.Load(poolID) + + if !ok { + t.Fatalf("Failed to get poolHash from poolID '%v'. Is poolHashMap initialized yet?", poolID) + return "" + } + + return poolHash.(string) +} + +// Executes closure on an integresql **template** database +func execClosureNewIntegresTemplate(ctx context.Context, t *testing.T, poolHash string, closure func(db *sql.DB) error) { + t.Helper() + + if err := client.SetupTemplateWithDBClient(ctx, poolHash, closure); err != nil { // This error is exceptionally fatal as it hinders ANY future other // test execution with this hash as the template was *never* properly // setuped successfully. All GetTestDatabase will wait unti timeout // unless we interrupt them by discarding the base template... - discardError := client.DiscardTemplate(ctx, hash) + discardError := client.DiscardTemplate(ctx, poolHash) if discardError != nil { - t.Fatalf("Failed to setup template database, also discarding failed for hash %q: %v, %v", hash, err, discardError) + t.Fatalf("Failed to setup template database, also discarding failed for poolHash %q: %v, %v", poolHash, err, discardError) } - t.Fatalf("Failed to setup template database (discarded) for hash %q: %v", hash, err) + t.Fatalf("Failed to setup template database (discarded) for poolHash %q: %v", poolHash, err) } } -func initIntegresClient(t *testing.T) { - +// Executes closure on an integresql **test** database (scaffolded from a template) +func execClosureNewIntegresDatabase(ctx context.Context, t *testing.T, poolHash string, callee string, closure func(db *sql.DB)) { t.Helper() - c, err := integresql.DefaultClientFromEnv() + testDatabase, err := client.GetTestDatabase(ctx, poolHash) + if err != nil { - t.Fatalf("Failed to create new integresql-client: %v", err) + t.Fatalf("Failed to obtain test database: %v", err) } - client = c -} - -func initTestDatabaseHash(t *testing.T) { + connectionString := testDatabase.Config.ConnectionString() + t.Logf("%v: %q", callee, testDatabase.Config.Database) - t.Helper() + db, err := sql.Open("postgres", connectionString) - h, err := util.GetTemplateHash(migDir, fixFile) if err != nil { - t.Fatalf("Failed to get template hash: %#v", err) + t.Fatalf("Failed to setup test database for connectionString %q: %v", connectionString, err) } - hash = h -} + if err := db.PingContext(ctx); err != nil { + t.Fatalf("Failed to ping test database for connectionString %q: %v", connectionString, err) + } -func applyMigrations(t *testing.T, db *sql.DB) error { + closure(db) + // this database object is managed and should close automatically after running the test + if err := db.Close(); err != nil { + t.Fatalf("Failed to close db %q: %v", connectionString, err) + } + + // disallow any further refs to managed object after running the test + db = nil +} + +// ApplyMigrations applies all current database migrations to db +func ApplyMigrations(t *testing.T, db *sql.DB) (countMigrations int, err error) { t.Helper() migrations := &migrate.FileMigrationSource{Dir: migDir} - n, err := migrate.Exec(db, "postgres", migrations, migrate.Up) + countMigrations, err = migrate.Exec(db, "postgres", migrations, migrate.Up) if err != nil { - return err + return 0, err } - t.Logf("Applied %d migrations for hash %q", n, hash) - - return nil + return countMigrations, err } -func insertFixtures(ctx context.Context, t *testing.T, db *sql.DB) error { - +// ApplyTestFixtures applies all current test fixtures (insert) to db +func ApplyTestFixtures(ctx context.Context, t *testing.T, db *sql.DB) (countFixtures int, err error) { t.Helper() - tx, err := db.BeginTx(ctx, nil) - if err != nil { - return err - } - inserts := Inserts() - for _, fixture := range inserts { - if err := fixture.Insert(ctx, db, boil.Infer()); err != nil { - if err := tx.Rollback(); err != nil { + // insert test fixtures in an auto-managed db transaction + err = dbutil.WithTransaction(ctx, db, func(tx boil.ContextExecutor) error { + t.Helper() + for _, fixture := range inserts { + if err := fixture.Insert(ctx, tx, boil.Infer()); err != nil { return err } - - return err } + return nil + }) + + if err != nil { + return 0, err } - if err := tx.Commit(); err != nil { + return len(inserts), nil +} + +// ApplyDump applies dumpFile (absolute path to .sql file) to db +func ApplyDump(ctx context.Context, t *testing.T, db *sql.DB, dumpFile string) error { + t.Helper() + + // ensure file exists + if _, err := os.Stat(dumpFile); err != nil { + return err + } + + // we need to get the db name before being able to do anything. + var targetDB string + if err := db.QueryRow("SELECT current_database();").Scan(&targetDB); err != nil { return err } - t.Logf("Inserted %d fixtures for hash %q", len(inserts), hash) + cmd := exec.Command("bash", "-c", fmt.Sprintf("cat %q | psql %q", dumpFile, targetDB)) //nolint:gosec + combinedOutput, err := cmd.CombinedOutput() + + if err != nil { + return errors.Wrap(err, string(combinedOutput)) + } - return nil + return err } diff --git a/internal/test/test_database_test.go b/internal/test/test_database_test.go index 57445f21..c308f046 100644 --- a/internal/test/test_database_test.go +++ b/internal/test/test_database_test.go @@ -1,18 +1,53 @@ -package test +package test_test import ( + "context" "database/sql" + "path/filepath" + "strings" + "sync" "testing" + "allaboutapps.dev/aw/go-starter/internal/test" + pUtil "allaboutapps.dev/aw/go-starter/internal/util" "github.com/stretchr/testify/require" ) -func TestWithTestDatabase(t *testing.T) { +func TestWithTestDatabaseConcurrentUsage(t *testing.T) { + wg := sync.WaitGroup{} + wg.Add(4) + + go func() { + test.WithTestDatabase(t, func(db1 *sql.DB) { + wg.Done() + }) + }() + + go func() { + test.WithTestDatabaseEmpty(t, func(db2 *sql.DB) { + wg.Done() + }) + }() + + go func() { + test.WithTestDatabaseFromDump(t, test.DatabaseDumpConfig{DumpFile: filepath.Join(pUtil.GetProjectRootDir(), "/test/testdata/plain.sql")}, func(db3 *sql.DB) { + wg.Done() + }) + }() + + go func() { + test.WithTestDatabaseFromDump(t, test.DatabaseDumpConfig{DumpFile: filepath.Join(pUtil.GetProjectRootDir(), "/test/testdata/users.sql")}, func(db4 *sql.DB) { + wg.Done() + }) + }() - t.Parallel() + // the above will concurrently write to the database pool maps, + wg.Wait() +} - WithTestDatabase(t, func(db1 *sql.DB) { - WithTestDatabase(t, func(db2 *sql.DB) { +func TestWithTestDatabase(t *testing.T) { + test.WithTestDatabase(t, func(db1 *sql.DB) { + test.WithTestDatabase(t, func(db2 *sql.DB) { var db1Name string err := db1.QueryRow("SELECT current_database();").Scan(&db1Name) @@ -30,3 +65,124 @@ func TestWithTestDatabase(t *testing.T) { }) }) } + +func TestWithTestDatabaseFromDump(t *testing.T) { + + dumpFile := filepath.Join(pUtil.GetProjectRootDir(), "/test/testdata/users.sql") + + test.WithTestDatabaseFromDump(t, test.DatabaseDumpConfig{DumpFile: dumpFile}, func(db1 *sql.DB) { + test.WithTestDatabaseFromDump(t, test.DatabaseDumpConfig{DumpFile: dumpFile}, func(db2 *sql.DB) { + + var db1Name string + if err := db1.QueryRow("SELECT current_database();").Scan(&db1Name); err != nil { + t.Fatal(err) + } + + var db2Name string + if err := db2.QueryRow("SELECT current_database();").Scan(&db2Name); err != nil { + t.Fatal(err) + } + + require.NotEqual(t, db1Name, db2Name) + + if _, err := db2.Exec("DELETE FROM users WHERE true;"); err != nil { + t.Fatal(err) + } + + var userCount1 int + if err := db1.QueryRow("SELECT count(id) FROM users;").Scan(&userCount1); err != nil { + t.Fatal(err) + } + require.Equal(t, 3, userCount1) + + var userCount2 int + if err := db2.QueryRow("SELECT count(id) FROM users;").Scan(&userCount2); err != nil { + t.Fatal(err) + } + require.Equal(t, 0, userCount2) + }) + }) +} + +func TestWithTestDatabaseFromDumpAutoMigrateAndTestFixtures(t *testing.T) { + dumpFile := filepath.Join(pUtil.GetProjectRootDir(), "/test/testdata/plain.sql") + + test.WithTestDatabaseFromDump(t, test.DatabaseDumpConfig{DumpFile: dumpFile}, func(db0 *sql.DB) { + test.WithTestDatabaseFromDump(t, test.DatabaseDumpConfig{DumpFile: dumpFile, ApplyMigrations: true}, func(db1 *sql.DB) { + test.WithTestDatabaseFromDump(t, test.DatabaseDumpConfig{DumpFile: dumpFile, ApplyMigrations: true, ApplyTestFixtures: true}, func(db2 *sql.DB) { + + // db0: has only a plain dump + // db1: has migrations + // db2: has migrations and testFixtures + + var db0Name string + if err := db0.QueryRow("SELECT current_database();").Scan(&db0Name); err != nil { + t.Fatal(err) + } + + var db1Name string + if err := db1.QueryRow("SELECT current_database();").Scan(&db1Name); err != nil { + t.Fatal(err) + } + + var db2Name string + if err := db2.QueryRow("SELECT current_database();").Scan(&db2Name); err != nil { + t.Fatal(err) + } + + require.NotEqual(t, db0Name, db1Name) + require.NotEqual(t, db1Name, db2Name) + require.NotEqual(t, db2Name, db0Name) + + // expect hash to be different for all 3 databases! + db0Hash := strings.Split(strings.Join(strings.Split(db0Name, "integresql_test_"), ""), "_")[0] + db1Hash := strings.Split(strings.Join(strings.Split(db1Name, "integresql_test_"), ""), "_")[0] + db2Hash := strings.Split(strings.Join(strings.Split(db2Name, "integresql_test_"), ""), "_")[0] + + require.NotEqual(t, db0Hash, db1Hash) + require.NotEqual(t, db1Hash, db2Hash) + require.NotEqual(t, db2Hash, db0Hash) + }) + }) + }) +} + +func TestWithTestDatabaseEmpty(t *testing.T) { + test.WithTestDatabaseEmpty(t, func(db1 *sql.DB) { + test.WithTestDatabaseEmpty(t, func(db2 *sql.DB) { + + var db1Name string + err := db1.QueryRow("SELECT current_database();").Scan(&db1Name) + if err != nil { + t.Fatal(err) + } + + var db2Name string + err = db2.QueryRow("SELECT current_database();").Scan(&db2Name) + if err != nil { + t.Fatal(err) + } + require.NotEqual(t, db1Name, db2Name) + + // test apply migrations + fixtures to a empty database 1 + _, err = test.ApplyMigrations(t, db1) + require.NoError(t, err) + + _, err = test.ApplyTestFixtures(context.Background(), t, db1) + require.NoError(t, err) + + // test apply dump to a empty database 2 + dumpFile := filepath.Join(pUtil.GetProjectRootDir(), "/test/testdata/users.sql") + err = test.ApplyDump(context.Background(), t, db2, dumpFile) + require.NoError(t, err) + + // check user count + var usrCount int + if err := db1.QueryRow("SELECT count(id) FROM users;").Scan(&usrCount); err != nil { + t.Fatal(err) + } + + require.Equal(t, 3, usrCount) + }) + }) +} diff --git a/internal/test/test_mailer.go b/internal/test/test_mailer.go index 36da8acf..accd7b98 100644 --- a/internal/test/test_mailer.go +++ b/internal/test/test_mailer.go @@ -15,16 +15,14 @@ const ( func NewTestMailer(t *testing.T) *mailer.Mailer { t.Helper() - config := config.DefaultServiceConfigFromEnv().Mailer - config.DefaultSender = TestMailerDefaultSender - - m := mailer.New(config, transport.NewMock()) + return newMailerWithTransporter(t, transport.NewMock()) +} - if err := m.ParseTemplates(); err != nil { - t.Fatal("Failed to parse mailer templates", err) - } +func NewSMTPMailerFromDefaultEnv(t *testing.T) *mailer.Mailer { + t.Helper() - return m + config := config.DefaultServiceConfigFromEnv().SMTP + return newMailerWithTransporter(t, transport.NewSMTP(config)) } func GetTestMailerMockTransport(t *testing.T, m *mailer.Mailer) *transport.MockMailTransport { @@ -36,3 +34,18 @@ func GetTestMailerMockTransport(t *testing.T, m *mailer.Mailer) *transport.MockM return mt } + +func newMailerWithTransporter(t *testing.T, transporter transport.MailTransporter) *mailer.Mailer { + t.Helper() + + config := config.DefaultServiceConfigFromEnv().Mailer + config.DefaultSender = TestMailerDefaultSender + + m := mailer.New(config, transporter) + + if err := m.ParseTemplates(); err != nil { + t.Fatal("Failed to parse mailer templates", err) + } + + return m +} diff --git a/internal/test/test_mailer_test.go b/internal/test/test_mailer_test.go new file mode 100644 index 00000000..e6b15e85 --- /dev/null +++ b/internal/test/test_mailer_test.go @@ -0,0 +1,59 @@ +package test_test + +import ( + "context" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/mailer/transport" + "allaboutapps.dev/aw/go-starter/internal/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWithTestMailer(t *testing.T) { + ctx := context.Background() + fixtures := test.Fixtures() + //nolint:gosec + passwordResetLink := "http://localhost/password/reset/12345" + + m1 := test.NewTestMailer(t) + m2 := test.NewTestMailer(t) + err := m1.SendPasswordReset(ctx, fixtures.User1.Username.String, passwordResetLink) + require.NoError(t, err) + + sender2 := "test2@example.com" + m2.Config.DefaultSender = sender2 + err = m2.SendPasswordReset(ctx, fixtures.User1.Username.String, passwordResetLink) + require.NoError(t, err) + + mt1 := test.GetTestMailerMockTransport(t, m1) + mail := mt1.GetLastSentMail() + mails := mt1.GetSentMails() + require.NotNil(t, mail) + require.Len(t, mails, 1) + assert.Equal(t, m1.Config.DefaultSender, mail.From) + assert.Len(t, mail.To, 1) + assert.Equal(t, fixtures.User1.Username.String, mail.To[0]) + assert.Equal(t, test.TestMailerDefaultSender, mail.From) + assert.Equal(t, "Password reset", mail.Subject) + assert.Contains(t, string(mail.HTML), passwordResetLink) + + mt2 := test.GetTestMailerMockTransport(t, m2) + mail = mt2.GetLastSentMail() + mails = mt2.GetSentMails() + require.NotNil(t, mail) + require.Len(t, mails, 1) + assert.Equal(t, m2.Config.DefaultSender, mail.From) + assert.Len(t, mail.To, 1) + assert.Equal(t, fixtures.User1.Username.String, mail.To[0]) + assert.Equal(t, sender2, mail.From) + assert.Equal(t, "Password reset", mail.Subject) + assert.Contains(t, string(mail.HTML), passwordResetLink) +} + +func TestWithSMTPMailerFromDefaultEnv(t *testing.T) { + m := test.NewSMTPMailerFromDefaultEnv(t) + require.NotNil(t, m) + require.NotEmpty(t, m.Transport) + assert.IsType(t, &transport.SMTPMailTransport{}, m.Transport) +} diff --git a/internal/test/test_server.go b/internal/test/test_server.go index 149249d2..0b31ffe8 100644 --- a/internal/test/test_server.go +++ b/internal/test/test_server.go @@ -10,47 +10,83 @@ import ( "allaboutapps.dev/aw/go-starter/internal/config" ) -// Use this utility func to test with an full blown server (default server config) +// WithTestServer returns a fully configured server (using the default server config). func WithTestServer(t *testing.T, closure func(s *api.Server)) { - t.Helper() - defaultConfig := config.DefaultServiceConfigFromEnv() - WithTestServerConfigurable(t, defaultConfig, closure) } -// Use this utility func to test with an full blown server (server env configurable) +// WithTestServerFromDump returns a fully configured server (using the default server config) and allows for a database dump to be injected. +func WithTestServerFromDump(t *testing.T, dumpConfig DatabaseDumpConfig, closure func(s *api.Server)) { + t.Helper() + defaultConfig := config.DefaultServiceConfigFromEnv() + WithTestServerConfigurableFromDump(t, defaultConfig, dumpConfig, closure) +} + +// WithTestServerConfigurable returns a fully configured server, allowing for configuration using the provided server config. func WithTestServerConfigurable(t *testing.T, config config.Server, closure func(s *api.Server)) { t.Helper() + ctx := context.Background() + WithTestServerConfigurableContext(ctx, t, config, closure) +} + +// WithTestServerConfigurableContext returns a fully configured server, allowing for configuration using the provided server config. +// The provided context will be used during setup (instead of the default background context). +func WithTestServerConfigurableContext(ctx context.Context, t *testing.T, config config.Server, closure func(s *api.Server)) { + t.Helper() + WithTestDatabaseContext(ctx, t, func(db *sql.DB) { + t.Helper() + execClosureNewTestServer(ctx, t, config, db, closure) + }) +} - WithTestDatabase(t, func(db *sql.DB) { +// WithTestServerConfigurableFromDump returns a fully configured server, allowing for configuration using the provided server config and a database dump to be injected. +func WithTestServerConfigurableFromDump(t *testing.T, config config.Server, dumpConfig DatabaseDumpConfig, closure func(s *api.Server)) { + t.Helper() + ctx := context.Background() + WithTestServerConfigurableFromDumpContext(ctx, t, config, dumpConfig, closure) +} +// WithTestServerConfigurableFromDumpContext returns a fully configured server, allowing for configuration using the provided server config and a database dump to be injected. +// The provided context will be used during setup (instead of the default background context). +func WithTestServerConfigurableFromDumpContext(ctx context.Context, t *testing.T, config config.Server, dumpConfig DatabaseDumpConfig, closure func(s *api.Server)) { + t.Helper() + WithTestDatabaseFromDump(t, dumpConfig, func(db *sql.DB) { t.Helper() + execClosureNewTestServer(ctx, t, config, db, closure) + }) +} - // https://stackoverflow.com/questions/43424787/how-to-use-next-available-port-in-http-listenandserve - // You may use port 0 to indicate you're not specifying an exact port but you want a free, available port selected by the system - config.Echo.ListenAddress = ":0" +// Executes closure on a new test server with a pre-provided database +func execClosureNewTestServer(ctx context.Context, t *testing.T, config config.Server, db *sql.DB, closure func(s *api.Server)) { + t.Helper() - s := api.NewServer(config) + // https://stackoverflow.com/questions/43424787/how-to-use-next-available-port-in-http-listenandserve + // You may use port 0 to indicate you're not specifying an exact port but you want a free, available port selected by the system + config.Echo.ListenAddress = ":0" - // attach the already initalized db - s.DB = db + s := api.NewServer(config) - // attach any other mocks - s.Mailer = NewTestMailer(t) - s.Push = NewTestPusher(t, db) + // attach the already initialized db + s.DB = db - router.Init(s) + if err := s.InitMailer(); err != nil { + t.Fatalf("Failed to init mailer: %v", err) + } - closure(s) + // attach any other mocks + s.Push = NewTestPusher(t, db) - // echo is managed and should close automatically after running the test - if err := s.Echo.Shutdown(context.Background()); err != nil { - t.Fatalf("failed to shutdown server: %v", err) - } + router.Init(s) - // disallow any further refs to managed object after running the test - s = nil - }) + closure(s) + + // echo is managed and should close automatically after running the test + if err := s.Echo.Shutdown(ctx); err != nil { + t.Fatalf("failed to shutdown server: %v", err) + } + + // disallow any further refs to managed object after running the test + s = nil } diff --git a/internal/test/test_server_test.go b/internal/test/test_server_test.go index 9a017922..8f12107b 100644 --- a/internal/test/test_server_test.go +++ b/internal/test/test_server_test.go @@ -1,15 +1,21 @@ -package test +package test_test import ( "net/http" + "path/filepath" + "strings" "testing" "allaboutapps.dev/aw/go-starter/internal/api" + "allaboutapps.dev/aw/go-starter/internal/config" + "allaboutapps.dev/aw/go-starter/internal/test" "allaboutapps.dev/aw/go-starter/internal/util" + pUtil "allaboutapps.dev/aw/go-starter/internal/util" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type TestRequestPayload struct { @@ -67,11 +73,8 @@ func (m *TestResponsePayload) UnmarshalBinary(b []byte) error { } func TestWithTestServer(t *testing.T) { - - t.Parallel() - - WithTestServer(t, func(s1 *api.Server) { - WithTestServer(t, func(s2 *api.Server) { + test.WithTestServer(t, func(s1 *api.Server) { + test.WithTestServer(t, func(s2 *api.Server) { path := "/testing-f679dbac-62bb-445d-b7e8-9f2c71ca382c" @@ -79,7 +82,7 @@ func TestWithTestServer(t *testing.T) { s1.Echo.POST(path, func(c echo.Context) error { var body TestRequestPayload - if err := util.BindAndValidate(c, &body); err != nil { + if err := util.BindAndValidateBody(c, &body); err != nil { t.Fatal(err) } @@ -90,22 +93,54 @@ func TestWithTestServer(t *testing.T) { return util.ValidateAndReturn(c, http.StatusOK, &response) }) - payload := GenericPayload{ + payload := test.GenericPayload{ "name": "Mario", } - res1 := PerformRequest(t, s1, "POST", path, payload, nil) + res1 := test.PerformRequest(t, s1, "POST", path, payload, nil) assert.Equal(t, http.StatusOK, res1.Result().StatusCode) var response1 TestResponsePayload - ParseResponseAndValidate(t, res1, &response1) + test.ParseResponseAndValidate(t, res1, &response1) assert.Equal(t, "Mario", response1.Hello) - res2 := PerformRequest(t, s2, "POST", path, payload, nil) + res2 := test.PerformRequest(t, s2, "POST", path, payload, nil) assert.Equal(t, http.StatusNotFound, res2.Result().StatusCode) }) }) } + +func TestWithTestServerFromDump(t *testing.T) { + dumpFile := filepath.Join(pUtil.GetProjectRootDir(), "/test/testdata/plain.sql") + + serverConfig := config.DefaultServiceConfigFromEnv() + dumpConfig := test.DatabaseDumpConfig{DumpFile: dumpFile, ApplyMigrations: true, ApplyTestFixtures: true} + + test.WithTestServerFromDump(t, dumpConfig, func(s1 *api.Server) { + test.WithTestServerConfigurableFromDump(t, serverConfig, dumpConfig, func(s2 *api.Server) { + + var db1Name string + if err := s1.DB.QueryRow("SELECT current_database();").Scan(&db1Name); err != nil { + t.Fatal(err) + } + + var db2Name string + if err := s2.DB.QueryRow("SELECT current_database();").Scan(&db2Name); err != nil { + t.Fatal(err) + } + + require.NotEqual(t, db1Name, db2Name) + + // same dumpConfig settings - must be same base template hash. + db1Hash := strings.Split(strings.Join(strings.Split(db1Name, "integresql_test_"), ""), "_")[0] + db2Hash := strings.Split(strings.Join(strings.Split(db2Name, "integresql_test_"), ""), "_")[0] + + require.Equal(t, db1Hash, db2Hash) + + }) + }) + +} diff --git a/internal/test/testdata/TestSnapshotWithLocation.golden b/internal/test/testdata/TestSnapshotWithLocation.golden new file mode 100644 index 00000000..66fe1a0d --- /dev/null +++ b/internal/test/testdata/TestSnapshotWithLocation.golden @@ -0,0 +1,6 @@ +(struct { A string; B int; C bool; D *string }) { + A: (string) (len=3) "foo", + B: (int) 1, + C: (bool) true, + D: (*string)((len=3) "bar") +} diff --git a/internal/test/testing_mock.go b/internal/test/testing_mock.go new file mode 100644 index 00000000..abd4fb48 --- /dev/null +++ b/internal/test/testing_mock.go @@ -0,0 +1,29 @@ +package test + +import "testing" + +// TestingT is used to generate a mock of testing.T to enable testing +// of helper methods which are using assert/require +// Inspired by: https://github.com/uber-go/zap/blob/master/zaptest/testingt_test.go, commit 5b0fd114dcc089875ee61dfad3617c3a43c2e93e +type TestingT interface { + Cleanup(func()) + Error(args ...interface{}) + Errorf(format string, args ...interface{}) + Fail() + FailNow() + Failed() bool + Fatal(args ...interface{}) + Fatalf(format string, args ...interface{}) + Helper() + Log(args ...interface{}) + Logf(format string, args ...interface{}) + Name() string + Skip(args ...interface{}) + SkipNow() + Skipf(format string, args ...interface{}) + Skipped() bool + TempDir() string +} + +// used to ensure compatibility between this interface and testing.TB +var _ TestingT = (testing.TB)(nil) diff --git a/internal/types/common/get_version_route_parameters.go b/internal/types/common/get_version_route_parameters.go new file mode 100644 index 00000000..ddf101a0 --- /dev/null +++ b/internal/types/common/get_version_route_parameters.go @@ -0,0 +1,55 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package common + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/strfmt" +) + +// NewGetVersionRouteParams creates a new GetVersionRouteParams object +// no default values defined in spec. +func NewGetVersionRouteParams() GetVersionRouteParams { + + return GetVersionRouteParams{} +} + +// GetVersionRouteParams contains all the bound params for the get version route operation +// typically these are obtained from a http.Request +// +// swagger:parameters GetVersionRoute +type GetVersionRouteParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewGetVersionRouteParams() beforehand. +func (o *GetVersionRouteParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (o *GetVersionRouteParams) Validate(formats strfmt.Registry) error { + var res []error + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/internal/types/get_user_info_response.go b/internal/types/get_user_info_response.go index 06aedf12..9c4c4e84 100644 --- a/internal/types/get_user_info_response.go +++ b/internal/types/get_user_info_response.go @@ -6,6 +6,7 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" "encoding/json" "strconv" @@ -21,18 +22,22 @@ import ( type GetUserInfoResponse struct { // Email address of user, if available + // Example: user@example.com // Max Length: 255 // Format: email Email strfmt.Email `json:"email,omitempty"` // Auth-Scopes of the user, if available + // Example: ["app"] Scopes []string `json:"scopes"` // ID of user + // Example: 82ebdfad-c586-4407-a873-4cc1c33d56fc // Required: true Sub *string `json:"sub"` // Unix timestamp the user's info was last updated at + // Example: 1591960808 // Required: true UpdatedAt *int64 `json:"updated_at"` } @@ -64,12 +69,11 @@ func (m *GetUserInfoResponse) Validate(formats strfmt.Registry) error { } func (m *GetUserInfoResponse) validateEmail(formats strfmt.Registry) error { - if swag.IsZero(m.Email) { // not required return nil } - if err := validate.MaxLength("email", "body", string(m.Email), 255); err != nil { + if err := validate.MaxLength("email", "body", m.Email.String(), 255); err != nil { return err } @@ -100,7 +104,6 @@ func (m *GetUserInfoResponse) validateScopesItemsEnum(path, location string, val } func (m *GetUserInfoResponse) validateScopes(formats strfmt.Registry) error { - if swag.IsZero(m.Scopes) { // not required return nil } @@ -135,6 +138,11 @@ func (m *GetUserInfoResponse) validateUpdatedAt(formats strfmt.Registry) error { return nil } +// ContextValidate validates this get user info response based on context it is used +func (m *GetUserInfoResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *GetUserInfoResponse) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/http_validation_error_detail.go b/internal/types/http_validation_error_detail.go index ddba8a99..0afbbbd9 100644 --- a/internal/types/http_validation_error_detail.go +++ b/internal/types/http_validation_error_detail.go @@ -6,6 +6,8 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -79,6 +81,11 @@ func (m *HTTPValidationErrorDetail) validateKey(formats strfmt.Registry) error { return nil } +// ContextValidate validates this http validation error detail based on context it is used +func (m *HTTPValidationErrorDetail) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *HTTPValidationErrorDetail) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/nullables.go b/internal/types/nullables.go new file mode 100644 index 00000000..9e71c047 --- /dev/null +++ b/internal/types/nullables.go @@ -0,0 +1,743 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package types + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + + "github.com/allaboutapps/nullable" + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" +) + +// Nullables nullables +// +// swagger:model nullables +type Nullables struct { + + // nullable bool + NullableBool nullable.Bool `json:"nullableBool,omitempty"` + + // nullable bool slice + NullableBoolSlice nullable.BoolSlice `json:"nullableBoolSlice,omitempty"` + + // nullable float + NullableFloat nullable.Float32 `json:"nullableFloat,omitempty"` + + // nullable float32 + NullableFloat32 nullable.Float32 `json:"nullableFloat32,omitempty"` + + // nullable float32 slice + NullableFloat32Slice nullable.Float32Slice `json:"nullableFloat32Slice,omitempty"` + + // nullable float64 + NullableFloat64 nullable.Float64 `json:"nullableFloat64,omitempty"` + + // nullable float64 slice + NullableFloat64Slice nullable.Float64Slice `json:"nullableFloat64Slice,omitempty"` + + // nullable float slice + NullableFloatSlice nullable.Float32Slice `json:"nullableFloatSlice,omitempty"` + + // nullable int + NullableInt nullable.Int `json:"nullableInt,omitempty"` + + // nullable int16 + NullableInt16 nullable.Int16 `json:"nullableInt16,omitempty"` + + // nullable int16 slice + NullableInt16Slice nullable.Int16Slice `json:"nullableInt16Slice,omitempty"` + + // nullable int32 + NullableInt32 nullable.Int32 `json:"nullableInt32,omitempty"` + + // nullable int32 slice + NullableInt32Slice nullable.Int32Slice `json:"nullableInt32Slice,omitempty"` + + // nullable int64 + NullableInt64 nullable.Int64 `json:"nullableInt64,omitempty"` + + // nullable int64 slice + NullableInt64Slice nullable.Int64Slice `json:"nullableInt64Slice,omitempty"` + + // nullable int slice + NullableIntSlice nullable.IntSlice `json:"nullableIntSlice,omitempty"` + + // nullable string + NullableString nullable.String `json:"nullableString,omitempty"` + + // nullable string slice + NullableStringSlice nullable.StringSlice `json:"nullableStringSlice,omitempty"` +} + +// Validate validates this nullables +func (m *Nullables) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateNullableBool(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableBoolSlice(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableFloat(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableFloat32(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableFloat32Slice(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableFloat64(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableFloat64Slice(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableFloatSlice(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableInt(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableInt16(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableInt16Slice(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableInt32(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableInt32Slice(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableInt64(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableInt64Slice(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableIntSlice(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableString(formats); err != nil { + res = append(res, err) + } + + if err := m.validateNullableStringSlice(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *Nullables) validateNullableBool(formats strfmt.Registry) error { + if swag.IsZero(m.NullableBool) { // not required + return nil + } + + if err := m.NullableBool.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableBool") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableBoolSlice(formats strfmt.Registry) error { + if swag.IsZero(m.NullableBoolSlice) { // not required + return nil + } + + if err := m.NullableBoolSlice.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableBoolSlice") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableFloat(formats strfmt.Registry) error { + if swag.IsZero(m.NullableFloat) { // not required + return nil + } + + if err := m.NullableFloat.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloat") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableFloat32(formats strfmt.Registry) error { + if swag.IsZero(m.NullableFloat32) { // not required + return nil + } + + if err := m.NullableFloat32.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloat32") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableFloat32Slice(formats strfmt.Registry) error { + if swag.IsZero(m.NullableFloat32Slice) { // not required + return nil + } + + if err := m.NullableFloat32Slice.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloat32Slice") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableFloat64(formats strfmt.Registry) error { + if swag.IsZero(m.NullableFloat64) { // not required + return nil + } + + if err := m.NullableFloat64.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloat64") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableFloat64Slice(formats strfmt.Registry) error { + if swag.IsZero(m.NullableFloat64Slice) { // not required + return nil + } + + if err := m.NullableFloat64Slice.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloat64Slice") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableFloatSlice(formats strfmt.Registry) error { + if swag.IsZero(m.NullableFloatSlice) { // not required + return nil + } + + if err := m.NullableFloatSlice.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloatSlice") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableInt(formats strfmt.Registry) error { + if swag.IsZero(m.NullableInt) { // not required + return nil + } + + if err := m.NullableInt.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableInt16(formats strfmt.Registry) error { + if swag.IsZero(m.NullableInt16) { // not required + return nil + } + + if err := m.NullableInt16.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt16") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableInt16Slice(formats strfmt.Registry) error { + if swag.IsZero(m.NullableInt16Slice) { // not required + return nil + } + + if err := m.NullableInt16Slice.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt16Slice") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableInt32(formats strfmt.Registry) error { + if swag.IsZero(m.NullableInt32) { // not required + return nil + } + + if err := m.NullableInt32.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt32") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableInt32Slice(formats strfmt.Registry) error { + if swag.IsZero(m.NullableInt32Slice) { // not required + return nil + } + + if err := m.NullableInt32Slice.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt32Slice") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableInt64(formats strfmt.Registry) error { + if swag.IsZero(m.NullableInt64) { // not required + return nil + } + + if err := m.NullableInt64.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt64") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableInt64Slice(formats strfmt.Registry) error { + if swag.IsZero(m.NullableInt64Slice) { // not required + return nil + } + + if err := m.NullableInt64Slice.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt64Slice") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableIntSlice(formats strfmt.Registry) error { + if swag.IsZero(m.NullableIntSlice) { // not required + return nil + } + + if err := m.NullableIntSlice.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableIntSlice") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableString(formats strfmt.Registry) error { + if swag.IsZero(m.NullableString) { // not required + return nil + } + + if err := m.NullableString.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableString") + } + return err + } + + return nil +} + +func (m *Nullables) validateNullableStringSlice(formats strfmt.Registry) error { + if swag.IsZero(m.NullableStringSlice) { // not required + return nil + } + + if err := m.NullableStringSlice.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableStringSlice") + } + return err + } + + return nil +} + +// ContextValidate validate this nullables based on the context it is used +func (m *Nullables) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateNullableBool(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableBoolSlice(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableFloat(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableFloat32(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableFloat32Slice(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableFloat64(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableFloat64Slice(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableFloatSlice(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableInt(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableInt16(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableInt16Slice(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableInt32(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableInt32Slice(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableInt64(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableInt64Slice(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableIntSlice(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableString(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateNullableStringSlice(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *Nullables) contextValidateNullableBool(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableBool.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableBool") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableBoolSlice(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableBoolSlice.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableBoolSlice") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableFloat(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableFloat.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloat") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableFloat32(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableFloat32.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloat32") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableFloat32Slice(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableFloat32Slice.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloat32Slice") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableFloat64(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableFloat64.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloat64") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableFloat64Slice(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableFloat64Slice.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloat64Slice") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableFloatSlice(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableFloatSlice.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableFloatSlice") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableInt(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableInt.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableInt16(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableInt16.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt16") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableInt16Slice(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableInt16Slice.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt16Slice") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableInt32(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableInt32.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt32") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableInt32Slice(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableInt32Slice.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt32Slice") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableInt64(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableInt64.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt64") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableInt64Slice(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableInt64Slice.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableInt64Slice") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableIntSlice(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableIntSlice.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableIntSlice") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableString(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableString.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableString") + } + return err + } + + return nil +} + +func (m *Nullables) contextValidateNullableStringSlice(ctx context.Context, formats strfmt.Registry) error { + + if err := m.NullableStringSlice.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("nullableStringSlice") + } + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *Nullables) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *Nullables) UnmarshalBinary(b []byte) error { + var res Nullables + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/internal/types/order_dir.go b/internal/types/order_dir.go index 1e681d87..013c9005 100644 --- a/internal/types/order_dir.go +++ b/internal/types/order_dir.go @@ -6,6 +6,7 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" "encoding/json" "github.com/go-openapi/errors" @@ -61,3 +62,8 @@ func (m OrderDir) Validate(formats strfmt.Registry) error { } return nil } + +// ContextValidate validates this order dir based on context it is used +func (m OrderDir) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} diff --git a/internal/types/post_change_password_payload.go b/internal/types/post_change_password_payload.go index 9d782dde..da407ba6 100644 --- a/internal/types/post_change_password_payload.go +++ b/internal/types/post_change_password_payload.go @@ -6,6 +6,8 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -18,12 +20,14 @@ import ( type PostChangePasswordPayload struct { // Current password of user + // Example: correct horse battery staple // Required: true // Max Length: 500 // Min Length: 1 CurrentPassword *string `json:"currentPassword"` // New password to set for user + // Example: correct horse battery staple // Required: true // Max Length: 500 // Min Length: 1 @@ -54,11 +58,11 @@ func (m *PostChangePasswordPayload) validateCurrentPassword(formats strfmt.Regis return err } - if err := validate.MinLength("currentPassword", "body", string(*m.CurrentPassword), 1); err != nil { + if err := validate.MinLength("currentPassword", "body", *m.CurrentPassword, 1); err != nil { return err } - if err := validate.MaxLength("currentPassword", "body", string(*m.CurrentPassword), 500); err != nil { + if err := validate.MaxLength("currentPassword", "body", *m.CurrentPassword, 500); err != nil { return err } @@ -71,17 +75,22 @@ func (m *PostChangePasswordPayload) validateNewPassword(formats strfmt.Registry) return err } - if err := validate.MinLength("newPassword", "body", string(*m.NewPassword), 1); err != nil { + if err := validate.MinLength("newPassword", "body", *m.NewPassword, 1); err != nil { return err } - if err := validate.MaxLength("newPassword", "body", string(*m.NewPassword), 500); err != nil { + if err := validate.MaxLength("newPassword", "body", *m.NewPassword, 500); err != nil { return err } return nil } +// ContextValidate validates this post change password payload based on context it is used +func (m *PostChangePasswordPayload) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *PostChangePasswordPayload) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/post_forgot_password_complete_payload.go b/internal/types/post_forgot_password_complete_payload.go index bf69cfe8..4ff2085f 100644 --- a/internal/types/post_forgot_password_complete_payload.go +++ b/internal/types/post_forgot_password_complete_payload.go @@ -6,6 +6,8 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -18,12 +20,14 @@ import ( type PostForgotPasswordCompletePayload struct { // New password to set for user + // Example: correct horse battery staple // Required: true // Max Length: 500 // Min Length: 1 Password *string `json:"password"` // Password reset token sent via email + // Example: ec16f032-3c44-4148-bbcc-45557466fa74 // Required: true // Format: uuid4 Token *strfmt.UUID4 `json:"token"` @@ -53,11 +57,11 @@ func (m *PostForgotPasswordCompletePayload) validatePassword(formats strfmt.Regi return err } - if err := validate.MinLength("password", "body", string(*m.Password), 1); err != nil { + if err := validate.MinLength("password", "body", *m.Password, 1); err != nil { return err } - if err := validate.MaxLength("password", "body", string(*m.Password), 500); err != nil { + if err := validate.MaxLength("password", "body", *m.Password, 500); err != nil { return err } @@ -77,6 +81,11 @@ func (m *PostForgotPasswordCompletePayload) validateToken(formats strfmt.Registr return nil } +// ContextValidate validates this post forgot password complete payload based on context it is used +func (m *PostForgotPasswordCompletePayload) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *PostForgotPasswordCompletePayload) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/post_forgot_password_payload.go b/internal/types/post_forgot_password_payload.go index d6c26189..6c04d437 100644 --- a/internal/types/post_forgot_password_payload.go +++ b/internal/types/post_forgot_password_payload.go @@ -6,6 +6,8 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -18,6 +20,7 @@ import ( type PostForgotPasswordPayload struct { // Username to initiate password reset for + // Example: user@example.com // Required: true // Max Length: 255 // Min Length: 1 @@ -45,11 +48,11 @@ func (m *PostForgotPasswordPayload) validateUsername(formats strfmt.Registry) er return err } - if err := validate.MinLength("username", "body", string(*m.Username), 1); err != nil { + if err := validate.MinLength("username", "body", m.Username.String(), 1); err != nil { return err } - if err := validate.MaxLength("username", "body", string(*m.Username), 255); err != nil { + if err := validate.MaxLength("username", "body", m.Username.String(), 255); err != nil { return err } @@ -60,6 +63,11 @@ func (m *PostForgotPasswordPayload) validateUsername(formats strfmt.Registry) er return nil } +// ContextValidate validates this post forgot password payload based on context it is used +func (m *PostForgotPasswordPayload) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *PostForgotPasswordPayload) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/post_login_payload.go b/internal/types/post_login_payload.go index 86bcc022..1be5c6c4 100644 --- a/internal/types/post_login_payload.go +++ b/internal/types/post_login_payload.go @@ -6,6 +6,8 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -18,12 +20,14 @@ import ( type PostLoginPayload struct { // Password of user to authenticate as + // Example: correct horse battery staple // Required: true // Max Length: 500 // Min Length: 1 Password *string `json:"password"` // Username of user to authenticate as + // Example: user@example.com // Required: true // Max Length: 255 // Min Length: 1 @@ -55,11 +59,11 @@ func (m *PostLoginPayload) validatePassword(formats strfmt.Registry) error { return err } - if err := validate.MinLength("password", "body", string(*m.Password), 1); err != nil { + if err := validate.MinLength("password", "body", *m.Password, 1); err != nil { return err } - if err := validate.MaxLength("password", "body", string(*m.Password), 500); err != nil { + if err := validate.MaxLength("password", "body", *m.Password, 500); err != nil { return err } @@ -72,11 +76,11 @@ func (m *PostLoginPayload) validateUsername(formats strfmt.Registry) error { return err } - if err := validate.MinLength("username", "body", string(*m.Username), 1); err != nil { + if err := validate.MinLength("username", "body", m.Username.String(), 1); err != nil { return err } - if err := validate.MaxLength("username", "body", string(*m.Username), 255); err != nil { + if err := validate.MaxLength("username", "body", m.Username.String(), 255); err != nil { return err } @@ -87,6 +91,11 @@ func (m *PostLoginPayload) validateUsername(formats strfmt.Registry) error { return nil } +// ContextValidate validates this post login payload based on context it is used +func (m *PostLoginPayload) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *PostLoginPayload) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/post_login_response.go b/internal/types/post_login_response.go index 3a72c163..6e1dfe5c 100644 --- a/internal/types/post_login_response.go +++ b/internal/types/post_login_response.go @@ -6,6 +6,8 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -18,20 +20,24 @@ import ( type PostLoginResponse struct { // Access token required for accessing protected API endpoints + // Example: c1247d8d-0d65-41c4-bc86-ec041d2ac437 // Required: true // Format: uuid4 AccessToken *strfmt.UUID4 `json:"access_token"` // Access token expiry in seconds + // Example: 86400 // Required: true ExpiresIn *int64 `json:"expires_in"` // Refresh token for refreshing the access token once it expires + // Example: 1dadb3bd-50d8-485d-83a3-6111392568f0 // Required: true // Format: uuid4 RefreshToken *strfmt.UUID4 `json:"refresh_token"` // Type of access token, will always be `bearer` + // Example: bearer // Required: true TokenType *string `json:"token_type"` } @@ -106,6 +112,11 @@ func (m *PostLoginResponse) validateTokenType(formats strfmt.Registry) error { return nil } +// ContextValidate validates this post login response based on context it is used +func (m *PostLoginResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *PostLoginResponse) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/post_logout_payload.go b/internal/types/post_logout_payload.go index b88a60b0..4b66f804 100644 --- a/internal/types/post_logout_payload.go +++ b/internal/types/post_logout_payload.go @@ -6,6 +6,8 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -18,6 +20,7 @@ import ( type PostLogoutPayload struct { // Optional refresh token to delete while logging out + // Example: 700ebed3-40f7-4211-bc83-a89b22b9875e // Format: uuid4 RefreshToken strfmt.UUID4 `json:"refresh_token,omitempty"` } @@ -37,7 +40,6 @@ func (m *PostLogoutPayload) Validate(formats strfmt.Registry) error { } func (m *PostLogoutPayload) validateRefreshToken(formats strfmt.Registry) error { - if swag.IsZero(m.RefreshToken) { // not required return nil } @@ -49,6 +51,11 @@ func (m *PostLogoutPayload) validateRefreshToken(formats strfmt.Registry) error return nil } +// ContextValidate validates this post logout payload based on context it is used +func (m *PostLogoutPayload) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *PostLogoutPayload) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/post_refresh_payload.go b/internal/types/post_refresh_payload.go index 4aec328b..39335420 100644 --- a/internal/types/post_refresh_payload.go +++ b/internal/types/post_refresh_payload.go @@ -6,6 +6,8 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -18,6 +20,7 @@ import ( type PostRefreshPayload struct { // Refresh token to use for retrieving new token set + // Example: 7503cd8a-c921-4368-a32d-6c1d01d86da9 // Required: true // Format: uuid4 RefreshToken *strfmt.UUID4 `json:"refresh_token"` @@ -50,6 +53,11 @@ func (m *PostRefreshPayload) validateRefreshToken(formats strfmt.Registry) error return nil } +// ContextValidate validates this post refresh payload based on context it is used +func (m *PostRefreshPayload) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *PostRefreshPayload) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/post_register_payload.go b/internal/types/post_register_payload.go index de40fb6e..75f47b45 100644 --- a/internal/types/post_register_payload.go +++ b/internal/types/post_register_payload.go @@ -6,6 +6,8 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -18,12 +20,14 @@ import ( type PostRegisterPayload struct { // Password to register with + // Example: correct horse battery staple // Required: true // Max Length: 500 // Min Length: 1 Password *string `json:"password"` // Username to register with + // Example: user@example.com // Required: true // Max Length: 255 // Min Length: 1 @@ -55,11 +59,11 @@ func (m *PostRegisterPayload) validatePassword(formats strfmt.Registry) error { return err } - if err := validate.MinLength("password", "body", string(*m.Password), 1); err != nil { + if err := validate.MinLength("password", "body", *m.Password, 1); err != nil { return err } - if err := validate.MaxLength("password", "body", string(*m.Password), 500); err != nil { + if err := validate.MaxLength("password", "body", *m.Password, 500); err != nil { return err } @@ -72,11 +76,11 @@ func (m *PostRegisterPayload) validateUsername(formats strfmt.Registry) error { return err } - if err := validate.MinLength("username", "body", string(*m.Username), 1); err != nil { + if err := validate.MinLength("username", "body", m.Username.String(), 1); err != nil { return err } - if err := validate.MaxLength("username", "body", string(*m.Username), 255); err != nil { + if err := validate.MaxLength("username", "body", m.Username.String(), 255); err != nil { return err } @@ -87,6 +91,11 @@ func (m *PostRegisterPayload) validateUsername(formats strfmt.Registry) error { return nil } +// ContextValidate validates this post register payload based on context it is used +func (m *PostRegisterPayload) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *PostRegisterPayload) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/post_update_push_token_payload.go b/internal/types/post_update_push_token_payload.go index 94b8247e..9576103c 100644 --- a/internal/types/post_update_push_token_payload.go +++ b/internal/types/post_update_push_token_payload.go @@ -6,6 +6,8 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -18,15 +20,18 @@ import ( type PostUpdatePushTokenPayload struct { // New push token for given provider. + // Example: 1c91e550-8167-439c-8021-dee7de2f7e96 // Required: true // Max Length: 500 NewToken *string `json:"newToken"` // Old token that can be deleted if present. + // Example: 495179de-b771-48f0-aab2-8d23701b0f02 // Max Length: 500 OldToken *string `json:"oldToken,omitempty"` // Identifier of the provider the token is for (eg. "fcm", "apn"). Currently only "fcm" is supported. + // Example: fcm // Required: true // Max Length: 500 Provider *string `json:"provider"` @@ -60,7 +65,7 @@ func (m *PostUpdatePushTokenPayload) validateNewToken(formats strfmt.Registry) e return err } - if err := validate.MaxLength("newToken", "body", string(*m.NewToken), 500); err != nil { + if err := validate.MaxLength("newToken", "body", *m.NewToken, 500); err != nil { return err } @@ -68,12 +73,11 @@ func (m *PostUpdatePushTokenPayload) validateNewToken(formats strfmt.Registry) e } func (m *PostUpdatePushTokenPayload) validateOldToken(formats strfmt.Registry) error { - if swag.IsZero(m.OldToken) { // not required return nil } - if err := validate.MaxLength("oldToken", "body", string(*m.OldToken), 500); err != nil { + if err := validate.MaxLength("oldToken", "body", *m.OldToken, 500); err != nil { return err } @@ -86,13 +90,18 @@ func (m *PostUpdatePushTokenPayload) validateProvider(formats strfmt.Registry) e return err } - if err := validate.MaxLength("provider", "body", string(*m.Provider), 500); err != nil { + if err := validate.MaxLength("provider", "body", *m.Provider, 500); err != nil { return err } return nil } +// ContextValidate validates this post update push token payload based on context it is used +func (m *PostUpdatePushTokenPayload) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *PostUpdatePushTokenPayload) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/public_http_error.go b/internal/types/public_http_error.go index f06204ec..fb5784ef 100644 --- a/internal/types/public_http_error.go +++ b/internal/types/public_http_error.go @@ -6,6 +6,8 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" + "github.com/go-openapi/errors" "github.com/go-openapi/strfmt" "github.com/go-openapi/swag" @@ -17,20 +19,24 @@ import ( // swagger:model publicHttpError type PublicHTTPError struct { - // More detailed, human-readable, optional explanation of the error - Detail string `json:"detail,omitempty"` - // HTTP status code returned for the error + // Example: 403 // Required: true // Maximum: 599 // Minimum: 100 Code *int64 `json:"status"` + // More detailed, human-readable, optional explanation of the error + // Example: User is lacking permission to access this resource + Detail string `json:"detail,omitempty"` + // Short, human-readable description of the error + // Example: Forbidden // Required: true Title *string `json:"title"` // Type of error returned, should be used for client-side error handling + // Example: generic // Required: true Type *string `json:"type"` } @@ -63,11 +69,11 @@ func (m *PublicHTTPError) validateCode(formats strfmt.Registry) error { return err } - if err := validate.MinimumInt("status", "body", int64(*m.Code), 100, false); err != nil { + if err := validate.MinimumInt("status", "body", *m.Code, 100, false); err != nil { return err } - if err := validate.MaximumInt("status", "body", int64(*m.Code), 599, false); err != nil { + if err := validate.MaximumInt("status", "body", *m.Code, 599, false); err != nil { return err } @@ -92,6 +98,11 @@ func (m *PublicHTTPError) validateType(formats strfmt.Registry) error { return nil } +// ContextValidate validates this public Http error based on context it is used +func (m *PublicHTTPError) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + // MarshalBinary interface implementation func (m *PublicHTTPError) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/public_http_validation_error.go b/internal/types/public_http_validation_error.go index a37106f3..61dc38dc 100644 --- a/internal/types/public_http_validation_error.go +++ b/internal/types/public_http_validation_error.go @@ -6,6 +6,7 @@ package types // Editing this file might prove futile when you re-run the swagger generate command import ( + "context" "strconv" "github.com/go-openapi/errors" @@ -114,6 +115,43 @@ func (m *PublicHTTPValidationError) validateValidationErrors(formats strfmt.Regi return nil } +// ContextValidate validate this public Http validation error based on the context it is used +func (m *PublicHTTPValidationError) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + // validation for a type composition with PublicHTTPError + if err := m.PublicHTTPError.ContextValidate(ctx, formats); err != nil { + res = append(res, err) + } + + if err := m.contextValidateValidationErrors(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *PublicHTTPValidationError) contextValidateValidationErrors(ctx context.Context, formats strfmt.Registry) error { + + for i := 0; i < len(m.ValidationErrors); i++ { + + if m.ValidationErrors[i] != nil { + if err := m.ValidationErrors[i].ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("validationErrors" + "." + strconv.Itoa(i)) + } + return err + } + } + + } + + return nil +} + // MarshalBinary interface implementation func (m *PublicHTTPValidationError) MarshalBinary() ([]byte, error) { if m == nil { diff --git a/internal/types/spec_handlers.go b/internal/types/spec_handlers.go index 8e4901a8..719ea22e 100644 --- a/internal/types/spec_handlers.go +++ b/internal/types/spec_handlers.go @@ -41,6 +41,7 @@ func (o *SwaggerSpec) initHandlerCache() { o.Handlers["GET"]["/-/ready"] = true o.Handlers["GET"]["/swagger.yml"] = true o.Handlers["GET"]["/api/v1/auth/userinfo"] = true + o.Handlers["GET"]["/-/version"] = true o.Handlers["POST"]["/api/v1/auth/change-password"] = true o.Handlers["POST"]["/api/v1/auth/forgot-password/complete"] = true o.Handlers["POST"]["/api/v1/auth/forgot-password"] = true diff --git a/internal/util/cache_control.go b/internal/util/cache_control.go new file mode 100644 index 00000000..dc0a7d65 --- /dev/null +++ b/internal/util/cache_control.go @@ -0,0 +1,68 @@ +package util + +import ( + "context" + "strings" +) + +type CacheControlDirective uint8 + +const ( + CacheControlDirectiveNoCache CacheControlDirective = 1 << iota + CacheControlDirectiveNoStore +) + +func (d CacheControlDirective) HasDirective(dir CacheControlDirective) bool { return d&dir != 0 } +func (d *CacheControlDirective) AddDirective(dir CacheControlDirective) { *d |= dir } +func (d *CacheControlDirective) ClearDirective(dir CacheControlDirective) { *d &= ^dir } +func (d *CacheControlDirective) ToggleDirective(dir CacheControlDirective) { *d ^= dir } + +func (d CacheControlDirective) String() string { + res := make([]string, 0) + + if d.HasDirective(CacheControlDirectiveNoCache) { + res = append(res, "no-cache") + } + if d.HasDirective(CacheControlDirectiveNoStore) { + res = append(res, "no-store") + } + + return strings.Join(res, "|") +} + +func ParseCacheControlDirective(d string) CacheControlDirective { + parts := strings.Split(d, "=") + switch strings.ToLower(parts[0]) { + case "no-cache": + return CacheControlDirectiveNoCache + case "no-store": + return CacheControlDirectiveNoStore + default: + return 0 + } +} + +func ParseCacheControlHeader(val string) CacheControlDirective { + res := CacheControlDirective(0) + + directives := strings.Split(val, ",") + for _, dir := range directives { + res = res | ParseCacheControlDirective(dir) + } + + return CacheControlDirective(res) +} + +func CacheControlDirectiveFromContext(ctx context.Context) CacheControlDirective { + d := ctx.Value(CTXKeyCacheControl) + if d == nil { + return CacheControlDirective(0) + } + + directive, ok := d.(CacheControlDirective) + if !ok { + return CacheControlDirective(0) + } + + return directive +} diff --git a/internal/util/cache_control_test.go b/internal/util/cache_control_test.go new file mode 100644 index 00000000..be024f42 --- /dev/null +++ b/internal/util/cache_control_test.go @@ -0,0 +1,73 @@ +package util_test + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/api" + "allaboutapps.dev/aw/go-starter/internal/api/middleware" + "allaboutapps.dev/aw/go-starter/internal/test" + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCacheControl(t *testing.T) { + test.WithTestServer(t, func(s *api.Server) { + path := "/testing-1c2cad5f-7545-4177-9bfe-8dc7ed368b33" + + s.Echo.GET(path, func(c echo.Context) error { + cache := util.CacheControlDirectiveFromContext(c.Request().Context()) + if cache.HasDirective(util.CacheControlDirectiveNoCache) && + cache.HasDirective(util.CacheControlDirectiveNoStore) { + return c.JSON(http.StatusOK, "no-cache,no-store") + } else if cache.HasDirective(util.CacheControlDirectiveNoCache) { + return c.JSON(http.StatusOK, "no-cache") + } else if cache.HasDirective(util.CacheControlDirectiveNoStore) { + return c.JSON(http.StatusOK, "no-store") + } else { + return c.NoContent(http.StatusNoContent) + } + }, middleware.CacheControl()) + + header := http.Header{} + header.Set(util.HTTPHeaderCacheControl, fmt.Sprintf("%s,%s", util.CacheControlDirectiveNoStore, util.CacheControlDirectiveNoCache)) + + res := test.PerformRequest(t, s, "GET", path, nil, header) + require.Equal(t, http.StatusOK, res.Result().StatusCode) + + var resp string + err := json.NewDecoder(res.Result().Body).Decode(&resp) + require.NoError(t, err) + assert.Equal(t, "no-cache,no-store", resp) + + header.Set(util.HTTPHeaderCacheControl, util.CacheControlDirectiveNoCache.String()) + + res = test.PerformRequest(t, s, "GET", path, nil, header) + require.Equal(t, http.StatusOK, res.Result().StatusCode) + + err = json.NewDecoder(res.Result().Body).Decode(&resp) + require.NoError(t, err) + assert.Equal(t, "no-cache", resp) + + header.Set(util.HTTPHeaderCacheControl, util.CacheControlDirectiveNoStore.String()) + + res = test.PerformRequest(t, s, "GET", path, nil, header) + require.Equal(t, http.StatusOK, res.Result().StatusCode) + + err = json.NewDecoder(res.Result().Body).Decode(&resp) + require.NoError(t, err) + assert.Equal(t, "no-store", resp) + + res = test.PerformRequest(t, s, "GET", path, nil, nil) + assert.Equal(t, http.StatusNoContent, res.Result().StatusCode) + + header.Set(util.HTTPHeaderCacheControl, "gunther") + + res = test.PerformRequest(t, s, "GET", path, nil, header) + assert.Equal(t, http.StatusNoContent, res.Result().StatusCode) + }) +} diff --git a/internal/util/context.go b/internal/util/context.go index 09c11b0d..00ce4543 100644 --- a/internal/util/context.go +++ b/internal/util/context.go @@ -1,8 +1,58 @@ package util +import ( + "context" + "errors" +) + type contextKey string const ( - CTXKeyUser contextKey = "user" - CTXKeyAccessToken contextKey = "access_token" + CTXKeyUser contextKey = "user" + CTXKeyAccessToken contextKey = "access_token" + CTXKeyRequestID contextKey = "request_id" + CTXKeyDisableLogger contextKey = "disable_logger" + CTXKeyCacheControl contextKey = "cache_control" ) + +// RequestIDFromContext returns the ID of the (HTTP) request, returning an error if it is not present. +func RequestIDFromContext(ctx context.Context) (string, error) { + val := ctx.Value(CTXKeyRequestID) + if val == nil { + return "", errors.New("No request ID present in context") + } + + id, ok := val.(string) + if !ok { + return "", errors.New("Request ID in context is not a string") + } + + return id, nil +} + +// ShouldDisableLogger checks whether the logger instance should be disabled for the provided context. +// `util.LogFromContext` will use this function to check whether it should return a default logger if +// none has been set by our logging middleware before, or fall back to the disabled logger, suppressing +// all output. Use `ctx = util.DisableLogger(ctx, true)` to disable logging for the given context. +func ShouldDisableLogger(ctx context.Context) bool { + s := ctx.Value(CTXKeyDisableLogger) + if s == nil { + return false + } + + shouldDisable, ok := s.(bool) + if !ok { + return false + } + + return shouldDisable +} + +// DisableLogger toggles the indication whether `util.LogFromContext` should return a disabled logger +// for a context if none has been set by our logging middleware before. Whilst the usecase for a disabled +// logger are relatively minimal (we almost always want to have some log output, even if the context +// was not directly derived from a HTTP request), this functionality was provideds so you can switch back +// to the old zerolog behavior if so desired. +func DisableLogger(ctx context.Context, shouldDisable bool) context.Context { + return context.WithValue(ctx, CTXKeyDisableLogger, shouldDisable) +} diff --git a/internal/util/currency.go b/internal/util/currency.go index 8e414af7..c4b383a3 100644 --- a/internal/util/currency.go +++ b/internal/util/currency.go @@ -7,7 +7,7 @@ func Int64PtrWithCentsToFloat64Ptr(c *int64) *float64 { return nil } - return swag.Float64(float64(*c) / 100.0) + return Int64WithCentsToFloat64Ptr(*c) } func Int64WithCentsToFloat64Ptr(c int64) *float64 { @@ -19,7 +19,7 @@ func IntPtrWithCentsToFloat64Ptr(c *int) *float64 { return nil } - return swag.Float64(float64(*c) / 100.0) + return IntWithCentsToFloat64Ptr(*c) } func IntWithCentsToFloat64Ptr(c int) *float64 { @@ -31,7 +31,7 @@ func Float64PtrToInt64PtrWithCents(f *float64) *int64 { return nil } - return swag.Int64(int64(*f * 100)) + return swag.Int64(Float64PtrToInt64WithCents(f)) } func Float64PtrToInt64WithCents(f *float64) int64 { @@ -43,7 +43,7 @@ func Float64PtrToIntPtrWithCents(f *float64) *int { return nil } - return swag.Int(int(*f * 100)) + return swag.Int(Float64PtrToIntWithCents(f)) } func Float64PtrToIntWithCents(f *float64) int { diff --git a/internal/util/currency_test.go b/internal/util/currency_test.go new file mode 100644 index 00000000..58c18b28 --- /dev/null +++ b/internal/util/currency_test.go @@ -0,0 +1,65 @@ +package util_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/stretchr/testify/assert" +) + +func TestCurrencyConversion(t *testing.T) { + tests := []struct { + name string + val int + }{ + { + name: "1", + val: 999999999999999, // this is the max size with accurate precision + }, + { + name: "2", + val: 0, + }, + { + name: "3", + val: -999999999999999, + }, + { + name: "4", + val: 3333333333333333, + }, + { + name: "5", + val: 1111111111111111, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + in := int64(tt.val) + res := util.Int64PtrWithCentsToFloat64Ptr(&in) + out := util.Float64PtrToInt64PtrWithCents(res) + assert.Equal(t, in, *out) + + inInt := int(in) + res = util.IntPtrWithCentsToFloat64Ptr(&inInt) + outInt := util.Float64PtrToIntPtrWithCents(res) + assert.Equal(t, inInt, *outInt) + }) + } +} + +func TestCurrencyConversionNil(t *testing.T) { + res := util.Int64PtrWithCentsToFloat64Ptr(nil) + assert.Nil(t, res) + + res = util.IntPtrWithCentsToFloat64Ptr(nil) + assert.Nil(t, res) + + res2 := util.Float64PtrToInt64PtrWithCents(nil) + assert.Nil(t, res2) + + res3 := util.Float64PtrToIntPtrWithCents(nil) + assert.Nil(t, res3) +} diff --git a/internal/util/db/db.go b/internal/util/db/db.go index c1161b03..1d19f3f9 100644 --- a/internal/util/db/db.go +++ b/internal/util/db/db.go @@ -12,7 +12,11 @@ import ( type TxFn func(boil.ContextExecutor) error func WithTransaction(ctx context.Context, db *sql.DB, fn TxFn) error { - tx, err := db.BeginTx(ctx, nil) + return WithConfiguredTransaction(ctx, db, nil, fn) +} + +func WithConfiguredTransaction(ctx context.Context, db *sql.DB, options *sql.TxOptions, fn TxFn) error { + tx, err := db.BeginTx(ctx, options) if err != nil { util.LogFromContext(ctx).Warn().Err(err).Msg("Failed to start transaction") return err diff --git a/internal/util/db/db_test.go b/internal/util/db/db_test.go new file mode 100644 index 00000000..56266023 --- /dev/null +++ b/internal/util/db/db_test.go @@ -0,0 +1,166 @@ +package db_test + +import ( + "context" + "database/sql" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/models" + "allaboutapps.dev/aw/go-starter/internal/test" + "allaboutapps.dev/aw/go-starter/internal/util/db" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/volatiletech/null/v8" + "github.com/volatiletech/sqlboiler/v4/boil" + "github.com/volatiletech/sqlboiler/v4/types" +) + +func TestWithTransactionSuccess(t *testing.T) { + test.WithTestDatabase(t, func(sqlDB *sql.DB) { + ctx := context.Background() + + count, err := models.Users().Count(ctx, sqlDB) + require.NoError(t, err) + assert.Greater(t, count, int64(0)) + + err = db.WithTransaction(ctx, sqlDB, func(tx boil.ContextExecutor) error { + newUser := models.User{ + IsActive: true, + Username: null.StringFrom("test"), + Scopes: types.StringArray{"cms"}, + } + + if err := newUser.Insert(ctx, tx, boil.Infer()); err != nil { + return err + } + + newCount, err := models.Users().Count(ctx, tx) + require.NoError(t, err) + assert.Equal(t, count+1, newCount) + + delCnt, err := models.Users().DeleteAll(ctx, tx) + if err != nil { + return err + } + assert.Equal(t, newCount, delCnt) + + newCount, err = models.Users().Count(ctx, tx) + require.NoError(t, err) + assert.Equal(t, int64(0), newCount) + + return nil + }) + require.NoError(t, err) + + newCount, err := models.Users().Count(ctx, sqlDB) + require.NoError(t, err) + assert.Equal(t, int64(0), newCount) + }) +} + +func TestWithTransactionWithError(t *testing.T) { + test.WithTestDatabase(t, func(sqlDB *sql.DB) { + ctx := context.Background() + + count, err := models.Users().Count(ctx, sqlDB) + require.NoError(t, err) + assert.Greater(t, count, int64(0)) + + err = db.WithTransaction(ctx, sqlDB, func(tx boil.ContextExecutor) error { + newUser := models.User{ + IsActive: true, + Username: null.StringFrom("test"), + Scopes: types.StringArray{"cms"}, + } + + err := newUser.Insert(ctx, tx, boil.Infer()) + require.NoError(t, err) + + newCount, err := models.Users().Count(ctx, tx) + require.NoError(t, err) + assert.Equal(t, count+1, newCount) + + delCnt, err := models.Users().DeleteAll(ctx, tx) + require.NoError(t, err) + assert.Equal(t, newCount, delCnt) + + newCount, err = models.Users().Count(ctx, tx) + require.NoError(t, err) + assert.Equal(t, int64(0), newCount) + + newUser2 := models.User{ + IsActive: true, + Username: null.StringFrom("test"), + } + + return newUser2.Insert(ctx, tx, boil.Infer()) + }) + require.Error(t, err) + + newCount, err := models.Users().Count(ctx, sqlDB) + require.NoError(t, err) + assert.Equal(t, count, newCount) + }) +} + +func TestWithTransactionWithPanic(t *testing.T) { + test.WithTestDatabase(t, func(sqlDB *sql.DB) { + ctx := context.Background() + + count, err := models.Users().Count(ctx, sqlDB) + require.NoError(t, err) + assert.Greater(t, count, int64(0)) + + panicFunc := func() { + //nolint:errcheck + _ = db.WithTransaction(ctx, sqlDB, func(tx boil.ContextExecutor) error { + newUser := models.User{ + IsActive: true, + Username: null.StringFrom("test"), + Scopes: types.StringArray{"cms"}, + } + + err := newUser.Insert(ctx, tx, boil.Infer()) + require.NoError(t, err) + + newCount, err := models.Users().Count(ctx, tx) + require.NoError(t, err) + assert.Equal(t, count+1, newCount) + + delCnt, err := models.Users().DeleteAll(ctx, tx) + require.NoError(t, err) + assert.Equal(t, newCount, delCnt) + + newCount, err = models.Users().Count(ctx, tx) + require.NoError(t, err) + assert.Equal(t, int64(0), newCount) + + panic("some panic") + }) + } + + require.Panics(t, panicFunc) + + newCount, err := models.Users().Count(ctx, sqlDB) + require.NoError(t, err) + assert.Equal(t, count, newCount) + }) +} + +func TestDBTypeConversions(t *testing.T) { + i := int64(19) + res := db.NullIntFromInt64Ptr(&i) + assert.Equal(t, 19, res.Int) + assert.True(t, res.Valid) + + res = db.NullIntFromInt64Ptr(nil) + assert.False(t, res.Valid) + + f := 19.9999 + res2 := db.NullFloat32FromFloat64Ptr(&f) + assert.Equal(t, float32(19.9999), res2.Float32) + assert.True(t, res2.Valid) + + res2 = db.NullFloat32FromFloat64Ptr(nil) + assert.False(t, res2.Valid) +} diff --git a/internal/util/db/example_test.go b/internal/util/db/example_test.go new file mode 100644 index 00000000..4b3e51d7 --- /dev/null +++ b/internal/util/db/example_test.go @@ -0,0 +1,69 @@ +package db_test + +import ( + "fmt" + + "allaboutapps.dev/aw/go-starter/internal/models" + "allaboutapps.dev/aw/go-starter/internal/util/db" + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +type PublicName struct { + First string `json:"firstName"` +} + +type Name struct { + PublicName + MiddleName string `json:"-"` + Lastname string `json:"lastName"` +} + +type UserFilter struct { + Name + Country string `json:"country"` + City string + Scopes []string `json:"scopes"` + Age *int `json:"age"` + Height *float32 `json:"height"` +} + +func ExampleWhereJSON() { + age := 42 + filter := UserFilter{ + Name: Name{ + PublicName: PublicName{ + First: "Max", + }, + MiddleName: "Gustav", + Lastname: "Muster", + }, + Country: "Austria", + City: "Vienna", + Scopes: []string{"app", "user_info"}, + Age: &age, + } + + q := models.NewQuery( + qm.Select("*"), + qm.From("users"), + db.WhereJSON("users", "profile", filter), + ) + + sql, args := queries.BuildQuery(q) + + fmt.Println(sql) + fmt.Print("[") + for i := range args { + if i < len(args)-1 { + fmt.Printf("%v, ", args[i]) + } else { + fmt.Printf("%v", args[i]) + } + } + fmt.Println("]") + + // Output: + // SELECT * FROM "users" WHERE (users.profile->>'firstName' = $1 AND users.profile->>'lastName' = $2 AND users.profile->>'country' = $3 AND users.profile->'scopes' <@ to_jsonb($4::text[]) AND users.profile->>'age' = $5); + // [Max, Muster, Austria, &[app user_info], 42] +} diff --git a/internal/util/db/ilike.go b/internal/util/db/ilike.go index c955c115..3670aa06 100644 --- a/internal/util/db/ilike.go +++ b/internal/util/db/ilike.go @@ -7,6 +7,12 @@ import ( "github.com/volatiletech/sqlboiler/v4/queries/qm" ) +// ILike returns a query mod containing a pre-formatted ILIKE clause. +// The value provided is applied directly - to perform a wildcard search, +// enclose the desired search value in `%` as desired before passing it +// to ILike. +// The path provided will be joined to construct the full SQL path used, +// allowing for filtering of values nested across multiple joins if needed. func ILike(val string, path ...string) qm.QueryMod { // ! Attention: we **must** use ? instead of $1 or similar to bind query parameters here since // ! other parts of the query might have already defined $1, leading to incorrect parameters diff --git a/internal/util/db/ilike_test.go b/internal/util/db/ilike_test.go new file mode 100644 index 00000000..467474e3 --- /dev/null +++ b/internal/util/db/ilike_test.go @@ -0,0 +1,26 @@ +package db_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/models" + "allaboutapps.dev/aw/go-starter/internal/test" + "allaboutapps.dev/aw/go-starter/internal/util/db" + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +func TestILike(t *testing.T) { + q := models.NewQuery( + qm.Select("*"), + qm.From("users"), + db.InnerJoin("users", "id", "app_user_profiles", "user_id"), + db.ILike("%Max.Muster%", "users", "username"), + db.ILike("Max", "users", "app_user_profiles", "first_name"), + ) + + sql, args := queries.BuildQuery(q) + + test.Snapshoter.Label("SQL").Save(t, sql) + test.Snapshoter.Label("Args").Save(t, args) +} diff --git a/internal/util/db/join.go b/internal/util/db/join.go index c788037a..394d8fbf 100644 --- a/internal/util/db/join.go +++ b/internal/util/db/join.go @@ -33,3 +33,31 @@ func InnerJoin(baseTable string, baseColumn string, joinTable string, joinColumn baseTable, baseColumn)) } + +// LeftOuterJoin returns an LeftOuterJoin QueryMod formatted using the provided join tables and columns. +func LeftOuterJoin(baseTable string, baseColumn string, joinTable string, joinColumn string) qm.QueryMod { + return qm.LeftOuterJoin(fmt.Sprintf("%s ON %s.%s=%s.%s", + joinTable, + joinTable, + joinColumn, + baseTable, + baseColumn)) +} + +// LeftOuterJoinWithFilter returns an LeftOuterJoin QueryMod formatted using the provided join tables and columns including an +// additional filter condition. Omitting the optional filter table will use the provided join table as a base for the filter. +func LeftOuterJoinWithFilter(baseTable string, baseColumn string, joinTable string, joinColumn string, filterColumn string, filterValue interface{}, optFilterTable ...string) qm.QueryMod { + filterTable := joinTable + if len(optFilterTable) > 0 { + filterTable = optFilterTable[0] + } + + return qm.LeftOuterJoin(fmt.Sprintf("%s ON %s.%s=%s.%s AND %s.%s=$1", + joinTable, + joinTable, + joinColumn, + baseTable, + baseColumn, + filterTable, + filterColumn), filterValue) +} diff --git a/internal/util/db/join_test.go b/internal/util/db/join_test.go new file mode 100644 index 00000000..e9281dd3 --- /dev/null +++ b/internal/util/db/join_test.go @@ -0,0 +1,104 @@ +package db_test + +import ( + "context" + "database/sql" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/models" + "allaboutapps.dev/aw/go-starter/internal/test" + "allaboutapps.dev/aw/go-starter/internal/util/db" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/volatiletech/null/v8" + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +func TestInnerJoinWithFilter(t *testing.T) { + test.WithTestDatabase(t, func(sqlDB *sql.DB) { + ctx := context.Background() + fixtures := test.Fixtures() + + profiles, err := models.AppUserProfiles(db.InnerJoinWithFilter(models.TableNames.AppUserProfiles, + models.AppUserProfileColumns.UserID, + models.TableNames.Users, + models.UserColumns.ID, + models.UserColumns.Username, + "user1@example.com", + )).All(ctx, sqlDB) + require.NoError(t, err) + require.Len(t, profiles, 1) + + assert.Equal(t, fixtures.User1AppUserProfile.UserID, profiles[0].UserID) + + profiles, err = models.AppUserProfiles(db.InnerJoinWithFilter(models.TableNames.AppUserProfiles, + models.AppUserProfileColumns.UserID, + models.TableNames.Users, + models.UserColumns.ID, + models.UserColumns.Username, + "user1@example.com", + models.TableNames.Users, + )).All(ctx, sqlDB) + require.NoError(t, err) + require.Len(t, profiles, 1) + + assert.Equal(t, fixtures.User1AppUserProfile.UserID, profiles[0].UserID) + }) +} + +func TestInnerJoin(t *testing.T) { + test.WithTestDatabase(t, func(sqlDB *sql.DB) { + ctx := context.Background() + fixtures := test.Fixtures() + + profiles, err := models.AppUserProfiles(db.InnerJoin(models.TableNames.AppUserProfiles, + models.AppUserProfileColumns.UserID, + models.TableNames.Users, + models.UserColumns.ID, + ), + models.UserWhere.Username.EQ(null.StringFrom("user1@example.com")), + ).All(ctx, sqlDB) + require.NoError(t, err) + require.Len(t, profiles, 1) + + assert.Equal(t, fixtures.User1AppUserProfile.UserID, profiles[0].UserID) + }) +} + +func TestLeftOuterJoinWithFilter(t *testing.T) { + q := models.NewQuery( + qm.Select("*"), + qm.From("users"), + db.LeftOuterJoinWithFilter("users", "id", "app_user_profiles", "user_id", "first_name", "Max"), + ) + + sql, args := queries.BuildQuery(q) + + test.Snapshoter.Label("SQL").Save(t, sql) + test.Snapshoter.Label("Args").Save(t, args) + + q = models.NewQuery( + qm.Select("*"), + qm.From("users"), + db.LeftOuterJoinWithFilter("users", "id", "app_user_profiles", "user_id", "first_name", "Max", "app_user_profiles"), + ) + + sql, args = queries.BuildQuery(q) + + test.Snapshoter.Label("SQL").Save(t, sql) + test.Snapshoter.Label("Args").Save(t, args) +} + +func TestLeftOuterJoin(t *testing.T) { + q := models.NewQuery( + qm.Select("*"), + qm.From("users"), + db.LeftOuterJoin("users", "id", "app_user_profiles", "user_id"), + ) + + sql, args := queries.BuildQuery(q) + + test.Snapshoter.Label("SQL").Save(t, sql) + test.Snapshoter.Label("Args").Save(t, args) +} diff --git a/internal/util/db/json.go b/internal/util/db/json.go new file mode 100644 index 00000000..eb8dab7d --- /dev/null +++ b/internal/util/db/json.go @@ -0,0 +1,125 @@ +package db + +import ( + "errors" + "fmt" + "reflect" + "strings" + + "github.com/lib/pq" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +const ( + whereJSONMaxLevel = 10 +) + +// WhereJSON constructs a QueryMod for querying a JSONB column. +// +// The filter interface provided is inspected using reflection, all fields with +// a (non-empty) `json` tag will be added to the query and combined using `AND` - +// fields tagged with `json:"-"` will be ignored as well. Alternatively, a string +// can be provided, performing a string comparison with the database value (the +// stored JSON value does not necessarily have to be a string, but could be an +// integer or similar). The `json` tag's (first) value will be used as the "key" +// for the query, allowing for field renaming or different capitalizations. +// +// At the moment, the root level `filter` value must either be a struct or a string. +// WhereJSON will panic should it encounter a type it cannot process or the filter +// provided results in an empty QueryMod - this allows for easier call chaining +// at the expense of panics in case of incorrect filters being passed. +// +// WhereJSON should support all basic types as well as pointers and array/slices +// of those out of the box, given the Postgres driver can handle their serialization. +// nil pointers are skipped automatically. +// At the moment, struct fields are only supported for composition purposes: if a +// struct is encountered, WhereJSON recursively traverses it (up to 10 levels deep) +// and adds all eligible fields to the top level query. +// Should an array or slice be encountered, their values will be added using the +// `<@` JSONB operator, checking whether all entries existx at the top level within +// the JSON column. +// At the time of writing, no support for special database/HTTP types such as the +// `null` or `strfmt` packages exists - use their respective base types instead. +// +// Whilst WhereJSON was designed to be used with Postgres' JSONB column type, the +// current implementation also supports the JSON type as long as the filter struct +// does not contain any arrays or slices. Note that this compatibility might change +// at some point in the future, so it is advised to use the JSONB data type unless +// your requirements do not allow for it. +func WhereJSON(table string, column string, filter interface{}) qm.QueryMod { + qms := whereJSON(table, column, filter, 0) + if len(qms) == 0 { + panic(errors.New("filter resulted in empty query")) + } + return qm.Expr(qms...) +} + +func whereJSON(table string, column string, filter interface{}, level int) []qm.QueryMod { + if level >= whereJSONMaxLevel { + panic(fmt.Errorf("whereJSON reached maximum recursion (%d/%d)", level, whereJSONMaxLevel)) + } + + qms := make([]qm.QueryMod, 0) + + rt := reflect.TypeOf(filter) + switch rt.Kind() { + case reflect.Struct: + rv := reflect.ValueOf(filter) + for i := 0; i < rt.NumField(); i++ { + f := rt.Field(i) + + // skip unexported fields as we cannot retrieve their values + if len(f.PkgPath) != 0 { + continue + } + + k := strings.Split(f.Tag.Get("json"), ",")[0] + if k == "-" { + continue + } + + fs := rv.Field(i) + if fs.Kind() != reflect.Struct && k == "" { + continue + } + + isArray := false + var v interface{} + switch fs.Kind() { + case reflect.Struct: + qms = append(qms, whereJSON(table, column, fs.Interface(), level+1)...) + continue + case reflect.Ptr: + if !fs.IsValid() || fs.IsNil() { + continue + } + if fs.Elem().Kind() == reflect.Array || + fs.Elem().Kind() == reflect.Slice { + isArray = true + } + v = fs.Elem().Interface() + case reflect.Array, + reflect.Slice: + if !fs.IsValid() || fs.IsNil() { + continue + } + isArray = true + v = fs.Interface() + default: + v = fs.Interface() + } + + if isArray { + qms = append(qms, qm.Where(fmt.Sprintf("%s.%s->'%s' <@ to_jsonb(?::text[])", table, column, k), pq.Array(v))) + } else { + qms = append(qms, qm.Where(fmt.Sprintf("%s.%s->>'%s' = ?", table, column, k), v)) + } + } + case reflect.String: + qms = append(qms, qm.Where(fmt.Sprintf("%s.%s::text = ?", table, column), filter)) + default: + panic(fmt.Errorf("invalid filter type %v", rt.Kind())) + } + + return qms +} diff --git a/internal/util/db/json_test.go b/internal/util/db/json_test.go new file mode 100644 index 00000000..a99c5d21 --- /dev/null +++ b/internal/util/db/json_test.go @@ -0,0 +1,205 @@ +package db_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/models" + "allaboutapps.dev/aw/go-starter/internal/test" + "allaboutapps.dev/aw/go-starter/internal/util/db" + "github.com/stretchr/testify/require" + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +func TestWhereJSONStruct(t *testing.T) { + age := 42 + filter := struct { + First string `json:"firstName"` + MiddleName string `json:"-"` + Lastname string `json:"lastName"` + Country string `json:"country"` + City string + Scopes []string `json:"scopes"` + Age *int `json:"age"` + Height *float32 `json:"height"` + PhoneNumbers *[2]string `json:"phoneNumbers"` + Addresses []string `json:"addresses"` + }{ + First: "Max", + MiddleName: "Gustav", + Lastname: "Muster", + Country: "Austria", + City: "Vienna", + Scopes: []string{"app", "user_info"}, + Age: &age, + PhoneNumbers: &[2]string{ + "+1 206 555 0100", + "+44 113 496 0000", + }, + } + + sql, args := buildWhereJSONQuery(t, filter) + + test.Snapshoter.Label("SQL").Save(t, sql) + test.Snapshoter.Label("Args").Save(t, args) +} + +func TestWhereJSONStructComposition(t *testing.T) { + age := 42 + filter := UserFilter{ + Name: Name{ + PublicName: PublicName{ + First: "Max", + }, + MiddleName: "Gustav", + Lastname: "Muster", + }, + Country: "Austria", + City: "Vienna", + Scopes: []string{"app", "user_info"}, + Age: &age, + } + + sql, args := buildWhereJSONQuery(t, filter) + + test.Snapshoter.Label("SQL").Save(t, sql) + test.Snapshoter.Label("Args").Save(t, args) +} + +func TestWhereJSONString(t *testing.T) { + sql, args := buildWhereJSONQuery(t, "https://example.org/users/123/profile") + + test.Snapshoter.Label("SQL").Save(t, sql) + test.Snapshoter.Label("Args").Save(t, args) +} + +func TestWhereJSONPanicEmptyResult(t *testing.T) { + type privateName struct { + First string `json:"firstName"` + MiddleName string `json:"-"` + Lastname string `json:"lastName"` + } + + filter := struct { + privateName + City string + }{ + privateName: privateName{ + First: "Max", + MiddleName: "Gustav", + Lastname: "Muster", + }, + City: "Vienna", + } + + panicFunc := func() { + db.WhereJSON("users", "profile", filter) + } + + require.PanicsWithError(t, "filter resulted in empty query", panicFunc) +} + +func TestWhereJSONPanicInvalidFilterType(t *testing.T) { + panicFunc := func() { + db.WhereJSON("users", "profile", 1) + } + + require.PanicsWithError(t, "invalid filter type int", panicFunc) +} + +func TestWhereJSONPanicRecursion(t *testing.T) { + type A struct { + One string `json:"one"` + } + type B struct { + A + Two string `json:"two"` + } + type C struct { + B + Three string `json:"three"` + } + type D struct { + C + Four string `json:"four"` + } + type E struct { + D + Five string `json:"five"` + } + type F struct { + E + Six string `json:"six"` + } + type G struct { + F + Seven string `json:"seven"` + } + type H struct { + G + Eight string `json:"eight"` + } + type I struct { + H + Nine string `json:"nine"` + } + type J struct { + I + Ten string `json:"ten"` + } + + filter := struct { + J + Country string `json:"country"` + }{ + J: J{ + I: I{ + H: H{ + G: G{ + F: F{ + E: E{ + D: D{ + C: C{ + B: B{ + A: A{ + One: "1", + }, + Two: "2", + }, + Three: "3", + }, + Four: "4", + }, + Five: "5", + }, + Six: "6", + }, + Seven: "7", + }, + Eight: "8", + }, + Nine: "9", + }, + Ten: "10", + }, + Country: "Austria", + } + + panicFunc := func() { + db.WhereJSON("users", "profile", filter) + } + + require.PanicsWithError(t, "whereJSON reached maximum recursion (10/10)", panicFunc) +} + +func buildWhereJSONQuery(t *testing.T, filter interface{}) (string, []interface{}) { + t.Helper() + + q := models.NewQuery( + qm.Select("*"), + qm.From("users"), + db.WhereJSON("users", "profile", filter), + ) + + return queries.BuildQuery(q) +} diff --git a/internal/util/db/or.go b/internal/util/db/or.go new file mode 100644 index 00000000..53d431a7 --- /dev/null +++ b/internal/util/db/or.go @@ -0,0 +1,22 @@ +package db + +import "github.com/volatiletech/sqlboiler/v4/queries/qm" + +// CombineWithOr receives a slice of query mods and returns a new slice with +// a single query mod, combining all other query mods into an OR expression. +func CombineWithOr(qms []qm.QueryMod) []qm.QueryMod { + if len(qms) == 0 { + return []qm.QueryMod{} + } + + if len(qms) == 1 { + return qms + } + + q := []qm.QueryMod{qms[0]} + for _, sq := range qms[1:] { + q = append(q, qm.Or2(sq)) + } + + return []qm.QueryMod{qm.Expr(q...)} +} diff --git a/internal/util/db/or_test.go b/internal/util/db/or_test.go new file mode 100644 index 00000000..cedb2b43 --- /dev/null +++ b/internal/util/db/or_test.go @@ -0,0 +1,67 @@ +package db_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/models" + "allaboutapps.dev/aw/go-starter/internal/test" + "allaboutapps.dev/aw/go-starter/internal/util/db" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +func TestOr(t *testing.T) { + age := 42 + filter := UserFilter{ + Name: Name{ + PublicName: PublicName{ + First: "Max", + }, + MiddleName: "Gustav", + Lastname: "Muster", + }, + Country: "Austria", + City: "Vienna", + Scopes: []string{"app", "user_info"}, + Age: &age, + } + + qms := []qm.QueryMod{ + qm.Where("id = ?", 123), + qm.Where("username = ?", "max.muster@example.org"), + db.WhereJSON("users", "profile", filter), + } + sql, args := buildOrQuery(t, qms) + + test.Snapshoter.Label("SQL").Save(t, sql) + test.Snapshoter.Label("Args").Save(t, args) +} + +func TestOrSingle(t *testing.T) { + q := qm.Where("username = ?", "max.muster@example.org") + qms := db.CombineWithOr([]qm.QueryMod{q}) + require.Len(t, qms, 1) + assert.Equal(t, q, qms[0]) +} + +func TestOrEmpty(t *testing.T) { + qms := db.CombineWithOr([]qm.QueryMod{}) + assert.Empty(t, qms) + + qms = db.CombineWithOr(nil) + assert.Empty(t, qms) +} + +func buildOrQuery(t *testing.T, qms []qm.QueryMod) (string, []interface{}) { + t.Helper() + + o := db.CombineWithOr(qms) + require.Greater(t, len(o), 0) + + o = append(o, qm.Select("*"), qm.From("users")) + q := models.NewQuery(o...) + + return queries.BuildQuery(q) +} diff --git a/internal/util/db/order_by_test.go b/internal/util/db/order_by_test.go new file mode 100644 index 00000000..806b51d3 --- /dev/null +++ b/internal/util/db/order_by_test.go @@ -0,0 +1,65 @@ +package db_test + +import ( + "context" + "database/sql" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/models" + "allaboutapps.dev/aw/go-starter/internal/test" + swaggerTypes "allaboutapps.dev/aw/go-starter/internal/types" + "allaboutapps.dev/aw/go-starter/internal/util/db" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/volatiletech/null/v8" + "github.com/volatiletech/sqlboiler/v4/boil" + "github.com/volatiletech/sqlboiler/v4/types" +) + +func TestOrderBy(t *testing.T) { + test.WithTestDatabase(t, func(sqlDB *sql.DB) { + ctx := context.Background() + fixtures := test.Fixtures() + + noUsername := models.User{ + Scopes: types.StringArray{"cms"}, + } + + upperUsername := models.User{ + Username: null.StringFrom("USER3@example.com"), + Scopes: types.StringArray{"cms"}, + } + + err := noUsername.Insert(ctx, sqlDB, boil.Infer()) + require.NoError(t, err) + + err = upperUsername.Insert(ctx, sqlDB, boil.Infer()) + require.NoError(t, err) + + users, err := models.Users(db.OrderBy(swaggerTypes.OrderDirAsc, models.TableNames.Users, models.UserColumns.Username)).All(ctx, sqlDB) + require.NoError(t, err) + require.NotEmpty(t, users) + assert.Equal(t, upperUsername.ID, users[0].ID) + assert.Equal(t, upperUsername.Username, users[0].Username) + + users, err = models.Users(db.OrderByLower(swaggerTypes.OrderDirAsc, models.TableNames.Users, models.UserColumns.Username)).All(ctx, sqlDB) + require.NoError(t, err) + require.NotEmpty(t, users) + assert.Equal(t, fixtures.User1.ID, users[0].ID) + assert.Equal(t, fixtures.User1.Username, users[0].Username) + + users, err = models.Users(db.OrderByWithNulls(swaggerTypes.OrderDirAsc, db.OrderByNullsFirst, models.TableNames.Users, models.UserColumns.Username)).All(ctx, sqlDB) + require.NoError(t, err) + require.NotEmpty(t, users) + assert.Equal(t, noUsername.ID, users[0].ID) + assert.Equal(t, noUsername.Username, users[0].Username) + + users, err = models.Users(db.OrderByLowerWithNulls(swaggerTypes.OrderDirDesc, db.OrderByNullsLast, models.TableNames.Users, models.UserColumns.Username)).All(ctx, sqlDB) + require.NoError(t, err) + require.NotEmpty(t, users) + assert.Equal(t, fixtures.UserDeactivated.ID, users[0].ID) + assert.Equal(t, fixtures.UserDeactivated.Username, users[0].Username) + assert.Equal(t, upperUsername.ID, users[1].ID) + assert.Equal(t, upperUsername.Username, users[1].Username) + }) +} diff --git a/internal/util/db/query_mods.go b/internal/util/db/query_mods.go new file mode 100644 index 00000000..42cbc4bf --- /dev/null +++ b/internal/util/db/query_mods.go @@ -0,0 +1,17 @@ +package db + +import ( + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" +) + +// QueryMods represents a slice of query mods, implementing the `queries.Applicator` +// interface to allow for usage with eager loading methods of models. Unfortunately, +// sqlboiler does not import the (identical) type used by the library, so we have to +// declare and "implemented" it ourselves... +type QueryMods []qm.QueryMod + +// Apply applies the query mods to the query provided +func (m QueryMods) Apply(q *queries.Query) { + qm.Apply(q, m...) +} diff --git a/internal/util/db/ts_vector.go b/internal/util/db/ts_vector.go index b22c701c..c7350436 100644 --- a/internal/util/db/ts_vector.go +++ b/internal/util/db/ts_vector.go @@ -1,7 +1,28 @@ package db -import "strings" +import ( + "regexp" + "strings" +) -func SearchStringToTSQuery(s string) string { - return strings.ReplaceAll(strings.Trim(s, " "), " ", ":* & ") + ":*" +var ( + tsQueryWhiteSpaceRegex = regexp.MustCompile(`\s+`) +) + +// SearchStringToTSQuery returns a TSQuery string from user input. +// The resulting query will match if every word matches a beginning of a word in the row. +// This function will trim all leading and trailing as well as consecutive whitespaces and remove all single quotes before +// transforming the input into TSQuery syntax. +// If no input was given (nil or empty string) or the value only contains invalid characters, an empty string will be returned. +func SearchStringToTSQuery(s *string) string { + if s == nil || len(*s) == 0 { + return "" + } + + v := strings.TrimSpace(strings.ReplaceAll(*s, "'", "")) + if len(v) == 0 { + return "" + } + + return "'" + tsQueryWhiteSpaceRegex.ReplaceAllString(v, "':* & '") + "':*" } diff --git a/internal/util/db/ts_vector_test.go b/internal/util/db/ts_vector_test.go new file mode 100644 index 00000000..fd5fdd29 --- /dev/null +++ b/internal/util/db/ts_vector_test.go @@ -0,0 +1,41 @@ +package db_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/util/db" + "github.com/go-openapi/swag" + "github.com/stretchr/testify/assert" +) + +func TestSearchStringToTSQuery(t *testing.T) { + expected := "'abcde':* & '12345':* & 'xyz':*" + in := swag.String(" abcde 12345 xyz ") + out := db.SearchStringToTSQuery(in) + assert.Equal(t, expected, out) + + expected = "'abcde':*" + in = swag.String("abcde") + out = db.SearchStringToTSQuery(in) + assert.Equal(t, expected, out) + + expected = "'Hello':* & 'world':* & 'lorem':* & '12345':* & 'ipsum':* & 'abc':* & 'def':*" + in = swag.String(" Hello world lorem 12345 ipsum abc def ") + out = db.SearchStringToTSQuery(in) + assert.Equal(t, expected, out) + + expected = "" + in = nil + out = db.SearchStringToTSQuery(in) + assert.Equal(t, expected, out) + + expected = "" + in = swag.String("") + out = db.SearchStringToTSQuery(in) + assert.Equal(t, expected, out) + + expected = "" + in = swag.String(" '' ' ' ' ") + out = db.SearchStringToTSQuery(in) + assert.Equal(t, expected, out) +} diff --git a/internal/util/env.go b/internal/util/env.go index f132318a..9b2d510b 100644 --- a/internal/util/env.go +++ b/internal/util/env.go @@ -3,7 +3,6 @@ package util import ( "net/url" "os" - "path/filepath" "strconv" "strings" "sync" @@ -11,9 +10,13 @@ import ( "github.com/rs/zerolog/log" ) +const ( + mgmtSecretLen = 16 +) + var ( - projectRootDir string - dirOnce sync.Once + mgmtSecret string + mgmtSecretOnce sync.Once ) func GetEnv(key string, defaultVal string) string { @@ -24,6 +27,24 @@ func GetEnv(key string, defaultVal string) string { return defaultVal } +func GetEnvEnum(key string, defaultVal string, allowedValues []string) string { + if !ContainsString(allowedValues, defaultVal) { + log.Panic().Str("key", key).Str("value", defaultVal).Msg("Default value is not in the allowed values list.") + } + + val, ok := os.LookupEnv(key) + if !ok { + return defaultVal + } + + if !ContainsString(allowedValues, val) { + log.Error().Str("key", key).Str("value", val).Msg("Value is not allowed. Fallback to default value.") + return defaultVal + } + + return val +} + func GetEnvAsInt(key string, defaultVal int) int { strVal := GetEnv(key, "") @@ -64,6 +85,7 @@ func GetEnvAsBool(key string, defaultVal bool) bool { return defaultVal } +// GetEnvAsStringArr reads ENV and returns the values split by separator. func GetEnvAsStringArr(key string, defaultVal []string, separator ...string) []string { strVal := GetEnv(key, "") @@ -79,13 +101,24 @@ func GetEnvAsStringArr(key string, defaultVal []string, separator ...string) []s return strings.Split(strVal, sep) } +// GetEnvAsStringArrTrimmed reads ENV and returns the whitespace trimmed values split by separator. +func GetEnvAsStringArrTrimmed(key string, defaultVal []string, separator ...string) []string { + slc := GetEnvAsStringArr(key, defaultVal, separator...) + + for i := range slc { + slc[i] = strings.TrimSpace(slc[i]) + } + + return slc +} + func GetEnvAsURL(key string, defaultVal string) *url.URL { strVal := GetEnv(key, "") if len(strVal) == 0 { u, err := url.Parse(defaultVal) if err != nil { - log.Panic().Err(err).Msg("Failed to parse default value for env variable as URL") + log.Panic().Str("key", key).Str("defaultVal", defaultVal).Err(err).Msg("Failed to parse default value for env variable as URL") } return u @@ -93,21 +126,33 @@ func GetEnvAsURL(key string, defaultVal string) *url.URL { u, err := url.Parse(strVal) if err != nil { - log.Panic().Err(err).Msg("Failed to parse env variable as URL") + log.Panic().Str("key", key).Str("strVal", strVal).Err(err).Msg("Failed to parse env variable as URL") } return u } -func GetProjectRootDir() string { - dirOnce.Do(func() { - ex, err := os.Executable() +// GetMgmtSecret returns the management secret for the app server, mainly used by health check and readiness endpoints. +// It first attempts to retrieve a value from the given environment variable and generates a cryptographically secure random string +// should no env var have been set. +// Failure to generate a random string will cause a panic as secret security cannot be guaranteed otherwise. +// Subsequent calls to GetMgmtSecret during the server's runtime will always return the same randomly generated secret for consistency. +func GetMgmtSecret(envKey string) string { + val := GetEnv(envKey, "") + + if len(val) > 0 { + return val + } + + mgmtSecretOnce.Do(func() { + var err error + mgmtSecret, err = GenerateRandomHexString(mgmtSecretLen) if err != nil { - log.Panic().Err(err).Msg("Failed to get executable path while retrieving project root directory") + log.Panic().Err(err).Msg("Failed to generate random management secret") } - projectRootDir = GetEnv("PROJECT_ROOT_DIR", filepath.Dir(ex)) + log.Warn().Str("envKey", envKey).Str("mgmtSecret", mgmtSecret).Msg("Could not retrieve management secret from env key, using randomly generated one") }) - return projectRootDir + return mgmtSecret } diff --git a/internal/util/env_test.go b/internal/util/env_test.go new file mode 100644 index 00000000..b4d8cdf6 --- /dev/null +++ b/internal/util/env_test.go @@ -0,0 +1,213 @@ +package util_test + +import ( + "fmt" + "net/url" + "os" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetEnv(t *testing.T) { + testVarKey := "TEST_ONLY_FOR_UNIT_TEST_STRING" + res := util.GetEnv(testVarKey, "noVal") + assert.Equal(t, "noVal", res) + + os.Setenv(testVarKey, "string") + defer os.Unsetenv(testVarKey) + res = util.GetEnv(testVarKey, "noVal") + assert.Equal(t, "string", res) +} + +func TestGetEnvEnum(t *testing.T) { + testVarKey := "TEST_ONLY_FOR_UNIT_TEST_ENUM" + + panicFunc := func() { + _ = util.GetEnvEnum(testVarKey, "smtp", []string{"mock", "foo"}) + } + assert.Panics(t, panicFunc) + + res := util.GetEnvEnum(testVarKey, "smtp", []string{"mock", "smtp"}) + assert.Equal(t, "smtp", res) + + os.Setenv(testVarKey, "mock") + defer os.Unsetenv(testVarKey) + res = util.GetEnvEnum(testVarKey, "smtp", []string{"mock", "smtp"}) + assert.Equal(t, "mock", res) + + os.Setenv(testVarKey, "foo") + res = util.GetEnvEnum(testVarKey, "smtp", []string{"mock", "smtp"}) + assert.Equal(t, "smtp", res) +} + +func TestGetEnvAsInt(t *testing.T) { + testVarKey := "TEST_ONLY_FOR_UNIT_TEST_INT" + res := util.GetEnvAsInt(testVarKey, 1) + assert.Equal(t, 1, res) + + os.Setenv(testVarKey, "2") + defer os.Unsetenv(testVarKey) + res = util.GetEnvAsInt(testVarKey, 1) + assert.Equal(t, 2, res) + + os.Setenv(testVarKey, "3x") + res = util.GetEnvAsInt(testVarKey, 1) + assert.Equal(t, 1, res) +} + +func TestGetEnvAsUint32(t *testing.T) { + testVarKey := "TEST_ONLY_FOR_UNIT_TEST_UINT32" + res := util.GetEnvAsUint32(testVarKey, 1) + assert.Equal(t, uint32(1), res) + + os.Setenv(testVarKey, "2") + defer os.Unsetenv(testVarKey) + res = util.GetEnvAsUint32(testVarKey, 1) + assert.Equal(t, uint32(2), res) + + os.Setenv(testVarKey, "3x") + res = util.GetEnvAsUint32(testVarKey, 1) + assert.Equal(t, uint32(1), res) +} + +func TestGetEnvAsUint8(t *testing.T) { + testVarKey := "TEST_ONLY_FOR_UNIT_TEST_UINT8" + res := util.GetEnvAsUint8(testVarKey, 1) + assert.Equal(t, uint8(1), res) + + os.Setenv(testVarKey, "2") + defer os.Unsetenv(testVarKey) + res = util.GetEnvAsUint8(testVarKey, 1) + assert.Equal(t, uint8(2), res) + + os.Setenv(testVarKey, "3x") + res = util.GetEnvAsUint8(testVarKey, 1) + assert.Equal(t, uint8(1), res) +} + +func TestGetEnvAsBool(t *testing.T) { + testVarKey := "TEST_ONLY_FOR_UNIT_TEST_BOOL" + res := util.GetEnvAsBool(testVarKey, true) + assert.Equal(t, true, res) + + os.Setenv(testVarKey, "f") + defer os.Unsetenv(testVarKey) + res = util.GetEnvAsBool(testVarKey, true) + assert.Equal(t, false, res) + + os.Setenv(testVarKey, "0") + res = util.GetEnvAsBool(testVarKey, true) + assert.Equal(t, false, res) + + os.Setenv(testVarKey, "false") + res = util.GetEnvAsBool(testVarKey, true) + assert.Equal(t, false, res) + + os.Setenv(testVarKey, "3x") + res = util.GetEnvAsBool(testVarKey, true) + assert.Equal(t, true, res) +} + +func TestGetEnvAsURL(t *testing.T) { + testVarKey := "TEST_ONLY_FOR_UNIT_TEST_URL" + testURL, err := url.Parse("https://allaboutapps.at/") + require.NoError(t, err) + + panicFunc := func() { + _ = util.GetEnvAsURL(testVarKey, "%") + } + assert.Panics(t, panicFunc) + + res := util.GetEnvAsURL(testVarKey, "https://allaboutapps.at/") + assert.Equal(t, *testURL, *res) + + os.Setenv(testVarKey, "https://allaboutapps.at/") + defer os.Unsetenv(testVarKey) + res = util.GetEnvAsURL(testVarKey, "foo") + assert.Equal(t, *testURL, *res) + + os.Setenv(testVarKey, "%") + panicFunc = func() { + _ = util.GetEnvAsURL(testVarKey, "https://allaboutapps.at/") + } + assert.Panics(t, panicFunc) +} + +func TestGetEnvAsStringArr(t *testing.T) { + testVarKey := "TEST_ONLY_FOR_UNIT_TEST_STRING_ARR" + testVal := []string{"a", "b", "c"} + res := util.GetEnvAsStringArr(testVarKey, testVal) + assert.Equal(t, testVal, res) + + os.Setenv(testVarKey, "1,2") + defer os.Unsetenv(testVarKey) + res = util.GetEnvAsStringArr(testVarKey, testVal) + assert.Equal(t, []string{"1", "2"}, res) + + os.Setenv(testVarKey, "") + res = util.GetEnvAsStringArr(testVarKey, testVal) + assert.Equal(t, testVal, res) + + os.Setenv(testVarKey, "a, b, c") + res = util.GetEnvAsStringArr(testVarKey, testVal) + assert.Equal(t, []string{"a", " b", " c"}, res) + + os.Setenv(testVarKey, "a|b|c") + res = util.GetEnvAsStringArr(testVarKey, testVal, "|") + assert.Equal(t, []string{"a", "b", "c"}, res) + + os.Setenv(testVarKey, "a,b,c") + res = util.GetEnvAsStringArr(testVarKey, testVal, "|") + assert.Equal(t, []string{"a,b,c"}, res) + + os.Setenv(testVarKey, "a||b||c") + res = util.GetEnvAsStringArr(testVarKey, testVal, "||") + assert.Equal(t, []string{"a", "b", "c"}, res) +} + +func TestGetEnvAsStringArrTrimmed(t *testing.T) { + testVarKey := "TEST_ONLY_FOR_UNIT_TEST_STRING_ARR_TRIMMED" + testVal := []string{"a", "b", "c"} + + os.Setenv(testVarKey, "a, b, c") + defer os.Unsetenv(testVarKey) + res := util.GetEnvAsStringArrTrimmed(testVarKey, testVal) + assert.Equal(t, []string{"a", "b", "c"}, res) + + os.Setenv(testVarKey, "a, b,c ") + res = util.GetEnvAsStringArrTrimmed(testVarKey, testVal) + assert.Equal(t, []string{"a", "b", "c"}, res) + + os.Setenv(testVarKey, " a || b || c ") + res = util.GetEnvAsStringArrTrimmed(testVarKey, testVal, "||") + assert.Equal(t, []string{"a", "b", "c"}, res) +} + +func TestGetMgmtSecret(t *testing.T) { + rs, err := util.GenerateRandomHexString(8) + require.NoError(t, err) + + key := fmt.Sprintf("WE_WILL_NEVER_USE_THIS_MGMT_SECRET_%s", rs) + expectedVal := fmt.Sprintf("SUPER_SECRET_%s", rs) + + err = os.Setenv(key, expectedVal) + require.NoError(t, err) + + for i := 0; i < 5; i++ { + val := util.GetMgmtSecret(key) + assert.Equal(t, expectedVal, val) + } +} + +func TestGetMgmtSecretRandom(t *testing.T) { + expectedVal := util.GetMgmtSecret("DOES_NOT_EXIST_MGMT_SECRET") + require.NotEmpty(t, expectedVal) + + for i := 0; i < 5; i++ { + val := util.GetMgmtSecret("DOES_NOT_EXIST_MGMT_SECRET") + assert.Equal(t, expectedVal, val) + } +} diff --git a/internal/util/fs.go b/internal/util/fs.go new file mode 100644 index 00000000..705e7924 --- /dev/null +++ b/internal/util/fs.go @@ -0,0 +1,35 @@ +package util + +import ( + "os" + "time" +) + +// TouchFile creates an empty file if the file doesn’t already exist. +// If the file already exists then TouchFile updates the modified time of the file. +// Returns the modification time of the created / updated file. +func TouchFile(absolutePathToFile string) (time.Time, error) { + _, err := os.Stat(absolutePathToFile) + + if os.IsNotExist(err) { + file, err := os.Create(absolutePathToFile) + + if err != nil { + return time.Time{}, err + } + + defer file.Close() + + stat, err := file.Stat() + + if err != nil { + return time.Time{}, err + } + + return stat.ModTime(), nil + } + + currentTime := time.Now().Local() + err = os.Chtimes(absolutePathToFile, currentTime, currentTime) + return currentTime, err +} diff --git a/internal/util/fs_test.go b/internal/util/fs_test.go new file mode 100644 index 00000000..b5595caf --- /dev/null +++ b/internal/util/fs_test.go @@ -0,0 +1,29 @@ +package util_test + +import ( + "os" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTouchfile(t *testing.T) { + err := os.Remove("/tmp/.touchfile-test") + + if err != nil { + require.Equalf(t, true, os.IsNotExist(err), "Only permitting os.IsNotExist(err) as file may not preexistant on test start, but is: %v", err) + } + + ts1, err := util.TouchFile("/tmp/.touchfile-test") + assert.NoError(t, err) + + ts2, err := util.TouchFile("/tmp/.touchfile-test") + assert.NoError(t, err) + require.NotEqual(t, ts1.UnixNano(), ts2.UnixNano()) + + zeroTime, err := util.TouchFile("/this/path/does/not/exist/.touchfile-test") + assert.Error(t, err) + assert.True(t, zeroTime.IsZero(), "time.Time on error should be zero time") +} diff --git a/internal/util/get_project_root_dir.go b/internal/util/get_project_root_dir.go new file mode 100644 index 00000000..6a96f090 --- /dev/null +++ b/internal/util/get_project_root_dir.go @@ -0,0 +1,35 @@ +//go:build !scripts + +package util + +import ( + "os" + "path/filepath" + "sync" + + "github.com/rs/zerolog/log" +) + +var ( + projectRootDir string + dirOnce sync.Once +) + +// GetProjectRootDir returns the path as string to the project_root for a **running application**. +// Note: This function should not be used for generation targets (go generate, make go-generate). +// Thus it's explicitly excluded from the build tag scripts, see instead: +// * /scripts/get_project_root_dir.go +// * ./get_project_root_dir_scripts.go (delegates to above) +// https://stackoverflow.com/questions/43215655/building-multiple-binaries-using-different-packages-and-build-tags +func GetProjectRootDir() string { + dirOnce.Do(func() { + ex, err := os.Executable() + if err != nil { + log.Panic().Err(err).Msg("Failed to get executable path while retrieving project root directory") + } + + projectRootDir = GetEnv("PROJECT_ROOT_DIR", filepath.Dir(ex)) + }) + + return projectRootDir +} diff --git a/internal/util/get_project_root_dir_scripts.go b/internal/util/get_project_root_dir_scripts.go new file mode 100644 index 00000000..db1a408b --- /dev/null +++ b/internal/util/get_project_root_dir_scripts.go @@ -0,0 +1,20 @@ +//go:build scripts + +package util + +import "os" + +// Note that VSCode/gopls currently spawns a "No packages found for open file: [...]" here. +// This is expected and will go away with gopls v1.0, see https://github.com/golang/go/issues/29202 + +// GetProjectRootDir returns the path as string to the project_root while **scripts generation**. +// Note: This function replaces the original util.GetProjectRootDir when go runs with the "script" build tag. +// https://stackoverflow.com/questions/43215655/building-multiple-binaries-using-different-packages-and-build-tags +// Should be in sync with "scripts/internal/util/get_project_root_dir.go" +func GetProjectRootDir() string { + if val, ok := os.LookupEnv("PROJECT_ROOT_DIR"); ok { + return val + } + + return "/app" +} diff --git a/internal/util/hashing/argon2.go b/internal/util/hashing/argon2.go index 77c74bb5..1429335e 100644 --- a/internal/util/hashing/argon2.go +++ b/internal/util/hashing/argon2.go @@ -13,11 +13,14 @@ import ( // Inspired by: https://github.com/alexedwards/argon2id @ 2020-04-22T14:13:23ZZ const ( + // Argon2HashID represents the hash ID set in the (pseudo) modular crypt format used to store the hashed password and params in a single string. Argon2HashID = "argon2id" ) var ( - ErrInvalidArgon2Hash = errors.New("invalid argon2id hash") + // ErrInvalidArgon2Hash indicates the argon2id hash was malformed and could not be decoded. + ErrInvalidArgon2Hash = errors.New("invalid argon2id hash") + // ErrIncompatibleArgon2Version indicates the argon2id hash provided was generated with a different, incompatible argon2 version. ErrIncompatibleArgon2Version = errors.New("incompatible argon2 version") ) diff --git a/internal/util/hashing/argon2_test.go b/internal/util/hashing/argon2_test.go index 59eb81e7..a735f6af 100644 --- a/internal/util/hashing/argon2_test.go +++ b/internal/util/hashing/argon2_test.go @@ -1,45 +1,34 @@ -package hashing +package hashing_test import ( "regexp" - "strings" "testing" + + "allaboutapps.dev/aw/go-starter/internal/util/hashing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestHashPassword(t *testing.T) { - t.Parallel() - hashRegex, err := regexp.Compile(`^\$argon2id\$v=19\$m=65536,t=1,p=4\$[A-Za-z0-9+/]{22}\$[A-Za-z0-9+/]{43}$`) - if err != nil { - t.Fatalf("failed to compile hash regex: %v", err) - } + require.NoError(t, err, "failed to compile hash regex") - hash1, err := HashPassword("t3stp4ssw0rd", DefaultArgon2Params) - if err != nil { - t.Fatalf("failed to hash password: %v", err) - } + hash1, err := hashing.HashPassword("t3stp4ssw0rd", hashing.DefaultArgon2Params) + require.NoError(t, err, "failed to hash password") - if !hashRegex.MatchString(hash1) { - t.Errorf("hash %q is not formatted properly", hash1) - } + assert.Truef(t, hashRegex.MatchString(hash1), "hash %q is not formatted properly", hash1) - hash2, err := HashPassword("t3stp4ssw0rd", DefaultArgon2Params) - if err != nil { - t.Fatalf("failed to hash password: %v", err) - } + hash2, err := hashing.HashPassword("t3stp4ssw0rd", hashing.DefaultArgon2Params) + require.NoError(t, err, "failed to hash password") - if !hashRegex.MatchString(hash2) { - t.Errorf("hash %q is not formatted properly", hash2) - } + assert.Truef(t, hashRegex.MatchString(hash2), "hash %q is not formatted properly", hash2) - if strings.Compare(hash1, hash2) == 0 { - t.Errorf("hashes %q and %q are not unique", hash1, hash2) - } + assert.NotEqualf(t, hash1, hash2, "hashes %q and %q are not unique", hash1, hash2) } func BenchmarkHashPassword(b *testing.B) { for i := 0; i < b.N; i++ { - _, err := HashPassword("t3stp4ssw0rd", DefaultArgon2Params) + _, err := hashing.HashPassword("t3stp4ssw0rd", hashing.DefaultArgon2Params) if err != nil { b.Errorf("failed to hash password #%d: %v", i, err) } @@ -47,34 +36,22 @@ func BenchmarkHashPassword(b *testing.B) { } func TestComparePasswordAndHash(t *testing.T) { - t.Parallel() - hash := "$argon2id$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk" - match, err := ComparePasswordAndHash("t3stp4ssw0rd", hash) - if err != nil { - t.Fatalf("failed to compare password and hash: %v", err) - } - - if !match { - t.Error("correct password and hash do not match") - } + match, err := hashing.ComparePasswordAndHash("t3stp4ssw0rd", hash) + require.NoError(t, err) + assert.True(t, match) - match, err = ComparePasswordAndHash("wr0ngt3stp4ssw0rd", hash) - if err != nil { - t.Fatalf("failed to compare password and hash: %v", err) - } - - if match { - t.Error("wrong password and hash match") - } + match, err = hashing.ComparePasswordAndHash("wr0ngt3stp4ssw0rd", hash) + require.NoError(t, err) + assert.False(t, match) } func BenchmarkComparePasswordAndHash(b *testing.B) { hash := "$argon2id$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk" for i := 0; i < b.N; i++ { - _, err := ComparePasswordAndHash("t3stp4ssw0rd", hash) + _, err := hashing.ComparePasswordAndHash("t3stp4ssw0rd", hash) if err != nil { b.Errorf("failed to compare password and hash #%d: %v", i, err) } @@ -85,7 +62,7 @@ func BenchmarkCompareWrongPasswordAndHash(b *testing.B) { hash := "$argon2id$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk" for i := 0; i < b.N; i++ { - _, err := ComparePasswordAndHash("wr0ngt3stp4ssw0rd", hash) + _, err := hashing.ComparePasswordAndHash("wr0ngt3stp4ssw0rd", hash) if err != nil { b.Errorf("failed to compare wrong password and hash #%d: %v", i, err) } @@ -93,50 +70,39 @@ func BenchmarkCompareWrongPasswordAndHash(b *testing.B) { } func TestComparePasswordAndInvalidHash(t *testing.T) { - t.Parallel() - - _, err := ComparePasswordAndHash("t3stp4ssw0rd", "$argon2i$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") - if err != ErrInvalidArgon2Hash { - t.Errorf("invalid error returned, got %v, want %v", err, ErrInvalidArgon2Hash) - } + _, err := hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2i$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") + require.Error(t, err) + assert.Equal(t, hashing.ErrInvalidArgon2Hash, err) - _, err = ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw") - if err != ErrInvalidArgon2Hash { - t.Errorf("invalid error returned, got %v, want %v", err, ErrInvalidArgon2Hash) - } + _, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw") + require.Error(t, err) + assert.Equal(t, hashing.ErrInvalidArgon2Hash, err) - _, err = ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=20$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") - if err != ErrIncompatibleArgon2Version { - t.Errorf("invalid error returned, got %v, want %v", err, ErrIncompatibleArgon2Version) - } + _, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=20$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") + require.Error(t, err) + assert.Equal(t, hashing.ErrIncompatibleArgon2Version, err) - _, err = ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=a$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") - if err != ErrIncompatibleArgon2Version { - t.Errorf("invalid error returned, got %v, want %v", err, ErrIncompatibleArgon2Version) - } + _, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=a$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") + require.Error(t, err) + assert.Equal(t, hashing.ErrIncompatibleArgon2Version, err) - _, err = ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=a,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") - if err != ErrInvalidArgon2Hash { - t.Errorf("invalid error returned, got %v, want %v", err, ErrInvalidArgon2Hash) - } + _, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=a,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") + require.Error(t, err) + assert.Equal(t, hashing.ErrInvalidArgon2Hash, err) - _, err = ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=a,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") - if err != ErrInvalidArgon2Hash { - t.Errorf("invalid error returned, got %v, want %v", err, ErrInvalidArgon2Hash) - } + _, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=a,p=4$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") + require.Error(t, err) + assert.Equal(t, hashing.ErrInvalidArgon2Hash, err) - _, err = ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=a$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") - if err != ErrInvalidArgon2Hash { - t.Errorf("invalid error returned, got %v, want %v", err, ErrInvalidArgon2Hash) - } + _, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=a$c8FqPHMT83tyxE2v0xDAFw$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") + require.Error(t, err) + assert.Equal(t, hashing.ErrInvalidArgon2Hash, err) - _, err = ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=4$ä$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") - if err != ErrInvalidArgon2Hash { - t.Errorf("invalid error returned, got %v, want %v", err, ErrInvalidArgon2Hash) - } + _, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=4$ä$s2qmbRoRRbfyLIVFUzRwzE7F8PLjchpLKaV7Wf7tHgk") + require.Error(t, err) + assert.Equal(t, hashing.ErrInvalidArgon2Hash, err) - _, err = ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$ä") - if err != ErrInvalidArgon2Hash { - t.Errorf("invalid error returned, got %v, want %v", err, ErrInvalidArgon2Hash) - } + _, err = hashing.ComparePasswordAndHash("t3stp4ssw0rd", "$argon2id$v=19$m=65536,t=1,p=4$c8FqPHMT83tyxE2v0xDAFw$ä") + require.Error(t, err) + assert.Equal(t, hashing.ErrInvalidArgon2Hash, err) } diff --git a/internal/util/http.go b/internal/util/http.go index fb9e55b9..90365b0d 100644 --- a/internal/util/http.go +++ b/internal/util/http.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "io" - "io/ioutil" "mime/multipart" "net/http" @@ -19,9 +18,91 @@ import ( "github.com/labstack/echo/v4" ) -// BindAndValidate binds the request, parsing its body (depending on the `Content-Type` request header) and performs payload -// validation as enforced by the Swagger schema associated with the provided type. In addition to binding the body, BindAndValidate -// can also assign query and URL parameters to a struct and perform validations on those. +const ( + HTTPHeaderCacheControl = "Cache-Control" +) + +// BindAndValidateBody binds the request, parsing **only** its body (depending on the `Content-Type` request header) and performs validation +// as enforced by the Swagger schema associated with the provided type. +// +// Note: In contrast to BindAndValidate, this method does not restore the body after binding (it's considered consumed). +// Thus use BindAndValidateBody only once per request! +// +// Returns an error that can directly be returned from an echo handler and sent to the client should binding or validating of any model fail. +func BindAndValidateBody(c echo.Context, v runtime.Validatable) error { + binder := c.Echo().Binder.(*echo.DefaultBinder) + + if err := binder.BindBody(c, v); err != nil { + return err + } + + return validatePayload(c, v) +} + +// BindAndValidatePathAndQueryParams binds the request, parsing **only** its path **and** query params and performs validation +// as enforced by the Swagger schema associated with the provided type. +// +// Returns an error that can directly be returned from an echo handler and sent to the client should binding or validating of any model fail. +func BindAndValidatePathAndQueryParams(c echo.Context, v runtime.Validatable) error { + binder := c.Echo().Binder.(*echo.DefaultBinder) + + if err := binder.BindPathParams(c, v); err != nil { + return err + } + + if err := binder.BindQueryParams(c, v); err != nil { + return err + } + + return validatePayload(c, v) +} + +// BindAndValidatePathParams binds the request, parsing **only** its path params and performs validation +// as enforced by the Swagger schema associated with the provided type. +// +// Returns an error that can directly be returned from an echo handler and sent to the client should binding or validating of any model fail. +func BindAndValidatePathParams(c echo.Context, v runtime.Validatable) error { + binder := c.Echo().Binder.(*echo.DefaultBinder) + + if err := binder.BindPathParams(c, v); err != nil { + return err + } + + return validatePayload(c, v) +} + +// BindAndValidateQueryParams binds the request, parsing **only** its query params and performs validation +// as enforced by the Swagger schema associated with the provided type. +// +// Returns an error that can directly be returned from an echo handler and sent to the client should binding or validating of any model fail. +func BindAndValidateQueryParams(c echo.Context, v runtime.Validatable) error { + binder := c.Echo().Binder.(*echo.DefaultBinder) + + if err := binder.BindQueryParams(c, v); err != nil { + return err + } + + return validatePayload(c, v) +} + +// BindAndValidate binds the request, parsing path+query+body and validating these structs. +// +// Deprecated: Use our dedicated BindAndValidate* mappers instead: +// BindAndValidateBody(c echo.Context, v runtime.Validatable) error // preferred +// BindAndValidatePathAndQueryParams(c echo.Context, v runtime.Validatable) error // preferred +// BindAndValidatePathParams(c echo.Context, v runtime.Validatable) error // rare usecases +// BindAndValidateQueryParams(c echo.Context, v runtime.Validatable) error // rare usecases +// +// BindAndValidate works like Echo =v4.2.0 DefaultBinder now supports binding query, path params and body to their **own** structs natively. +// Thus, you areencouraged to use our new dedicated BindAndValidate* mappers, which are relevant for the structs goswagger +// autogenerates for you. +// +// Original: Parses body (depending on the `Content-Type` request header) and performs payload validation as enforced by +// the Swagger schema associated with the provided type. In addition to binding the body, BindAndValidate also assigns query +// and URL parameters (if any) to a struct and perform validations on those. // // Providing more than one struct allows for binding payload and parameters simultaneously since echo and goswagger expect data // to be structured differently. If you do not require parsing of both body and params, additional structs can be omitted. @@ -30,17 +111,17 @@ import ( func BindAndValidate(c echo.Context, v runtime.Validatable, vs ...runtime.Validatable) error { // TODO error handling for all occurrences of Bind() due to JSON unmarshal type mismatches if len(vs) == 0 { - if err := c.Bind(v); err != nil { + if err := defaultEchoBindAll(c, v); err != nil { return err } return validatePayload(c, v) } - var reqBody []byte = nil + var reqBody []byte var err error if c.Request().Body != nil { - reqBody, err = ioutil.ReadAll(c.Request().Body) + reqBody, err = io.ReadAll(c.Request().Body) if err != nil { return err } @@ -134,10 +215,10 @@ func ParseFileUpload(c echo.Context, formNameFile string, allowedMIMETypes []str func restoreBindAndValidate(c echo.Context, reqBody []byte, v runtime.Validatable) error { if reqBody != nil { - c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) + c.Request().Body = io.NopCloser(bytes.NewBuffer(reqBody)) } - if err := c.Bind(v); err != nil { + if err := defaultEchoBindAll(c, v); err != nil { return err } @@ -174,6 +255,31 @@ func validatePayload(c echo.Context, v runtime.Validatable) error { return nil } +// Bind it all +// Restores echo query binding pre 4.2.0 handling +// Newer echo versions no longer automatically bind query params to tagged :query struct-fields unless its a GET or DELETE request +// Workaround, depends on the internal echo.DefaultBinder methods. +// +// TODO: Eventually move to a customly implemented Binder. +// Hopefully BindPathParams, BindQueryParams and BindBody stay provided in the future. +// +// This upstream security fix does not directly affect us, as our goswagger generated params/query structs +// and body structs are separated from each other and cannot collide/overwrite props. +// https://github.com/labstack/echo/commit/4d626c210d3946814a30d545adf9b8f2296686a7#diff-aade326d3512b5a2ada6faa791ddec468f2a0adedb352339c9e314e74c8949d2 +func defaultEchoBindAll(c echo.Context, v runtime.Validatable) (err error) { + + binder := c.Echo().Binder.(*echo.DefaultBinder) + + if err := binder.BindPathParams(c, v); err != nil { + return err + } + if err = binder.BindQueryParams(c, v); err != nil { + return err + } + + return binder.BindBody(c, v) +} + func formatValidationErrors(ctx context.Context, err *errors.CompositeError) []*types.HTTPValidationErrorDetail { valErrs := make([]*types.HTTPValidationErrorDetail, 0, len(err.Errors)) for _, e := range err.Errors { diff --git a/internal/util/http_test.go b/internal/util/http_test.go new file mode 100644 index 00000000..4b587665 --- /dev/null +++ b/internal/util/http_test.go @@ -0,0 +1,167 @@ +package util_test + +import ( + "bytes" + "io" + "mime/multipart" + "net/http" + "os" + "path/filepath" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/api" + "allaboutapps.dev/aw/go-starter/internal/test" + "allaboutapps.dev/aw/go-starter/internal/types" + "allaboutapps.dev/aw/go-starter/internal/types/auth" + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/strfmt/conv" + "github.com/go-openapi/swag" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBindAndValidateSuccess(t *testing.T) { + e := echo.New() + //nolint:gosec + testToken := "a546daf5-c845-46a7-8fa6-3d94ae7e1424" + testResponse := &types.PostLoginResponse{ + AccessToken: conv.UUID4(strfmt.UUID4("afbcbc30-4794-48bd-93f1-08373a031fe3")), + RefreshToken: conv.UUID4(strfmt.UUID4("1dd1228c-fa9a-4755-b995-30e24dd6247d")), + ExpiresIn: swag.Int64(3600), + TokenType: swag.String("Bearer"), + } + + e.POST("/", func(c echo.Context) error { + testParam1 := auth.NewGetUserInfoRouteParams() + testParam2 := auth.NewPostForgotPasswordRouteParams() + var body types.PostRefreshPayload + + err := util.BindAndValidate(c, &body, &testParam1, &testParam2) + assert.NoError(t, err) + assert.NotEmpty(t, body) + assert.Equal(t, strfmt.UUID4(testToken), *body.RefreshToken) + + return util.ValidateAndReturn(c, 200, testResponse) + }) + testBody := test.GenericPayload{ + "refresh_token": testToken, + } + + s := &api.Server{ + Echo: e, + } + + res := test.PerformRequest(t, s, "POST", "/?test=true", testBody, nil) + + assert.Equal(t, http.StatusOK, res.Result().StatusCode) + + var response types.PostLoginResponse + test.ParseResponseAndValidate(t, res, &response) + + assert.Equal(t, *testResponse, response) +} + +func TestBindAndValidateBadRequest(t *testing.T) { + e := echo.New() + testToken := "foo" + + e.POST("/", func(c echo.Context) error { + var body types.PostRefreshPayload + + err := util.BindAndValidateBody(c, &body) + assert.Error(t, err) + + return nil + }) + testBody := test.GenericPayload{ + "refresh_token": testToken, + } + + s := &api.Server{ + Echo: e, + } + + _ = test.PerformRequest(t, s, "POST", "/?test=true", testBody, nil) +} + +func TestParseFileUplaod(t *testing.T) { + originalDocumentPath := filepath.Join(util.GetProjectRootDir(), "test", "testdata", "example.jpg") + body, contentType := prepareFileUpload(t, originalDocumentPath) + + e := echo.New() + e.POST("/", func(c echo.Context) error { + + fh, file, mime, err := util.ParseFileUpload(c, "file", []string{"image/jpeg"}) + require.NoError(t, err) + assert.True(t, mime.Is("image/jpeg")) + assert.NotEmpty(t, fh) + assert.NotEmpty(t, file) + + return c.NoContent(204) + }) + + s := &api.Server{ + Echo: e, + } + + headers := http.Header{} + headers.Set(echo.HeaderContentType, contentType) + + res := test.PerformRequestWithRawBody(t, s, "POST", "/", body, headers, nil) + + require.Equal(t, http.StatusNoContent, res.Result().StatusCode) +} + +func TestParseFileUplaodUnsupported(t *testing.T) { + originalDocumentPath := filepath.Join(util.GetProjectRootDir(), "test", "testdata", "example.jpg") + body, contentType := prepareFileUpload(t, originalDocumentPath) + + e := echo.New() + e.POST("/", func(c echo.Context) error { + + fh, file, mime, err := util.ParseFileUpload(c, "file", []string{"image/png"}) + assert.Nil(t, fh) + assert.Nil(t, file) + assert.Nil(t, mime) + if err != nil { + return err + } + + return c.NoContent(204) + }) + + s := &api.Server{ + Echo: e, + } + + headers := http.Header{} + headers.Set(echo.HeaderContentType, contentType) + + res := test.PerformRequestWithRawBody(t, s, "POST", "/", body, headers, nil) + + require.Equal(t, http.StatusUnsupportedMediaType, res.Result().StatusCode) +} + +func prepareFileUpload(t *testing.T, filePath string) (*bytes.Buffer, string) { + t.Helper() + + var body bytes.Buffer + writer := multipart.NewWriter(&body) + + src, err := os.Open(filePath) + require.NoError(t, err) + defer src.Close() + + dst, err := writer.CreateFormFile("file", src.Name()) + require.NoError(t, err) + + _, err = io.Copy(dst, src) + require.NoError(t, err) + + err = writer.Close() + require.NoError(t, err) + + return &body, writer.FormDataContentType() +} diff --git a/internal/util/log.go b/internal/util/log.go index eb3dd3dc..c1f4cebb 100644 --- a/internal/util/log.go +++ b/internal/util/log.go @@ -10,8 +10,19 @@ import ( // LogFromContext returns a request-specific zerolog instance using the provided context. // The returned logger will have the request ID as well as some other value predefined. +// If no logger is associated with the context provided, the global zerolog instance +// will be returned instead - this function will _always_ return a valid (enabled) logger. +// Should you ever need to force a disabled logger for a context, use `util.DisableLogger(ctx, true)` +// and pass the context returned to other code/`LogFromContext`. func LogFromContext(ctx context.Context) *zerolog.Logger { - return log.Ctx(ctx) + l := log.Ctx(ctx) + if l.GetLevel() == zerolog.Disabled { + if ShouldDisableLogger(ctx) { + return l + } + l = &log.Logger + } + return l } // LogFromEchoContext returns a request-specific zerolog instance using the echo.Context of the request. diff --git a/internal/util/log_test.go b/internal/util/log_test.go new file mode 100644 index 00000000..8a75f108 --- /dev/null +++ b/internal/util/log_test.go @@ -0,0 +1,20 @@ +package util_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func TestLogLevelFromString(t *testing.T) { + res := util.LogLevelFromString("panic") + assert.Equal(t, zerolog.PanicLevel, res) + + res = util.LogLevelFromString("warn") + assert.Equal(t, zerolog.WarnLevel, res) + + res = util.LogLevelFromString("foo") + assert.Equal(t, zerolog.DebugLevel, res) +} diff --git a/internal/util/map_test.go b/internal/util/map_test.go new file mode 100644 index 00000000..84e6eaf5 --- /dev/null +++ b/internal/util/map_test.go @@ -0,0 +1,41 @@ +package util_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/stretchr/testify/assert" +) + +func TestMergeStringMap(t *testing.T) { + baseMap := map[string]string{ + "A": "a", + "B": "b", + "C": "c", + } + + toMerge := map[string]string{ + "C": "1", + "D": "2", + } + + expected := map[string]string{ + "A": "a", + "B": "b", + "C": "c", + "D": "2", + } + + res := util.MergeStringMap(baseMap, toMerge) + assert.Equal(t, expected, res) + + expected = map[string]string{ + "C": "1", + "D": "2", + "A": "a", + "B": "b", + } + + res = util.MergeStringMap(toMerge, baseMap) + assert.Equal(t, expected, res) +} diff --git a/internal/util/math_test.go b/internal/util/math_test.go new file mode 100644 index 00000000..ccc84ed4 --- /dev/null +++ b/internal/util/math_test.go @@ -0,0 +1,20 @@ +package util_test + +import ( + "math" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/stretchr/testify/assert" +) + +func TestMinAndMapInt(t *testing.T) { + max := math.MaxInt32 + min := math.MinInt32 + assert.Equal(t, max, util.MaxInt(max, min)) + assert.Equal(t, max, util.MaxInt(min, max)) + assert.Equal(t, min, util.MinInt(max, min)) + assert.Equal(t, min, util.MinInt(min, max)) + assert.Equal(t, min, util.MaxInt(min, min)) + assert.Equal(t, max, util.MinInt(max, max)) +} diff --git a/internal/util/slice.go b/internal/util/slice.go new file mode 100644 index 00000000..56e52320 --- /dev/null +++ b/internal/util/slice.go @@ -0,0 +1,49 @@ +package util + +// ContainsString checks whether the given string slice contains the string provided. +func ContainsString(slice []string, s string) bool { + for _, v := range slice { + if v == s { + return true + } + } + + return false +} + +// ContainsAllString checks whether the given string slice contains all strings provided. +func ContainsAllString(slice []string, sub ...string) bool { + contains := make(map[string]bool) + for _, v := range sub { + contains[v] = false + } + + for _, v := range slice { + if _, ok := contains[v]; ok { + contains[v] = true + } + } + + for _, v := range contains { + if !v { + return false + } + } + + return true +} + +// UniqueString takes the string slice provided and returns a new slice with all duplicates removed. +func UniqueString(slice []string) []string { + seen := make(map[string]struct{}) + res := make([]string, 0) + + for _, s := range slice { + if _, ok := seen[s]; !ok { + res = append(res, s) + seen[s] = struct{}{} + } + } + + return res +} diff --git a/internal/util/slice_test.go b/internal/util/slice_test.go new file mode 100644 index 00000000..e8b58d33 --- /dev/null +++ b/internal/util/slice_test.go @@ -0,0 +1,39 @@ +package util_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/stretchr/testify/assert" +) + +func TestContainsString(t *testing.T) { + test := []string{"a", "b", "d"} + assert.True(t, util.ContainsString(test, "a")) + assert.True(t, util.ContainsString(test, "b")) + assert.False(t, util.ContainsString(test, "c")) + assert.True(t, util.ContainsString(test, "d")) +} + +func TestContainsAllString(t *testing.T) { + test := []string{"a", "b", "d"} + assert.True(t, util.ContainsAllString(test, "a")) + assert.True(t, util.ContainsAllString(test, "b")) + assert.False(t, util.ContainsAllString(test, "c")) + assert.True(t, util.ContainsAllString(test, "d")) + assert.True(t, util.ContainsAllString(test, "a", "b")) + assert.True(t, util.ContainsAllString(test, "a", "d")) + assert.True(t, util.ContainsAllString(test, "b", "d")) + assert.False(t, util.ContainsAllString(test, "a", "c")) + assert.False(t, util.ContainsAllString(test, "b", "c")) + assert.False(t, util.ContainsAllString(test, "c", "d")) + assert.True(t, util.ContainsAllString(test, "a", "b", "d")) + assert.False(t, util.ContainsAllString(test, "a", "b", "c")) + assert.False(t, util.ContainsAllString(test, "a", "b", "c", "d")) + assert.True(t, util.ContainsAllString(test)) +} + +func TestUniqueString(t *testing.T) { + test := []string{"a", "b", "d", "d", "a", "d"} + assert.Equal(t, []string{"a", "b", "d"}, util.UniqueString(test)) +} diff --git a/internal/util/string.go b/internal/util/string.go index 2cd48dc5..42e83382 100644 --- a/internal/util/string.go +++ b/internal/util/string.go @@ -4,6 +4,8 @@ import ( "crypto/rand" "encoding/base64" "encoding/hex" + "errors" + "strings" ) // GenerateRandomBytes returns n random bytes securely generated using the system's default CSPRNG. @@ -36,7 +38,7 @@ func GenerateRandomBase64String(n int) (string, error) { return base64.StdEncoding.EncodeToString(b), nil } -// GenerateRandomBase64String returns a string with n random bytes securely generated using the system's +// GenerateRandomHexString returns a string with n random bytes securely generated using the system's // default CSPRNG in hexadecimal encoding. The resulting string might not be of length n as the encoding // for the raw bytes generated may vary. // @@ -50,3 +52,70 @@ func GenerateRandomHexString(n int) (string, error) { return hex.EncodeToString(b), nil } + +type CharRange int + +const ( + CharRangeNumeric CharRange = iota + CharRangeAlphaLowerCase + CharRangeAlphaUpperCase +) + +// GenerateRandomString returns a string with n random bytes securely generated using the system's +// default CSPRNG. The characters within the generated string will either be part of one ore more supplied +// range of characters, or based on characters in the extra string supplied. +// +// An error will be returned if reading from the secure random number generator fails, at which point +// the returned result should be discarded and not used any further. +func GenerateRandomString(n int, ranges []CharRange, extra string) (string, error) { + var str strings.Builder + + if len(ranges) == 0 && len(extra) == 0 { + return "", errors.New("Random string can only be created if set of characters or extra string characters supplied") + } + + validateFn := func(c byte) bool { + // IndexByte(string, byte) is basically Contains(string, string) without casting + if strings.IndexByte(extra, c) >= 0 { + return true + } + + for _, r := range ranges { + switch r { + case CharRangeNumeric: + if c >= '0' && c <= '9' { + return true + } + case CharRangeAlphaLowerCase: + if c >= 'a' && c <= 'z' { + return true + } + case CharRangeAlphaUpperCase: + if c >= 'A' && c <= 'Z' { + return true + } + } + } + + return false + } + + for str.Len() < n { + + buf, err := GenerateRandomBytes(n) + if err != nil { + return "", err + } + + for _, b := range buf { + if validateFn(b) { + str.WriteByte(b) + } + if str.Len() >= n { + break + } + } + } + + return str.String(), nil +} diff --git a/internal/util/string_test.go b/internal/util/string_test.go new file mode 100644 index 00000000..1a3c9ded --- /dev/null +++ b/internal/util/string_test.go @@ -0,0 +1,58 @@ +package util_test + +import ( + "encoding/base64" + "encoding/hex" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateRandom(t *testing.T) { + res, err := util.GenerateRandomBytes(13) + require.NoError(t, err) + assert.Len(t, res, 13) + + randString, err := util.GenerateRandomBase64String(17) + require.NoError(t, err) + res, err = base64.StdEncoding.DecodeString(randString) + require.NoError(t, err) + assert.Len(t, res, 17) + + randString, err = util.GenerateRandomHexString(19) + require.NoError(t, err) + res, err = hex.DecodeString(randString) + require.NoError(t, err) + assert.Len(t, res, 19) + + randString, err = util.GenerateRandomString(19, []util.CharRange{util.CharRangeAlphaLowerCase}, "/%$") + require.NoError(t, err) + assert.Len(t, randString, 19) + for _, r := range randString { + assert.True(t, (r >= 'a' && r <= 'z') || r == '/' || r == '%' || r == '$') + } + + randString, err = util.GenerateRandomString(19, []util.CharRange{util.CharRangeAlphaUpperCase}, "^\"") + require.NoError(t, err) + assert.Len(t, randString, 19) + for _, r := range randString { + assert.True(t, (r >= 'A' && r <= 'Z') || r == '^' || r == '"') + } + + randString, err = util.GenerateRandomString(19, []util.CharRange{util.CharRangeNumeric}, "") + require.NoError(t, err) + assert.Len(t, randString, 19) + for _, r := range randString { + assert.True(t, (r >= '0' && r <= '9')) + } + + _, err = util.GenerateRandomString(1, nil, "") + require.Error(t, err) + + randString, err = util.GenerateRandomString(8, nil, "a") + require.NoError(t, err) + assert.Len(t, randString, 8) + assert.Equal(t, "aaaaaaaa", randString) +} diff --git a/internal/util/test.go b/internal/util/test.go new file mode 100644 index 00000000..90de81a9 --- /dev/null +++ b/internal/util/test.go @@ -0,0 +1,14 @@ +package util + +import ( + "os" + "strings" +) + +// RunningInTest returns true if the current executing is within the go test framework. +// The function first checks the `CI` env variable defined by various CI environments, +// then tests whether the executable ends with the `.test` suffix generated by `go test`. +func RunningInTest() bool { + // Partially taken from: https://stackoverflow.com/a/45913089 @ 2021-06-02T14:55:01+00:00 + return len(os.Getenv("CI")) > 0 || strings.HasSuffix(os.Args[0], ".test") +} diff --git a/internal/util/test_test.go b/internal/util/test_test.go new file mode 100644 index 00000000..4117b12f --- /dev/null +++ b/internal/util/test_test.go @@ -0,0 +1,12 @@ +package util_test + +import ( + "testing" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/stretchr/testify/require" +) + +func TestRunningInTest(t *testing.T) { + require.True(t, util.RunningInTest(), "Should be true while we are running in the test env/context") +} diff --git a/internal/util/time.go b/internal/util/time.go index 1544b054..869d84df 100644 --- a/internal/util/time.go +++ b/internal/util/time.go @@ -6,7 +6,7 @@ const ( DateFormat = "2006-01-02" ) -// Returns an instance of time.Time from a given string asuming RFC3339 format +// TimeFromString returns an instance of time.Time from a given string asuming RFC3339 format func TimeFromString(timeString string) (time.Time, error) { return time.Parse(time.RFC3339, timeString) } @@ -15,9 +15,8 @@ func DateFromString(dateString string) (time.Time, error) { return time.Parse(DateFormat, dateString) } -func EndOfMonth(d time.Time, timeZone *time.Location) time.Time { - - return time.Date(d.Year(), d.Month()+1, 1, 0, 0, 0, -1, timeZone) +func EndOfMonth(d time.Time) time.Time { + return time.Date(d.Year(), d.Month()+1, 1, 0, 0, 0, -1, d.Location()) } func EndOfDay(d time.Time) time.Time { @@ -25,16 +24,16 @@ func EndOfDay(d time.Time) time.Time { } func StartOfMonth(d time.Time) time.Time { - return time.Date(d.Year(), d.Month(), 1, 0, 0, 0, 0, time.UTC) + return time.Date(d.Year(), d.Month(), 1, 0, 0, 0, 0, d.Location()) } func StartOfQuarter(d time.Time) time.Time { quarter := (int(d.Month()) - 1) / 3 m := quarter*3 + 1 - return time.Date(d.Year(), time.Month(m), 1, 0, 0, 0, 0, time.UTC) + return time.Date(d.Year(), time.Month(m), 1, 0, 0, 0, 0, d.Location()) } -// Returns the monday (assuming week starts at monday) of the week of the date +// StartOfWeek returns the monday (assuming week starts at monday) of the week of the date func StartOfWeek(d time.Time) time.Time { dayOffset := int(d.Weekday()) - 1 @@ -42,11 +41,11 @@ func StartOfWeek(d time.Time) time.Time { if dayOffset < 0 { dayOffset = 6 } - return time.Date(d.Year(), d.Month(), d.Day()-dayOffset, 0, 0, 0, 0, time.UTC) + return time.Date(d.Year(), d.Month(), d.Day()-dayOffset, 0, 0, 0, 0, d.Location()) } -func Date(year int, month int, day int) time.Time { - return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC) +func Date(year int, month int, day int, loc *time.Location) time.Time { + return time.Date(year, time.Month(month), day, 0, 0, 0, 0, loc) } func AddWeeks(d time.Time, weeks int) time.Time { @@ -58,9 +57,9 @@ func AddMonths(d time.Time, months int) time.Time { } func DayBefore(d time.Time) time.Time { - return time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, -1, time.UTC) + return time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, -1, d.Location()) } func TruncateTime(d time.Time) time.Time { - return time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, time.UTC) + return time.Date(d.Year(), d.Month(), d.Day(), 0, 0, 0, 0, d.Location()) } diff --git a/internal/util/time_test.go b/internal/util/time_test.go index f2cafa5a..af684a83 100644 --- a/internal/util/time_test.go +++ b/internal/util/time_test.go @@ -10,20 +10,16 @@ import ( ) func TestStartOfMonth(t *testing.T) { - t.Parallel() - - d := util.Date(2020, 3, 12) + d := util.Date(2020, 3, 12, time.UTC) expected := time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC) assert.Equal(t, expected, util.StartOfMonth(d)) - d = util.Date(2020, 12, 35) + d = util.Date(2020, 12, 35, time.UTC) expected = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) assert.Equal(t, expected, util.StartOfMonth(d)) } func TestTimeFromString(t *testing.T) { - t.Parallel() - expected := time.Date(2020, 3, 29, 12, 34, 54, 0, time.UTC) d, err := util.TimeFromString("2020-03-29T12:34:54Z") @@ -33,41 +29,105 @@ func TestTimeFromString(t *testing.T) { } func TestStartOfQuarter(t *testing.T) { - t.Parallel() - - d := util.Date(2020, 3, 31) + d := util.Date(2020, 3, 31, time.UTC) expected := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) assert.Equal(t, expected, util.StartOfQuarter(d)) - d = util.Date(2020, 1, 1) + d = util.Date(2020, 1, 1, time.UTC) expected = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) assert.Equal(t, expected, util.StartOfQuarter(d)) - d = util.Date(2020, 12, 1) + d = util.Date(2020, 12, 1, time.UTC) expected = time.Date(2020, 10, 1, 0, 0, 0, 0, time.UTC) assert.Equal(t, expected, util.StartOfQuarter(d)) - d = util.Date(2020, 12, 35) + d = util.Date(2020, 12, 35, time.UTC) expected = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) assert.Equal(t, expected, util.StartOfQuarter(d)) - d = util.Date(2020, 4, 1) + d = util.Date(2020, 4, 1, time.UTC) expected = time.Date(2020, 4, 1, 0, 0, 0, 0, time.UTC) assert.Equal(t, expected, util.StartOfQuarter(d)) } func TestStartOfWeek(t *testing.T) { - t.Parallel() - - d := util.Date(2020, 3, 12) + d := util.Date(2020, 3, 12, time.UTC) expected := time.Date(2020, 3, 9, 0, 0, 0, 0, time.UTC) assert.Equal(t, expected, util.StartOfWeek(d)) - d = util.Date(2020, 6, 15) + d = util.Date(2020, 6, 15, time.UTC) expected = time.Date(2020, 6, 15, 0, 0, 0, 0, time.UTC) assert.Equal(t, expected, util.StartOfWeek(d)) - d = util.Date(2020, 6, 21) + d = util.Date(2020, 6, 21, time.UTC) expected = time.Date(2020, 6, 15, 0, 0, 0, 0, time.UTC) assert.Equal(t, expected, util.StartOfWeek(d)) } + +func TestDateFromString(t *testing.T) { + res, err := util.DateFromString("2020-01-03") + require.NoError(t, err) + + require.True(t, res.Equal(time.Date(2020, 1, 3, 0, 0, 0, 0, time.UTC))) + + res, err = util.DateFromString("2020-xx-03") + require.Error(t, err) + assert.Empty(t, res) +} + +func TestEndOfMonth(t *testing.T) { + d := util.Date(2020, 3, 12, time.UTC) + expected := time.Date(2020, 3, 31, 23, 59, 59, 999999999, time.UTC) + assert.True(t, expected.Equal(util.EndOfMonth(d))) + + d = util.Date(2020, 12, 35, time.UTC) + expected = time.Date(2021, 1, 31, 23, 59, 59, 999999999, time.UTC) + res := util.EndOfMonth(d) + assert.True(t, expected.Equal(res)) + + expected = time.Date(2021, 1, 31, 0, 0, 0, 0, time.UTC) + assert.Equal(t, expected, util.TruncateTime(res)) + +} + +func TestEndOfDay(t *testing.T) { + d := util.Date(2020, 3, 12, time.UTC) + expected := time.Date(2020, 3, 12, 23, 59, 59, 999999999, time.UTC) + assert.True(t, expected.Equal(util.EndOfDay(d))) + + d = util.Date(2020, 12, 35, time.UTC) + expected = time.Date(2021, 1, 4, 23, 59, 59, 999999999, time.UTC) + res := util.EndOfDay(d) + assert.True(t, expected.Equal(res)) + + expected = time.Date(2021, 1, 4, 0, 0, 0, 0, time.UTC) + assert.Equal(t, expected, util.TruncateTime(res)) +} + +func TestDateAdds(t *testing.T) { + d := util.Date(2020, 3, 12, time.UTC) + expected := time.Date(2022, 4, 12, 0, 0, 0, 0, time.UTC) + res := util.AddMonths(d, 25) + assert.True(t, expected.Equal(res)) + + d = util.Date(2020, 1, 30, time.UTC) + expected = time.Date(2020, 3, 1, 0, 0, 0, 0, time.UTC) + res = util.AddMonths(d, 1) + assert.True(t, expected.Equal(res)) + + d = util.Date(2020, 1, 30, time.UTC) + expected = time.Date(2020, 3, 5, 0, 0, 0, 0, time.UTC) + res = util.AddWeeks(d, 5) + assert.True(t, expected.Equal(res)) +} + +func TestDayBefore(t *testing.T) { + d := util.Date(2020, 3, 1, time.UTC) + expected := time.Date(2020, 2, 29, 23, 59, 59, 999999999, time.UTC) + res := util.DayBefore(d) + assert.True(t, expected.Equal(res)) + + expected = time.Date(2020, 2, 29, 0, 0, 0, 0, time.UTC) + assert.Equal(t, expected, util.TruncateTime(res)) + +} diff --git a/internal/util/wait_group.go b/internal/util/wait_group.go new file mode 100644 index 00000000..31c62498 --- /dev/null +++ b/internal/util/wait_group.go @@ -0,0 +1,29 @@ +package util + +import ( + "errors" + "sync" + "time" +) + +var ( + ErrWaitTimeout = errors.New("WaitGroup has timed out") +) + +// WaitTimeout waits for the waitgroup for the specified max timeout. +// Returns nil on completion or ErrWaitTimeout if waiting timed out. +// See https://stackoverflow.com/questions/32840687/timeout-for-waitgroup-wait +// Note that the spawned goroutine to wg.Wait() gets leaked and will continue running detached +func WaitTimeout(wg *sync.WaitGroup, timeout time.Duration) error { + c := make(chan struct{}) + go func() { + defer close(c) + wg.Wait() + }() + select { + case <-c: + return nil // completed normally + case <-time.After(timeout): + return ErrWaitTimeout // timed out + } +} diff --git a/internal/util/wait_group_test.go b/internal/util/wait_group_test.go new file mode 100644 index 00000000..c42e4cdf --- /dev/null +++ b/internal/util/wait_group_test.go @@ -0,0 +1,47 @@ +package util_test + +import ( + "sync" + "sync/atomic" + "testing" + "time" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/stretchr/testify/assert" +) + +func TestWaitTimeoutErr(t *testing.T) { + var wg sync.WaitGroup + var n int32 + + wg.Add(1) + go func() { + atomic.AddInt32(&n, 1) + time.Sleep(10 * time.Millisecond) + atomic.AddInt32(&n, 1) + wg.Done() + }() + + wg.Add(1) + go func() { + atomic.AddInt32(&n, 1) + wg.Done() + }() + + wg.Add(1) + go func() { + time.Sleep(400 * time.Millisecond) + atomic.AddInt32(&n, 1) // available after wg.Wait() + wg.Done() + }() + + // timeout reached. + err := util.WaitTimeout(&wg, 200*time.Millisecond) + assert.Equal(t, util.ErrWaitTimeout, err) + assert.Equal(t, int32(3), atomic.LoadInt32(&n)) + + // ok (after timeout). + err = util.WaitTimeout(&wg, 800*time.Second) + assert.Equal(t, nil, err) + assert.Equal(t, int32(4), atomic.LoadInt32(&n)) +} diff --git a/migrations/20200428072232-create-users.sql b/migrations/20200428072232-create-users.sql index 35679571..f456e3e3 100644 --- a/migrations/20200428072232-create-users.sql +++ b/migrations/20200428072232-create-users.sql @@ -6,7 +6,6 @@ CREATE TABLE users ( is_active bool NOT NULL, -- TODO: use user_scope enum as "scopes user_scope[]" when supported -- https://github.com/volatiletech/sqlboiler/issues/739 - scopes text[] NOT NULL, last_authenticated_at timestamptz, created_at timestamptz NOT NULL, diff --git a/scripts/README.md b/scripts/README.md index e674d737..3a7e8c93 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -12,4 +12,31 @@ Examples: * https://github.com/cockroachdb/cockroach/tree/master/scripts * https://github.com/hashicorp/terraform/tree/master/scripts -Please note that this scripts are not available in a final product. Use `/cmd` instead if you need to execute your script an a live environment. \ No newline at end of file +Please note that this scripts are not available in a final product. Head to `../cmd` if you need to execute your script in live environments. + +The `gsdev` cli util executes this `scripts/main.go` file here and also describes all available commands available while developing a project locally. `gsdev` is made available during the `Dockerfile`'s development stage. + +### `/scripts/cmd/*.go` + +`func`s may define shared logic used in `/scripts/internal/**/*.go`, the actual usable commands are defined within `/scripts/internal`. + +### `//go:build scripts` + +Any `*.go` file in all subdirectories of `/scripts/**` should specify `//go:build scripts` to signal that those files are not part of of our final product. To execute any script that has this build tag, you need to specify `-tags scripts`, otherwise you will run into an error like the following (also see our `Makefile` for a reference): + +```bash +# Works +go run -tags scripts scripts/main.go +# go-starter development scripts +# Utility commands while developing go-starter based projects. + +# Works (same as above) +gsdev +# go-starter development scripts +# Utility commands while developing go-starter based projects. + +# Misses build tag "scripts" +go run scripts/main.go +# package command-line-arguments +# imports allaboutapps.dev/aw/go-starter/scripts/cmd: build constraints exclude all Go files in /app/scripts/cmd +``` diff --git a/scripts/cmd/handlers.go b/scripts/cmd/handlers.go new file mode 100644 index 00000000..29caa63d --- /dev/null +++ b/scripts/cmd/handlers.go @@ -0,0 +1,28 @@ +//go:build scripts + +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// handlersCmd represents the handlers command +// see handlers_*.go for sub_commands +var handlersCmd = &cobra.Command{ + Use: "handlers ", + Short: "Handlers related subcommands", + Run: func(cmd *cobra.Command, args []string) { + if err := cmd.Help(); err != nil { + fmt.Println(err) + os.Exit(1) + } + os.Exit(0) + }, +} + +func init() { + rootCmd.AddCommand(handlersCmd) +} diff --git a/scripts/cmd/handlers_check.go b/scripts/cmd/handlers_check.go new file mode 100644 index 00000000..987b73e0 --- /dev/null +++ b/scripts/cmd/handlers_check.go @@ -0,0 +1,33 @@ +//go:build scripts + +package cmd + +import ( + "log" + + "allaboutapps.dev/aw/go-starter/scripts/internal/handlers" + "github.com/spf13/cobra" +) + +const ( + printAllFlag = "print-all" +) + +var handlersCheckCmd = &cobra.Command{ + Use: "check", + Short: "Checks handlers vs. swagger spec.", + Long: `Checks currently implemented handlers against swagger spec.`, + Run: func(cmd *cobra.Command, args []string) { + + printAll, err := cmd.Flags().GetBool(printAllFlag) + if err != nil { + log.Fatal(err) + } + handlers.CheckHandlers(printAll) + }, +} + +func init() { + handlersCmd.AddCommand(handlersCheckCmd) + handlersCheckCmd.Flags().Bool(printAllFlag, false, "Print only print the current implemented handlers, do not generate the file.") +} diff --git a/scripts/cmd/handlers_gen.go b/scripts/cmd/handlers_gen.go new file mode 100644 index 00000000..57960cb9 --- /dev/null +++ b/scripts/cmd/handlers_gen.go @@ -0,0 +1,33 @@ +//go:build scripts + +package cmd + +import ( + "log" + + "allaboutapps.dev/aw/go-starter/scripts/internal/handlers" + "github.com/spf13/cobra" +) + +const ( + printOnlyFlag = "print-only" +) + +var handlersGenCmd = &cobra.Command{ + Use: "gen", + Short: "Generate internal/api/handlers/handlers.go.", + Long: `Generates internal/api/handlers/handlers.go file based on the current implemented handlers.`, + Run: func(cmd *cobra.Command, args []string) { + + printOnly, err := cmd.Flags().GetBool(printOnlyFlag) + if err != nil { + log.Fatal(err) + } + handlers.GenHandlers(printOnly) + }, +} + +func init() { + handlersCmd.AddCommand(handlersGenCmd) + handlersGenCmd.Flags().Bool(printOnlyFlag, false, "Print all checked handlers regardless of errors.") +} diff --git a/scripts/cmd/modulename.go b/scripts/cmd/modulename.go new file mode 100644 index 00000000..f3795d7e --- /dev/null +++ b/scripts/cmd/modulename.go @@ -0,0 +1,35 @@ +//go:build scripts + +package cmd + +import ( + "fmt" + "log" + + "allaboutapps.dev/aw/go-starter/scripts/internal/util" + "github.com/spf13/cobra" +) + +// moduleCmd represents the server command +var moduleCmd = &cobra.Command{ + Use: "modulename", + Short: "Prints the modulename", + Long: `Prints the currently applied go modulename of this project.`, + Run: func(cmd *cobra.Command, args []string) { + runModulename() + }, +} + +func init() { + rootCmd.AddCommand(moduleCmd) +} + +func runModulename() { + baseModuleName, err := util.GetModuleName(modulePath) + + if err != nil { + log.Fatal(err) + } + + fmt.Println(baseModuleName) +} diff --git a/scripts/cmd/root.go b/scripts/cmd/root.go new file mode 100644 index 00000000..a7f985d5 --- /dev/null +++ b/scripts/cmd/root.go @@ -0,0 +1,34 @@ +//go:build scripts + +package cmd + +import ( + "fmt" + "os" + "path/filepath" + + "allaboutapps.dev/aw/go-starter/scripts/internal/util" + "github.com/spf13/cobra" +) + +var ( + projectRoot = util.GetProjectRootDir() + modulePath = filepath.Join(util.GetProjectRootDir(), "go.mod") +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "gsdev", + Short: "gsdev", + Long: `go-starter development scripts +Utility commands while developing go-starter based projects.`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/scripts/cmd/scaffold.go b/scripts/cmd/scaffold.go new file mode 100644 index 00000000..a923b303 --- /dev/null +++ b/scripts/cmd/scaffold.go @@ -0,0 +1,85 @@ +//go:build scripts + +package cmd + +import ( + "log" + "os/exec" + "path/filepath" + + "allaboutapps.dev/aw/go-starter/scripts/internal/scaffold" + "github.com/spf13/cobra" +) + +const ( + methodsFlag = "methods" + forceFlag = "force" +) + +var ( + modelPath = filepath.Join(projectRoot, "internal/models") + swaggerPath = filepath.Join(projectRoot, "api") + handlerPath = filepath.Join(projectRoot, "internal/api/handlers") + + makeSwaggerCmd = exec.Command("make", "swagger") + makeGoGenerateCmd = exec.Command("make", "go-generate") + + defaultMethods = []string{"get-all", "get", "post", "put", "delete"} +) + +// rootCmd represents the base command when called without any subcommands +var scaffoldCmd = &cobra.Command{ + Use: "scaffold [resource name]", + Short: "Scaffolding tool for CRUD endpoints.", + Long: "Scaffolding tool to generate CRUD endpoint stubs from sqlboiler model definitions.", + Run: generate, +} + +func init() { + rootCmd.AddCommand(scaffoldCmd) + scaffoldCmd.Flags().StringSliceP(methodsFlag, "m", defaultMethods, "Specify HTTP methods to generate handlers for. Example: scaffold --methods get-all,get,delete") + scaffoldCmd.Flags().BoolP(forceFlag, "f", false, "Forces the tool to overwrite existing files.") +} + +func generate(cmd *cobra.Command, args []string) { + if len(args) < 1 { + log.Fatal("Please provide a valid resource name") + } + + resourceName := args[0] + if resourceName == "" { + log.Fatal("Please provide a valid resource name") + } + + methods, err := cmd.Flags().GetStringSlice(methodsFlag) + if err != nil { + log.Fatalf("Failed to read %s: %v", methodsFlag, err) + } + + force, err := cmd.Flags().GetBool(forceFlag) + if err != nil { + log.Fatalf("Failed to read %s: %v", forceFlag, err) + } + + resourcePath := filepath.Join(modelPath, resourceName+".go") + resource, err := scaffold.ParseModel(resourcePath) + if err != nil { + log.Fatalf("Failed to parse resource from model file: %v", err) + } + + if err = scaffold.GenerateSwagger(resource, swaggerPath, force); err != nil { + log.Fatalf("Failed to generate Swagger spec: %v", err) + } + + if err = scaffold.GenerateHandlers(resource, handlerPath, modulePath, methods, force); err != nil { + log.Fatalf("Failed to generate handlers: %v", err) + } + + if err = makeSwaggerCmd.Run(); err != nil { + log.Fatalf("Failed to run 'make swagger': %v", err) + } + + if err = makeGoGenerateCmd.Run(); err != nil { + log.Fatalf("Failed to run 'make go-generate': %v", err) + } +} diff --git a/scripts/handlers/check_handlers.go b/scripts/internal/handlers/check.go similarity index 92% rename from scripts/handlers/check_handlers.go rename to scripts/internal/handlers/check.go index d35d563d..a38b693e 100644 --- a/scripts/handlers/check_handlers.go +++ b/scripts/internal/handlers/check.go @@ -1,16 +1,15 @@ -// +build ignore +//go:build scripts // This program checks /internal/api/handlers.go and /internal/types/spec_handlers.go -// It can be invoked by running go run scripts/handlers/check_handlers.go +// It can be invoked by running go run -tags scripts scripts/handlers/check_handlers.go // Supported args: // --print-all -package main +package handlers import ( "context" - "flag" "fmt" "strings" "sync" @@ -22,11 +21,7 @@ import ( "github.com/rs/zerolog" ) -func main() { - - printAll := flag.Bool("print-all", false, "If specified, will print all handlers.") - flag.Parse() - +func CheckHandlers(printAll bool) { // https://golangbyexample.com/print-output-text-color-console/ // https://gist.github.com/ik5/d8ecde700972d4378d87 warningLine := "\033[1;33m%s\033[0m\n" @@ -69,7 +64,7 @@ func main() { if !ok { fmt.Printf(warningLine, tt.Method+" "+tt.Path+"\n WARNING: Missing swagger spec in api/swagger.yml!") } else { - if *printAll == true { + if printAll { fmt.Println(tt.Method + " " + tt.Path) } } @@ -124,5 +119,4 @@ func main() { } wg.Wait() - } diff --git a/scripts/handlers/gen_handlers.go b/scripts/internal/handlers/gen.go similarity index 63% rename from scripts/handlers/gen_handlers.go rename to scripts/internal/handlers/gen.go index ba63ff81..03bfeb7d 100644 --- a/scripts/handlers/gen_handlers.go +++ b/scripts/internal/handlers/gen.go @@ -1,15 +1,14 @@ -// +build ignore +//go:build scripts // This program generates /internal/api/handlers.go. -// It can be invoked by running go run scripts/handlers/gen_handlers.go +// It can be invoked by running go run -tags scripts scripts/handlers/gen_handlers.go // Supported args: // -print-only -package main +package handlers import ( - "flag" "fmt" "go/ast" "go/parser" @@ -21,27 +20,28 @@ import ( "strings" "text/template" - "allaboutapps.dev/aw/go-starter/internal/util" + "allaboutapps.dev/aw/go-starter/scripts/internal/util" ) // https://blog.carlmjohnson.net/post/2016-11-27-how-to-use-go-generate/ var ( - HANDLERS_PACKAGE = "/internal/api/handlers" + handlersPackage = "/internal/api/handlers" - PATH_PROJECT_ROOT = util.GetProjectRootDir() - PATH_HANDLERS_ROOT = PATH_PROJECT_ROOT + HANDLERS_PACKAGE - PATH_MOD_FILE = PATH_PROJECT_ROOT + "/go.mod" - PATH_HANDLERS_FILE = PATH_HANDLERS_ROOT + "/handlers.go" + pathProjectRoot = util.GetProjectRootDir() + pathHandlersRoot = pathProjectRoot + handlersPackage + pathModFile = pathProjectRoot + "/go.mod" + pathHandlersFile = pathHandlersRoot + "/handlers.go" - // * route must look like this - METHOD_PREFIXES = []string{ + // methodPrefixes defines all keywords that we search for in the handlers sub packages + // * must be the naming of the route (Capitalized) + methodPrefixes = []string{ // https://swagger.io/specification/v2/ fixed fields: GET, PUT, POST, DELETE, OPTIONS, HEAD, PATCH "Get", "Put", "Post", "Delete", "Options", "Head", "Patch", } - METHOD_SUFFIX = "Route" + methodSuffix = "Route" - PACKAGE_TEMPLATE = template.Must(template.New("").Parse(`// Code generated by go run scripts/handlers/gen_handlers.go; DO NOT EDIT. + packageTemplate = template.Must(template.New("").Parse(`// Code generated by go run -tags scripts scripts/handlers/gen_handlers.go; DO NOT EDIT. package handlers import ( @@ -75,15 +75,11 @@ type TempateData struct { } // get all functions in above handler packages -// that match * -func main() { - - printOnly := flag.Bool("print-only", false, "If specified, will only print our handlers without doing anything.") - flag.Parse() - +// that match * +func GenHandlers(printOnly bool) { funcs := []ResolvedFunction{} - baseModuleName, err := util.GetModuleName(PATH_MOD_FILE) + baseModuleName, err := util.GetModuleName(pathModFile) if err != nil { log.Fatal(err) @@ -91,13 +87,17 @@ func main() { set := token.NewFileSet() - err = filepath.Walk(PATH_HANDLERS_ROOT, func(path string, f os.FileInfo, err error) error { + err = filepath.Walk(pathHandlersRoot, func(path string, f os.FileInfo, err error) error { + if err != nil { + fmt.Printf("Failed to access path %q: %v\n", path, err) + os.Exit(1) + } // ignore handler file to be generated // ignore directories // ignore non go files // ignore test go files - if path == PATH_HANDLERS_FILE || f.IsDir() || strings.HasSuffix(path, ".go") == false || strings.HasSuffix(path, "test.go") { + if path == pathHandlersFile || f.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "test.go") { return nil } @@ -109,7 +109,7 @@ func main() { } fileDir := filepath.Dir(path) - packageNameFQDN := strings.Replace(fileDir, PATH_PROJECT_ROOT, baseModuleName, 1) + packageNameFQDN := strings.Replace(fileDir, pathProjectRoot, baseModuleName, 1) for _, d := range gofile.Decls { @@ -117,8 +117,8 @@ func main() { fnName := fn.Name.String() - for _, prefix := range METHOD_PREFIXES { - if strings.HasPrefix(fnName, prefix) && strings.HasSuffix(fnName, METHOD_SUFFIX) { + for _, prefix := range methodPrefixes { + if strings.HasPrefix(fnName, prefix) && strings.HasSuffix(fnName, methodSuffix) { funcs = append(funcs, ResolvedFunction{ FunctionName: fnName, PackageName: gofile.Name.Name, @@ -156,12 +156,12 @@ func main() { } } - if mustAppend == true { + if mustAppend { subPkgs = append(subPkgs, fun.PackageNameFQDN) } } - if *printOnly == true { + if printOnly { for _, function := range funcs { fmt.Println(function.PackageNameFQDN, function.FunctionName) } @@ -170,7 +170,7 @@ func main() { return } - f, err := os.Create(PATH_HANDLERS_FILE) + f, err := os.Create(pathHandlersFile) if err != nil { log.Fatal(err) @@ -178,9 +178,10 @@ func main() { defer f.Close() - PACKAGE_TEMPLATE.Execute(f, TempateData{ + if err = packageTemplate.Execute(f, TempateData{ SubPkgs: subPkgs, Funcs: funcs, - }) - + }); err != nil { + log.Fatal(err) + } } diff --git a/scripts/internal/scaffold/scaffold.go b/scripts/internal/scaffold/scaffold.go new file mode 100644 index 00000000..4d0a408b --- /dev/null +++ b/scripts/internal/scaffold/scaffold.go @@ -0,0 +1,358 @@ +//go:build scripts + +package scaffold + +import ( + "fmt" + "go/ast" + "go/parser" + "os" + "path/filepath" + "regexp" + "strings" + "text/template" + + "allaboutapps.dev/aw/go-starter/internal/util" + "github.com/go-openapi/swag" + "github.com/rogpeppe/go-internal/modfile" +) + +// Scaffolding tool to auto-generate basic CRUD handlers for a given database model. + +type FieldType struct { + Name string +} + +type Field struct { + Name string + Type FieldType +} + +type StorageResource struct { + Name string + Fields []Field +} + +func ParseModel(path string) (*StorageResource, error) { + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + regex, err := regexp.Compile(`type ([^\s]+) (struct {[^}]+})`) + if err != nil { + return nil, err + } + + matches := regex.FindStringSubmatch(string(content)) + resourceName := matches[1] + expression := matches[2] + + node, err := parser.ParseExpr(expression) + if err != nil { + return nil, err + } + + structType, ok := node.(*ast.StructType) + if !ok { + return nil, fmt.Errorf("%s is not a struct definition", expression) + } + + fields := []Field{} + for _, field := range structType.Fields.List { + name := field.Names[0].Name + fieldType := expression[field.Type.Pos()-1 : field.Type.End()-1] + field := Field{ + Name: name, + Type: FieldType{ + Name: fieldType, + }, + } + + if field.Name == "R" || field.Name == "L" { + break + } + + fields = append(fields, field) + } + + resource := StorageResource{ + Name: resourceName, + Fields: fields, + } + + return &resource, nil +} + +type Property struct { + Name string + Type string + Required bool + Format *string +} + +type SwaggerResource struct { + Name string + URLName string + Properties []Property + PayloadProperties []Property +} + +func GenerateSwagger(resource *StorageResource, outputPath string, force bool) error { + definitionsPath := filepath.Join(outputPath, "definitions") + pathsPath := filepath.Join(outputPath, "paths") + if err := createDirIfAbsent(definitionsPath); err != nil { + return err + } + if err := createDirIfAbsent(pathsPath); err != nil { + return err + } + + swaggerResource := toSwaggerResource(resource) + definitionsSpecPath := filepath.Join(definitionsPath, swaggerResource.URLName+".yml") + pathsSpecPath := filepath.Join(pathsPath, swaggerResource.URLName+".yml") + + if err := executeTemplate(swaggerDefinitionsTemplate, definitionsSpecPath, swaggerResource, force); err != nil { + return err + } + + return executeTemplate(swaggerPathsTemplate, pathsSpecPath, swaggerResource, force) +} + +var payloadExcluded = []string{"ID", "CreatedAt", "UpdatedAt"} + +func toSwaggerResource(resource *StorageResource) *SwaggerResource { + properties := make([]Property, 0, len(resource.Fields)) + payloadProperties := make([]Property, 0, len(resource.Fields)) + for _, field := range resource.Fields { + property := fieldToProperty(field) + properties = append(properties, property) + + if !util.ContainsString(payloadExcluded, field.Name) { + payloadProperties = append(payloadProperties, property) + } + } + + swaggerResource := SwaggerResource{ + Name: resource.Name, + URLName: strings.ToLower(resource.Name), // TODO: Use dash separator + Properties: properties, + PayloadProperties: payloadProperties, + } + + return &swaggerResource +} + +func fieldToProperty(field Field) Property { + propertyType := "string" // Fallback type + required := true + var format *string + + switch field.Type.Name { + case "int": + propertyType = "integer" + case "null.Int": + propertyType = "integer" + required = false + case "bool": + propertyType = "boolean" + case "null.Bool": + propertyType = "boolean" + required = false + case "null.String": + propertyType = "string" + required = false + case "types.Decimal": + propertyType = "number" + case "types.NullDecimal": + propertyType = "number" + required = false + case "time.Time": + format = swag.String("date-time") + case "null.Time": + format = swag.String("date-time") + required = false + } + + if strings.Contains(field.Name, "ID") { + format = swag.String("uuid4") + } + + return Property{ + Name: goToJSNaming(field.Name), + Type: propertyType, + Required: required, + Format: format, + } +} + +type HandlerField struct { + Name string + Value string + PlaceholderValue string +} + +type HandlerResource struct { + Name string + Fields []HandlerField +} + +type Handler struct { + Module string + Package string + Resource *HandlerResource +} + +func toHandlerResource(storageResource *StorageResource, swaggerResource *SwaggerResource) *HandlerResource { + fields := make([]HandlerField, len(swaggerResource.Properties)) + for i, property := range swaggerResource.Properties { + fields[i] = propertyToHandlerField(property) + + // Hack to get the proper field name. + fields[i].Name = storageResource.Fields[i].Name + } + + return &HandlerResource{ + Name: swaggerResource.Name, + Fields: fields, + } +} + +func propertyToHandlerField(property Property) HandlerField { + placeholderValue := `swag.String("` + property.Name + `")` // Fallback placeholder + + switch property.Type { + case "integer": + placeholderValue = `swag.Int64(100)` + case "boolean": + placeholderValue = `swag.Bool(true)` + case "number": + placeholderValue = `swag.Float64(10.0)` + } + + if property.Format != nil { + switch *property.Format { + case "date-time": + placeholderValue = `conv.DateTime(strfmt.DateTime(time.Now()))` + + case "uuid4": + placeholderValue = `conv.UUID4(strfmt.UUID4("1606388b-1f88-4f56-bd97-c27fbc3fe080"))` + } + } + + return HandlerField{ + Name: property.Name, + PlaceholderValue: placeholderValue, + } +} + +type handlerConfig struct { + filePrefix string + fileSuffix string + template string +} + +var configuredHandlers = map[string]handlerConfig{ + "get-all": {"get_", "_list.go", getListHandlerTemplate}, + "get": {"get_", ".go", getHandlerTemplate}, + "post": {"post_", ".go", postHandlerTemplate}, + "put": {"put_", ".go", putHandlerTemplate}, + "delete": {"delete_", ".go", deleteHandlerTemplate}, +} + +func GenerateHandlers(resource *StorageResource, handlerBaseDir, modulePath string, methods []string, force bool) error { + packageName := strings.ToLower(resource.Name) + resourceBaseDir := filepath.Join(handlerBaseDir, packageName) + + if _, err := os.Stat(resourceBaseDir); os.IsNotExist(err) { + if err := os.Mkdir(resourceBaseDir, 0755); err != nil { + return err + } + } + + module, err := getModuleName(modulePath) + if err != nil { + return err + } + + handler := Handler{ + Module: module, + Package: packageName, + Resource: toHandlerResource(resource, toSwaggerResource(resource)), + } + + for _, method := range methods { + handlerConfig, ok := configuredHandlers[method] + if !ok { + return fmt.Errorf("unsupported method: %s", method) + } + + outputPath := filepath.Join(resourceBaseDir, handlerConfig.filePrefix+packageName+handlerConfig.fileSuffix) + if err := executeTemplate(handlerConfig.template, outputPath, handler, force); err != nil { + return err + } + } + + return nil +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) +} + +func createDirIfAbsent(path string) error { + if !fileExists(path) { + if err := os.MkdirAll(path, 0755); err != nil { + return fmt.Errorf("failed to create directory '%s': %w", path, err) + } + } + + return nil +} + +func executeTemplate(templateStr, outputPath string, data interface{}, force bool) error { + templ := template.Template{} + if _, err := templ.Parse(templateStr); err != nil { + return fmt.Errorf("failed to parse template: %w", err) + } + + if !force && fileExists(outputPath) { + return fmt.Errorf("file '%s' already exists; call with --force to overwrite", outputPath) + } + + file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to open file '%s': %w", outputPath, err) + } + defer file.Close() + + if err := templ.Execute(file, data); err != nil { + return fmt.Errorf("failed to execute template: %w", err) + } + + return nil +} + +func goToJSNaming(name string) string { + // Hack to properly handle ID. + switch name { + case "ID": + return "id" + default: + first := strings.ToLower(name[0:1]) + return first + name[1:] + } +} + +func getModuleName(absolutePathToGoMod string) (string, error) { + dat, err := os.ReadFile(absolutePathToGoMod) + + if err != nil { + return "", fmt.Errorf("failed to read go.mod: %w", err) + } + + mod := modfile.ModulePath(dat) + + return mod, nil +} diff --git a/scripts/internal/scaffold/scaffold_test.go b/scripts/internal/scaffold/scaffold_test.go new file mode 100644 index 00000000..2ca4f32e --- /dev/null +++ b/scripts/internal/scaffold/scaffold_test.go @@ -0,0 +1,86 @@ +//go:build scripts + +package scaffold_test + +import ( + "os" + "path/filepath" + "testing" + + "allaboutapps.dev/aw/go-starter/internal/test" + "allaboutapps.dev/aw/go-starter/internal/util" + "allaboutapps.dev/aw/go-starter/scripts/internal/scaffold" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + resourcePath = "testdata/test_resource.txt" + definitionsPath = "testdata/definitions" + pathsPath = "testdata/paths" + handlerPath = "testdata/testresource" + modulePath = filepath.Join(util.GetProjectRootDir(), "go.mod") + + defaultMethods = []string{"get-all", "get", "post", "put", "delete"} +) + +func TestParseModel_Success(t *testing.T) { + // Execute + resource, err := scaffold.ParseModel(resourcePath) + + // Assert + require.NoError(t, err) + test.Snapshoter.Save(t, resource) +} + +func TestGenerateSwagger_Success(t *testing.T) { + // Setup + resource, err := scaffold.ParseModel(resourcePath) + require.NoError(t, err) + + // Execute + err = scaffold.GenerateSwagger(resource, "testdata", true) + + // Assert + require.NoError(t, err) + + assert.DirExists(t, definitionsPath, "Should create the definitions directory") + assert.DirExists(t, pathsPath, "Should create the paths directory") + assert.FileExists(t, filepath.Join(definitionsPath, "testresource.yml"), "Should generate the definition spec") + assert.FileExists(t, filepath.Join(pathsPath, "testresource.yml"), "Should generate the paths spec") + + // Cleanup + err = os.RemoveAll(definitionsPath) + require.NoError(t, err) + err = os.RemoveAll(pathsPath) + require.NoError(t, err) +} + +func TestGenerateHandlers_Success(t *testing.T) { + // Setup + resource, err := scaffold.ParseModel(resourcePath) + require.NoError(t, err) + err = scaffold.GenerateSwagger(resource, "testdata", true) + require.NoError(t, err) + + // Execute + err = scaffold.GenerateHandlers(resource, "testdata", modulePath, defaultMethods, true) + + // Assert + require.NoError(t, err) + + assert.DirExists(t, handlerPath, "Should create the handler directory") + assert.FileExists(t, filepath.Join(handlerPath, "get_testresource_list.go"), "Should create the GET list handler") + assert.FileExists(t, filepath.Join(handlerPath, "get_testresource.go"), "Should create the GET handler") + assert.FileExists(t, filepath.Join(handlerPath, "post_testresource.go"), "Should create the POST handler") + assert.FileExists(t, filepath.Join(handlerPath, "put_testresource.go"), "Should create the PUT handler") + assert.FileExists(t, filepath.Join(handlerPath, "delete_testresource.go"), "Should create the DELETE handler") + + // Cleanup + err = os.RemoveAll(definitionsPath) + require.NoError(t, err) + err = os.RemoveAll(pathsPath) + require.NoError(t, err) + err = os.RemoveAll(handlerPath) + require.NoError(t, err) +} diff --git a/scripts/internal/scaffold/templates.go b/scripts/internal/scaffold/templates.go new file mode 100644 index 00000000..9abe81ad --- /dev/null +++ b/scripts/internal/scaffold/templates.go @@ -0,0 +1,366 @@ +//go:build scripts + +package scaffold + +const ( + swaggerDefinitionsTemplate = `swagger: "2.0" +info: + title: "" + version: 0.1.0 +paths: {} +definitions: + {{ .Name }}: + type: object + required: + {{- range .Properties}} + {{- if .Required }} + - {{ .Name }} + {{- end }} + {{- end }} + properties: + {{- range .Properties}} + {{ .Name }}: + type: {{ .Type }} + {{- if not .Required }} + x-nullable: true + {{- end }} + {{- if .Format }} + format: {{ .Format }} + {{- end }} + {{- end }} + + {{ .Name }}Payload: + type: object + required: + {{- range .PayloadProperties}} + {{- if .Required }} + - {{ .Name }} + {{- end }} + {{- end }} + properties: + {{- range .PayloadProperties}} + {{ .Name }}: + type: {{ .Type }} + {{- if not .Required }} + x-nullable: true + {{- end }} + {{- if .Format }} + format: {{ .Format }} + {{- end }} + {{- end }} + + {{ .Name }}List: + type: array + items: + $ref: "#/definitions/{{ .Name }}" +` + + swaggerPathsTemplate = `swagger: "2.0" +info: + title: "" + version: 0.1.0 +parameters: + {{ .Name }}IdParam: + type: string + format: uuid4 + name: id + description: ID of {{ .Name }} + in: path + required: true + +paths: + /api/v1/{{ .URLName }}s: + get: + security: + - Bearer: [] + description: "Return a list of {{ .Name }}" + tags: + - {{ .Name }} + summary: "Return a list of {{ .Name }}" + operationId: Get{{ .Name }}ListRoute + responses: + "200": + description: Success + schema: + $ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}List" + + post: + security: + - Bearer: [] + description: "Update the given {{ .Name }}" + tags: + - {{ .Name }} + summary: "Update the given {{ .Name }}" + operationId: Post{{ .Name }}Route + parameters: + - name: Payload + in: body + schema: + $ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}Payload" + responses: + "200": + description: Success + schema: + $ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}" + + /api/v1/{{ .URLName }}s/{id}: + get: + security: + - Bearer: [] + description: "Return {{ .Name }} with ID" + tags: + - {{ .Name }} + summary: "Return {{ .Name }} with ID" + operationId: Get{{ .Name }}Route + parameters: + - $ref: "#/parameters/{{ .Name }}IdParam" + responses: + "200": + description: Success + schema: + $ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}" + + put: + security: + - Bearer: [] + description: "Update the given {{ .Name }}" + tags: + - {{ .Name }} + summary: "Update the given {{ .Name }}" + operationId: Put{{ .Name }}Route + parameters: + - $ref: "#/parameters/{{ .Name }}IdParam" + - name: Payload + in: body + schema: + $ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}Payload" + responses: + "200": + description: Success + schema: + $ref: "../definitions/{{ .URLName }}.yml#/definitions/{{ .Name }}" + + delete: + security: + - Bearer: [] + description: "Delete {{ .Name }} with ID" + tags: + - {{ .Name }} + summary: "Delete {{ .Name }} with ID" + operationId: Delete{{ .Name }}Route + parameters: + - $ref: "#/parameters/{{ .Name }}IdParam" + responses: + "204": + description: Success +` + + getHandlerTemplate = `package {{ .Package }} + +import ( + "net/http" + "time" + + "{{ .Module }}/internal/api" + "{{ .Module }}/internal/types" + "{{ .Module }}/internal/util" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/strfmt/conv" + "github.com/go-openapi/swag" + "github.com/labstack/echo/v4" +) + +func Get{{ .Resource.Name }}Route(s *api.Server) *echo.Route { + return s.Router.APIV1{{ .Resource.Name }}.GET("/:id", get{{ .Resource.Name }}Handler(s)) +} + +func get{{ .Resource.Name }}Handler(s *api.Server) echo.HandlerFunc { + return func(c echo.Context) error { + /* Uncomment for real implementation + ctx := c.Request().Context() + + params := {{ .Package }}.NewGet{{ .Resource.Name }}RouteParams() + err := util.BindAndValidatePathParams(c, ¶ms) + if err != nil { + return err + } + id := params.ID.String() + + // TODO: Implement + */ + + response := types.{{ .Resource.Name }}{ + {{- range .Resource.Fields }} + {{ .Name }}: {{ .PlaceholderValue }}, + {{- end }} + } + + return util.ValidateAndReturn(c, http.StatusOK, &response) + } +} +` + + getListHandlerTemplate = `package {{ .Package }} + +import ( + "net/http" + "time" + + "{{ .Module }}/internal/api" + "{{ .Module }}/internal/types" + "{{ .Module }}/internal/util" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/strfmt/conv" + "github.com/go-openapi/swag" + "github.com/labstack/echo/v4" +) + +func Get{{ .Resource.Name }}ListRoute(s *api.Server) *echo.Route { + return s.Router.APIV1{{ .Resource.Name }}.GET("", get{{ .Resource.Name }}ListHandler(s)) +} + +func get{{ .Resource.Name }}ListHandler(s *api.Server) echo.HandlerFunc { + return func(c echo.Context) error { + /* Uncomment for real implementation + ctx := c.Request().Context() + + // TODO: Implement + */ + + item := types.{{ .Resource.Name }}{ + {{- range .Resource.Fields }} + {{ .Name }}: {{ .PlaceholderValue }}, + {{- end }} + } + response := types.{{ .Resource.Name }}List{&item} + + return util.ValidateAndReturn(c, http.StatusOK, &response) + } +} +` + postHandlerTemplate = `package {{ .Package }} + +import ( + "net/http" + "time" + + "{{ .Module }}/internal/api" + "{{ .Module }}/internal/types" + "{{ .Module }}/internal/util" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/strfmt/conv" + "github.com/go-openapi/swag" + "github.com/labstack/echo/v4" +) + +func Post{{ .Resource.Name }}Route(s *api.Server) *echo.Route { + return s.Router.APIV1{{ .Resource.Name }}.POST("", post{{ .Resource.Name }}Handler(s)) +} + +func post{{ .Resource.Name }}Handler(s *api.Server) echo.HandlerFunc { + return func(c echo.Context) error { + /* Uncomment for real implementation + ctx := c.Request().Context() + + var body types.{{ .Resource.Name }}Payload + err := util.BindAndValidateBody(c, &body) + if err != nil { + return err + } + + // TODO: Implement + */ + + response := types.{{ .Resource.Name }}{ + {{- range .Resource.Fields }} + {{ .Name }}: {{ .PlaceholderValue }}, + {{- end }} + } + + return util.ValidateAndReturn(c, http.StatusOK, &response) + } +} +` + putHandlerTemplate = `package {{ .Package }} + +import ( + "net/http" + "time" + + "{{ .Module }}/internal/api" + "{{ .Module }}/internal/types" + "{{ .Module }}/internal/util" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/strfmt/conv" + "github.com/go-openapi/swag" + "github.com/labstack/echo/v4" +) + +func Put{{ .Resource.Name }}Route(s *api.Server) *echo.Route { + return s.Router.APIV1{{ .Resource.Name }}.PUT("/:id", put{{ .Resource.Name }}Handler(s)) +} + +func put{{ .Resource.Name }}Handler(s *api.Server) echo.HandlerFunc { + return func(c echo.Context) error { + /* Uncomment for real implementation + ctx := c.Request().Context() + + params := {{ .Package }}.NewGet{{ .Resource.Name }}RouteParams() + err := util.BindAndValidatePathParams(c, ¶ms) + if err != nil { + return err + } + id := params.ID.String() + + var body types.{{ .Resource.Name }}Payload + err = util.BindAndValidateBody(c, &body) + if err != nil { + return err + } + + // TODO: Implement + */ + + response := types.{{ .Resource.Name }}{ + {{- range .Resource.Fields }} + {{ .Name }}: {{ .PlaceholderValue }}, + {{- end }} + } + + return util.ValidateAndReturn(c, http.StatusOK, &response) + } +} +` + deleteHandlerTemplate = `package {{ .Package }} + +import ( + "net/http" + + "{{ .Module }}/internal/api" + "github.com/labstack/echo/v4" +) + +func Delete{{ .Resource.Name }}Route(s *api.Server) *echo.Route { + return s.Router.APIV1{{ .Resource.Name }}.DELETE("/:id", delete{{ .Resource.Name }}Handler(s)) +} + +func delete{{ .Resource.Name }}Handler(s *api.Server) echo.HandlerFunc { + return func(c echo.Context) error { + /* Uncomment for real implementation + ctx := c.Request().Context() + + params := {{ .Package }}.NewGet{{ .Resource.Name }}RouteParams() + err := util.BindAndValidatePathParams(c, ¶ms) + if err != nil { + return err + } + id := params.ID.String() + + // TODO: Implement + */ + + return c.NoContent(http.StatusNoContent) + } +} +` +) diff --git a/scripts/internal/scaffold/testdata/test_resource.txt b/scripts/internal/scaffold/testdata/test_resource.txt new file mode 100644 index 00000000..5f6a29dd --- /dev/null +++ b/scripts/internal/scaffold/testdata/test_resource.txt @@ -0,0 +1,758 @@ +// Code generated by SQLBoiler 4.5.0 (https://github.com/volatiletech/sqlboiler). DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. + +package models + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "strconv" + "strings" + "sync" + "time" + + "github.com/friendsofgo/errors" + "github.com/volatiletech/null/v8" + "github.com/volatiletech/sqlboiler/v4/boil" + "github.com/volatiletech/sqlboiler/v4/queries" + "github.com/volatiletech/sqlboiler/v4/queries/qm" + "github.com/volatiletech/sqlboiler/v4/queries/qmhelper" + "github.com/volatiletech/sqlboiler/v4/types" + "github.com/volatiletech/strmangle" +) + +// TestResource is an object representing the database table. +type TestResource struct { + ID string `boil:"id" json:"id" toml:"id" yaml:"id"` + NumericField types.Decimal `boil:"numeric_field" json:"numeric_field" toml:"numeric_field" yaml:"numeric_field"` + NumericNullField types.NullDecimal `boil:"numeric_null_field" json:"numeric_null_field,omitempty" toml:"numeric_null_field" yaml:"numeric_null_field,omitempty"` + IntegerField int `boil:"integer_field" json:"integer_field" toml:"integer_field" yaml:"integer_field"` + IntegerNullField null.Int `boil:"integer_null_field" json:"integer_null_field,omitempty" toml:"integer_null_field" yaml:"integer_null_field,omitempty"` + BoolField bool `boil:"bool_field" json:"bool_field" toml:"bool_field" yaml:"bool_field"` + BoolNullField null.Bool `boil:"bool_null_field" json:"bool_null_field,omitempty" toml:"bool_null_field" yaml:"bool_null_field,omitempty"` + DecimalField types.Decimal `boil:"decimal_field" json:"decimal_field" toml:"decimal_field" yaml:"decimal_field"` + DecimalNullField types.NullDecimal `boil:"decimal_null_field" json:"decimal_null_field,omitempty" toml:"decimal_null_field" yaml:"decimal_null_field,omitempty"` + TextField string `boil:"text_field" json:"text_field" toml:"text_field" yaml:"text_field"` + TextNullField null.String `boil:"text_null_field" json:"text_null_field,omitempty" toml:"text_null_field" yaml:"text_null_field,omitempty"` + TimtestamptzNullField null.Time `boil:"timtestamptz_null_field" json:"timtestamptz_null_field,omitempty" toml:"timtestamptz_null_field" yaml:"timtestamptz_null_field,omitempty"` + CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"` + UpdatedAt time.Time `boil:"updated_at" json:"updated_at" toml:"updated_at" yaml:"updated_at"` + + R *testResourceR `boil:"-" json:"-" toml:"-" yaml:"-"` + L testResourceL `boil:"-" json:"-" toml:"-" yaml:"-"` +} + +var TestResourceColumns = struct { + ID string + NumericField string + NumericNullField string + IntegerField string + IntegerNullField string + BoolField string + BoolNullField string + DecimalField string + DecimalNullField string + TextField string + TextNullField string + TimtestamptzNullField string + CreatedAt string + UpdatedAt string +}{ + ID: "id", + NumericField: "numeric_field", + NumericNullField: "numeric_null_field", + IntegerField: "integer_field", + IntegerNullField: "integer_null_field", + BoolField: "bool_field", + BoolNullField: "bool_null_field", + DecimalField: "decimal_field", + DecimalNullField: "decimal_null_field", + TextField: "text_field", + TextNullField: "text_null_field", + TimtestamptzNullField: "timtestamptz_null_field", + CreatedAt: "created_at", + UpdatedAt: "updated_at", +} + +// Generated where + +type whereHelpernull_Bool struct{ field string } + +func (w whereHelpernull_Bool) EQ(x null.Bool) qm.QueryMod { + return qmhelper.WhereNullEQ(w.field, false, x) +} +func (w whereHelpernull_Bool) NEQ(x null.Bool) qm.QueryMod { + return qmhelper.WhereNullEQ(w.field, true, x) +} +func (w whereHelpernull_Bool) IsNull() qm.QueryMod { return qmhelper.WhereIsNull(w.field) } +func (w whereHelpernull_Bool) IsNotNull() qm.QueryMod { return qmhelper.WhereIsNotNull(w.field) } +func (w whereHelpernull_Bool) LT(x null.Bool) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LT, x) +} +func (w whereHelpernull_Bool) LTE(x null.Bool) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.LTE, x) +} +func (w whereHelpernull_Bool) GT(x null.Bool) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GT, x) +} +func (w whereHelpernull_Bool) GTE(x null.Bool) qm.QueryMod { + return qmhelper.Where(w.field, qmhelper.GTE, x) +} + +var TestResourceWhere = struct { + ID whereHelperstring + NumericField whereHelpertypes_Decimal + NumericNullField whereHelpertypes_NullDecimal + IntegerField whereHelperint + IntegerNullField whereHelpernull_Int + BoolField whereHelperbool + BoolNullField whereHelpernull_Bool + DecimalField whereHelpertypes_Decimal + DecimalNullField whereHelpertypes_NullDecimal + TextField whereHelperstring + TextNullField whereHelpernull_String + TimtestamptzNullField whereHelpernull_Time + CreatedAt whereHelpertime_Time + UpdatedAt whereHelpertime_Time +}{ + ID: whereHelperstring{field: "\"test_resource\".\"id\""}, + NumericField: whereHelpertypes_Decimal{field: "\"test_resource\".\"numeric_field\""}, + NumericNullField: whereHelpertypes_NullDecimal{field: "\"test_resource\".\"numeric_null_field\""}, + IntegerField: whereHelperint{field: "\"test_resource\".\"integer_field\""}, + IntegerNullField: whereHelpernull_Int{field: "\"test_resource\".\"integer_null_field\""}, + BoolField: whereHelperbool{field: "\"test_resource\".\"bool_field\""}, + BoolNullField: whereHelpernull_Bool{field: "\"test_resource\".\"bool_null_field\""}, + DecimalField: whereHelpertypes_Decimal{field: "\"test_resource\".\"decimal_field\""}, + DecimalNullField: whereHelpertypes_NullDecimal{field: "\"test_resource\".\"decimal_null_field\""}, + TextField: whereHelperstring{field: "\"test_resource\".\"text_field\""}, + TextNullField: whereHelpernull_String{field: "\"test_resource\".\"text_null_field\""}, + TimtestamptzNullField: whereHelpernull_Time{field: "\"test_resource\".\"timtestamptz_null_field\""}, + CreatedAt: whereHelpertime_Time{field: "\"test_resource\".\"created_at\""}, + UpdatedAt: whereHelpertime_Time{field: "\"test_resource\".\"updated_at\""}, +} + +// TestResourceRels is where relationship names are stored. +var TestResourceRels = struct { +}{} + +// testResourceR is where relationships are stored. +type testResourceR struct { +} + +// NewStruct creates a new relationship struct +func (*testResourceR) NewStruct() *testResourceR { + return &testResourceR{} +} + +// testResourceL is where Load methods for each relationship are stored. +type testResourceL struct{} + +var ( + testResourceAllColumns = []string{"id", "numeric_field", "numeric_null_field", "integer_field", "integer_null_field", "bool_field", "bool_null_field", "decimal_field", "decimal_null_field", "text_field", "text_null_field", "timtestamptz_null_field", "created_at", "updated_at"} + testResourceColumnsWithoutDefault = []string{"id", "numeric_field", "numeric_null_field", "integer_field", "integer_null_field", "bool_field", "bool_null_field", "decimal_field", "decimal_null_field", "text_field", "text_null_field", "timtestamptz_null_field", "created_at", "updated_at"} + testResourceColumnsWithDefault = []string{} + testResourcePrimaryKeyColumns = []string{"id"} +) + +type ( + // TestResourceSlice is an alias for a slice of pointers to TestResource. + // This should generally be used opposed to []TestResource. + TestResourceSlice []*TestResource + + testResourceQuery struct { + *queries.Query + } +) + +// Cache for insert, update and upsert +var ( + testResourceType = reflect.TypeOf(&TestResource{}) + testResourceMapping = queries.MakeStructMapping(testResourceType) + testResourcePrimaryKeyMapping, _ = queries.BindMapping(testResourceType, testResourceMapping, testResourcePrimaryKeyColumns) + testResourceInsertCacheMut sync.RWMutex + testResourceInsertCache = make(map[string]insertCache) + testResourceUpdateCacheMut sync.RWMutex + testResourceUpdateCache = make(map[string]updateCache) + testResourceUpsertCacheMut sync.RWMutex + testResourceUpsertCache = make(map[string]insertCache) +) + +var ( + // Force time package dependency for automated UpdatedAt/CreatedAt. + _ = time.Second + // Force qmhelper dependency for where clause generation (which doesn't + // always happen) + _ = qmhelper.Where +) + +// One returns a single testResource record from the query. +func (q testResourceQuery) One(ctx context.Context, exec boil.ContextExecutor) (*TestResource, error) { + o := &TestResource{} + + queries.SetLimit(q.Query, 1) + + err := q.Bind(ctx, exec, o) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "models: failed to execute a one query for test_resource") + } + + return o, nil +} + +// All returns all TestResource records from the query. +func (q testResourceQuery) All(ctx context.Context, exec boil.ContextExecutor) (TestResourceSlice, error) { + var o []*TestResource + + err := q.Bind(ctx, exec, &o) + if err != nil { + return nil, errors.Wrap(err, "models: failed to assign all query results to TestResource slice") + } + + return o, nil +} + +// Count returns the count of all TestResource records in the query. +func (q testResourceQuery) Count(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return 0, errors.Wrap(err, "models: failed to count test_resource rows") + } + + return count, nil +} + +// Exists checks if the row exists in the table. +func (q testResourceQuery) Exists(ctx context.Context, exec boil.ContextExecutor) (bool, error) { + var count int64 + + queries.SetSelect(q.Query, nil) + queries.SetCount(q.Query) + queries.SetLimit(q.Query, 1) + + err := q.Query.QueryRowContext(ctx, exec).Scan(&count) + if err != nil { + return false, errors.Wrap(err, "models: failed to check if test_resource exists") + } + + return count > 0, nil +} + +// TestResources retrieves all the records using an executor. +func TestResources(mods ...qm.QueryMod) testResourceQuery { + mods = append(mods, qm.From("\"test_resource\"")) + return testResourceQuery{NewQuery(mods...)} +} + +// FindTestResource retrieves a single record by ID with an executor. +// If selectCols is empty Find will return all columns. +func FindTestResource(ctx context.Context, exec boil.ContextExecutor, iD string, selectCols ...string) (*TestResource, error) { + testResourceObj := &TestResource{} + + sel := "*" + if len(selectCols) > 0 { + sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",") + } + query := fmt.Sprintf( + "select %s from \"test_resource\" where \"id\"=$1", sel, + ) + + q := queries.Raw(query, iD) + + err := q.Bind(ctx, exec, testResourceObj) + if err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, sql.ErrNoRows + } + return nil, errors.Wrap(err, "models: unable to select from test_resource") + } + + return testResourceObj, nil +} + +// Insert a single record using an executor. +// See boil.Columns.InsertColumnSet documentation to understand column list inference for inserts. +func (o *TestResource) Insert(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) error { + if o == nil { + return errors.New("models: no test_resource provided for insertion") + } + + var err error + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + if o.CreatedAt.IsZero() { + o.CreatedAt = currTime + } + if o.UpdatedAt.IsZero() { + o.UpdatedAt = currTime + } + } + + nzDefaults := queries.NonZeroDefaultSet(testResourceColumnsWithDefault, o) + + key := makeCacheKey(columns, nzDefaults) + testResourceInsertCacheMut.RLock() + cache, cached := testResourceInsertCache[key] + testResourceInsertCacheMut.RUnlock() + + if !cached { + wl, returnColumns := columns.InsertColumnSet( + testResourceAllColumns, + testResourceColumnsWithDefault, + testResourceColumnsWithoutDefault, + nzDefaults, + ) + + cache.valueMapping, err = queries.BindMapping(testResourceType, testResourceMapping, wl) + if err != nil { + return err + } + cache.retMapping, err = queries.BindMapping(testResourceType, testResourceMapping, returnColumns) + if err != nil { + return err + } + if len(wl) != 0 { + cache.query = fmt.Sprintf("INSERT INTO \"test_resource\" (\"%s\") %%sVALUES (%s)%%s", strings.Join(wl, "\",\""), strmangle.Placeholders(dialect.UseIndexPlaceholders, len(wl), 1, 1)) + } else { + cache.query = "INSERT INTO \"test_resource\" %sDEFAULT VALUES%s" + } + + var queryOutput, queryReturning string + + if len(cache.retMapping) != 0 { + queryReturning = fmt.Sprintf(" RETURNING \"%s\"", strings.Join(returnColumns, "\",\"")) + } + + cache.query = fmt.Sprintf(cache.query, queryOutput, queryReturning) + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, vals) + } + + if len(cache.retMapping) != 0 { + err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(queries.PtrsFromMapping(value, cache.retMapping)...) + } else { + _, err = exec.ExecContext(ctx, cache.query, vals...) + } + + if err != nil { + return errors.Wrap(err, "models: unable to insert into test_resource") + } + + if !cached { + testResourceInsertCacheMut.Lock() + testResourceInsertCache[key] = cache + testResourceInsertCacheMut.Unlock() + } + + return nil +} + +// Update uses an executor to update the TestResource. +// See boil.Columns.UpdateColumnSet documentation to understand column list inference for updates. +// Update does not automatically update the record in case of default values. Use .Reload() to refresh the records. +func (o *TestResource) Update(ctx context.Context, exec boil.ContextExecutor, columns boil.Columns) (int64, error) { + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + o.UpdatedAt = currTime + } + + var err error + key := makeCacheKey(columns, nil) + testResourceUpdateCacheMut.RLock() + cache, cached := testResourceUpdateCache[key] + testResourceUpdateCacheMut.RUnlock() + + if !cached { + wl := columns.UpdateColumnSet( + testResourceAllColumns, + testResourcePrimaryKeyColumns, + ) + + if !columns.IsWhitelist() { + wl = strmangle.SetComplement(wl, []string{"created_at"}) + } + if len(wl) == 0 { + return 0, errors.New("models: unable to update test_resource, could not build whitelist") + } + + cache.query = fmt.Sprintf("UPDATE \"test_resource\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, wl), + strmangle.WhereClause("\"", "\"", len(wl)+1, testResourcePrimaryKeyColumns), + ) + cache.valueMapping, err = queries.BindMapping(testResourceType, testResourceMapping, append(wl, testResourcePrimaryKeyColumns...)) + if err != nil { + return 0, err + } + } + + values := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), cache.valueMapping) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, values) + } + var result sql.Result + result, err = exec.ExecContext(ctx, cache.query, values...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to update test_resource row") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by update for test_resource") + } + + if !cached { + testResourceUpdateCacheMut.Lock() + testResourceUpdateCache[key] = cache + testResourceUpdateCacheMut.Unlock() + } + + return rowsAff, nil +} + +// UpdateAll updates all rows with the specified column values. +func (q testResourceQuery) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + queries.SetUpdate(q.Query, cols) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "models: unable to update all for test_resource") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: unable to retrieve rows affected for test_resource") + } + + return rowsAff, nil +} + +// UpdateAll updates all rows with the specified column values, using an executor. +func (o TestResourceSlice) UpdateAll(ctx context.Context, exec boil.ContextExecutor, cols M) (int64, error) { + ln := int64(len(o)) + if ln == 0 { + return 0, nil + } + + if len(cols) == 0 { + return 0, errors.New("models: update all requires at least one column argument") + } + + colNames := make([]string, len(cols)) + args := make([]interface{}, len(cols)) + + i := 0 + for name, value := range cols { + colNames[i] = name + args[i] = value + i++ + } + + // Append all of the primary key values for each column + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), testResourcePrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := fmt.Sprintf("UPDATE \"test_resource\" SET %s WHERE %s", + strmangle.SetParamNames("\"", "\"", 1, colNames), + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), len(colNames)+1, testResourcePrimaryKeyColumns, len(o))) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args...) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to update all in testResource slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: unable to retrieve rows affected all in update all testResource") + } + return rowsAff, nil +} + +// Upsert attempts an insert using an executor, and does an update or ignore on conflict. +// See boil.Columns documentation for how to properly use updateColumns and insertColumns. +func (o *TestResource) Upsert(ctx context.Context, exec boil.ContextExecutor, updateOnConflict bool, conflictColumns []string, updateColumns, insertColumns boil.Columns) error { + if o == nil { + return errors.New("models: no test_resource provided for upsert") + } + if !boil.TimestampsAreSkipped(ctx) { + currTime := time.Now().In(boil.GetLocation()) + + if o.CreatedAt.IsZero() { + o.CreatedAt = currTime + } + o.UpdatedAt = currTime + } + + nzDefaults := queries.NonZeroDefaultSet(testResourceColumnsWithDefault, o) + + // Build cache key in-line uglily - mysql vs psql problems + buf := strmangle.GetBuffer() + if updateOnConflict { + buf.WriteByte('t') + } else { + buf.WriteByte('f') + } + buf.WriteByte('.') + for _, c := range conflictColumns { + buf.WriteString(c) + } + buf.WriteByte('.') + buf.WriteString(strconv.Itoa(updateColumns.Kind)) + for _, c := range updateColumns.Cols { + buf.WriteString(c) + } + buf.WriteByte('.') + buf.WriteString(strconv.Itoa(insertColumns.Kind)) + for _, c := range insertColumns.Cols { + buf.WriteString(c) + } + buf.WriteByte('.') + for _, c := range nzDefaults { + buf.WriteString(c) + } + key := buf.String() + strmangle.PutBuffer(buf) + + testResourceUpsertCacheMut.RLock() + cache, cached := testResourceUpsertCache[key] + testResourceUpsertCacheMut.RUnlock() + + var err error + + if !cached { + insert, ret := insertColumns.InsertColumnSet( + testResourceAllColumns, + testResourceColumnsWithDefault, + testResourceColumnsWithoutDefault, + nzDefaults, + ) + update := updateColumns.UpdateColumnSet( + testResourceAllColumns, + testResourcePrimaryKeyColumns, + ) + + if updateOnConflict && len(update) == 0 { + return errors.New("models: unable to upsert test_resource, could not build update column list") + } + + conflict := conflictColumns + if len(conflict) == 0 { + conflict = make([]string, len(testResourcePrimaryKeyColumns)) + copy(conflict, testResourcePrimaryKeyColumns) + } + cache.query = buildUpsertQueryPostgres(dialect, "\"test_resource\"", updateOnConflict, ret, update, conflict, insert) + + cache.valueMapping, err = queries.BindMapping(testResourceType, testResourceMapping, insert) + if err != nil { + return err + } + if len(ret) != 0 { + cache.retMapping, err = queries.BindMapping(testResourceType, testResourceMapping, ret) + if err != nil { + return err + } + } + } + + value := reflect.Indirect(reflect.ValueOf(o)) + vals := queries.ValuesFromMapping(value, cache.valueMapping) + var returns []interface{} + if len(cache.retMapping) != 0 { + returns = queries.PtrsFromMapping(value, cache.retMapping) + } + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, cache.query) + fmt.Fprintln(writer, vals) + } + if len(cache.retMapping) != 0 { + err = exec.QueryRowContext(ctx, cache.query, vals...).Scan(returns...) + if err == sql.ErrNoRows { + err = nil // Postgres doesn't return anything when there's no update + } + } else { + _, err = exec.ExecContext(ctx, cache.query, vals...) + } + if err != nil { + return errors.Wrap(err, "models: unable to upsert test_resource") + } + + if !cached { + testResourceUpsertCacheMut.Lock() + testResourceUpsertCache[key] = cache + testResourceUpsertCacheMut.Unlock() + } + + return nil +} + +// Delete deletes a single TestResource record with an executor. +// Delete will match against the primary key column to find the record to delete. +func (o *TestResource) Delete(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if o == nil { + return 0, errors.New("models: no TestResource provided for delete") + } + + args := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(o)), testResourcePrimaryKeyMapping) + sql := "DELETE FROM \"test_resource\" WHERE \"id\"=$1" + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args...) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to delete from test_resource") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by delete for test_resource") + } + + return rowsAff, nil +} + +// DeleteAll deletes all matching rows. +func (q testResourceQuery) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if q.Query == nil { + return 0, errors.New("models: no testResourceQuery provided for delete all") + } + + queries.SetDelete(q.Query) + + result, err := q.Query.ExecContext(ctx, exec) + if err != nil { + return 0, errors.Wrap(err, "models: unable to delete all from test_resource") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for test_resource") + } + + return rowsAff, nil +} + +// DeleteAll deletes all rows in the slice, using an executor. +func (o TestResourceSlice) DeleteAll(ctx context.Context, exec boil.ContextExecutor) (int64, error) { + if len(o) == 0 { + return 0, nil + } + + var args []interface{} + for _, obj := range o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), testResourcePrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "DELETE FROM \"test_resource\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 1, testResourcePrimaryKeyColumns, len(o)) + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, args) + } + result, err := exec.ExecContext(ctx, sql, args...) + if err != nil { + return 0, errors.Wrap(err, "models: unable to delete all from testResource slice") + } + + rowsAff, err := result.RowsAffected() + if err != nil { + return 0, errors.Wrap(err, "models: failed to get rows affected by deleteall for test_resource") + } + + return rowsAff, nil +} + +// Reload refetches the object from the database +// using the primary keys with an executor. +func (o *TestResource) Reload(ctx context.Context, exec boil.ContextExecutor) error { + ret, err := FindTestResource(ctx, exec, o.ID) + if err != nil { + return err + } + + *o = *ret + return nil +} + +// ReloadAll refetches every row with matching primary key column values +// and overwrites the original object slice with the newly updated slice. +func (o *TestResourceSlice) ReloadAll(ctx context.Context, exec boil.ContextExecutor) error { + if o == nil || len(*o) == 0 { + return nil + } + + slice := TestResourceSlice{} + var args []interface{} + for _, obj := range *o { + pkeyArgs := queries.ValuesFromMapping(reflect.Indirect(reflect.ValueOf(obj)), testResourcePrimaryKeyMapping) + args = append(args, pkeyArgs...) + } + + sql := "SELECT \"test_resource\".* FROM \"test_resource\" WHERE " + + strmangle.WhereClauseRepeated(string(dialect.LQ), string(dialect.RQ), 1, testResourcePrimaryKeyColumns, len(*o)) + + q := queries.Raw(sql, args...) + + err := q.Bind(ctx, exec, &slice) + if err != nil { + return errors.Wrap(err, "models: unable to reload all in TestResourceSlice") + } + + *o = slice + + return nil +} + +// TestResourceExists checks if the TestResource row exists. +func TestResourceExists(ctx context.Context, exec boil.ContextExecutor, iD string) (bool, error) { + var exists bool + sql := "select exists(select 1 from \"test_resource\" where \"id\"=$1 limit 1)" + + if boil.IsDebug(ctx) { + writer := boil.DebugWriterFrom(ctx) + fmt.Fprintln(writer, sql) + fmt.Fprintln(writer, iD) + } + row := exec.QueryRowContext(ctx, sql, iD) + + err := row.Scan(&exists) + if err != nil { + return false, errors.Wrap(err, "models: unable to check if test_resource exists") + } + + return exists, nil +} diff --git a/internal/util/modulename.go b/scripts/internal/util/get_module_name.go similarity index 70% rename from internal/util/modulename.go rename to scripts/internal/util/get_module_name.go index d86421b7..ed123614 100644 --- a/internal/util/modulename.go +++ b/scripts/internal/util/get_module_name.go @@ -1,16 +1,19 @@ +//go:build scripts + package util import ( - "io/ioutil" "log" + "os" "github.com/rogpeppe/go-internal/modfile" ) +// GetModuleName returns the current go module's name as defined in the go.mod file. // https://stackoverflow.com/questions/53183356/api-to-get-the-module-name // https://github.com/rogpeppe/go-internal func GetModuleName(absolutePathToGoMod string) (string, error) { - dat, err := ioutil.ReadFile(absolutePathToGoMod) + dat, err := os.ReadFile(absolutePathToGoMod) if err != nil { log.Fatal(err) diff --git a/scripts/internal/util/get_project_root_dir.go b/scripts/internal/util/get_project_root_dir.go new file mode 100644 index 00000000..c5f20806 --- /dev/null +++ b/scripts/internal/util/get_project_root_dir.go @@ -0,0 +1,14 @@ +//go:build scripts + +package util + +import "os" + +func GetProjectRootDir() string { + + if val, ok := os.LookupEnv("PROJECT_ROOT_DIR"); ok { + return val + } + + return "/app" +} diff --git a/scripts/main.go b/scripts/main.go new file mode 100644 index 00000000..7243bfdc --- /dev/null +++ b/scripts/main.go @@ -0,0 +1,9 @@ +//go:build scripts + +package main + +import "allaboutapps.dev/aw/go-starter/scripts/cmd" + +func main() { + cmd.Execute() +} diff --git a/scripts/modulename/modulename.go b/scripts/modulename/modulename.go deleted file mode 100644 index a85f4c26..00000000 --- a/scripts/modulename/modulename.go +++ /dev/null @@ -1,31 +0,0 @@ -// +build ignore - -// This program prints the current project's module name -// It can be invoked by running go run scripts/modulename/modulename.go -package main - -import ( - "fmt" - "log" - - "allaboutapps.dev/aw/go-starter/internal/util" -) - -// https://blog.carlmjohnson.net/post/2016-11-27-how-to-use-go-generate/ - -var ( - PROJECT_ROOT = util.GetProjectRootDir() - PATH_MOD_FILE = PROJECT_ROOT + "/go.mod" -) - -// get all functions in above handler packages -// that match Get*, Put*, Post*, Patch*, Delete* -func main() { - baseModuleName, err := util.GetModuleName(PATH_MOD_FILE) - - if err != nil { - log.Fatal(err) - } - - fmt.Println(baseModuleName) -} diff --git a/scripts/sql/default_zero_values.sql b/scripts/sql/default_zero_values.sql index 8cd6c866..199fb71d 100644 --- a/scripts/sql/default_zero_values.sql +++ b/scripts/sql/default_zero_values.sql @@ -30,7 +30,6 @@ -- -- https://stackoverflow.com/questions/8146448/get-the-default-values-of-table-columns-in-postgres -- https://dba.stackexchange.com/questions/205471/why-does-information-schema-have-yes-and-no-character-strings-rather-than-bo - CREATE OR REPLACE FUNCTION check_default_go_sql_zero_values () RETURNS SETOF information_schema.columns AS $BODY$ @@ -48,7 +47,8 @@ BEGIN OR (data_type IN ('char', 'character', 'varchar', 'character varying', 'text') AND column_default NOT LIKE concat('''''', '::%')) OR (data_type IN ('smallint', 'integer', 'bigint', 'smallserial', 'serial', 'bigserial') - AND column_default <> '0') + AND (column_default <> '0' + AND column_default NOT LIKE 'nextval(%')) OR (data_type IN ('decimal', 'numeric', 'real', 'double precision') AND column_default <> '0.0')); END @@ -73,11 +73,11 @@ BEGIN check_default_go_sql_zero_values () LOOP RAISE WARNING ' %.% % : INVALID DEFAULT ''%''', item.table_name, item.column_name, item.data_type, item.column_default USING HINT = to_json(item); - END LOOP; +END LOOP; IF FOUND THEN RAISE EXCEPTION 'NOT NULL columns require the respective go zero value () AS their DEFAULT value or no DEFAULT at all' USING HINT = '0 for integer types, 0.0 for floating point numbers, false for booleans, "" for strings'; - END IF; + END IF; END; $$ LANGUAGE plpgsql; diff --git a/scripts/sql/fk_missing_index.sql b/scripts/sql/fk_missing_index.sql index bbe8b63a..b739eb13 100644 --- a/scripts/sql/fk_missing_index.sql +++ b/scripts/sql/fk_missing_index.sql @@ -40,8 +40,7 @@ WHERE c.confrelid ORDER BY pg_catalog.pg_relation_size(c.conrelid) DESC LOOP - RAISE WARNING 'CREATE INDEX "idx_%_fk_%" ON "%" ("%");', item.table, item.columns, item.table, item.columns - USING HINT = to_json(item); + RAISE WARNING 'CREATE INDEX "idx_%_fk_%" ON "%" ("%");', item.table, item.columns, item.table, item.columns USING HINT = to_json(item); END LOOP; IF FOUND THEN RAISE EXCEPTION ' We require ALL FOREIGN keys TO have an INDEX defined. '; diff --git a/test/testdata/example.jpg b/test/testdata/example.jpg new file mode 100644 index 00000000..59ff9c0b Binary files /dev/null and b/test/testdata/example.jpg differ diff --git a/test/testdata/plain.sql b/test/testdata/plain.sql new file mode 100644 index 00000000..c9d689f0 --- /dev/null +++ b/test/testdata/plain.sql @@ -0,0 +1,45 @@ +-- +-- PostgreSQL database dump +-- +-- Dumped from database version 12.4 +-- Dumped by pg_dump version 12.4 +SET statement_timeout = 0; + +SET lock_timeout = 0; + +SET idle_in_transaction_session_timeout = 0; + +SET client_encoding = 'UTF8'; + +SET standard_conforming_strings = ON; + +SELECT + pg_catalog.set_config('search_path', '', FALSE); + +SET check_function_bodies = FALSE; + +SET xmloption = content; + +SET client_min_messages = warning; + +SET row_security = OFF; + +DROP EXTENSION IF EXISTS "uuid-ossp"; + +-- +-- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: - +-- +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; + +-- +-- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: +-- +COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)'; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- PostgreSQL database dump complete +-- diff --git a/test/testdata/snapshots/TestGetUserInfo.golden b/test/testdata/snapshots/TestGetUserInfo.golden new file mode 100644 index 00000000..fee38dcf --- /dev/null +++ b/test/testdata/snapshots/TestGetUserInfo.golden @@ -0,0 +1,8 @@ +(types.GetUserInfoResponse) { + Email: (strfmt.Email) (len=17) user1@example.com, + Scopes: ([]string) (len=1) { + (string) (len=3) "app" + }, + Sub: (*string)((len=36) "f6ede5d8-e22a-4ca5-aa12-67821865a3e5"), + UpdatedAt: , +} diff --git a/test/testdata/snapshots/TestILikeArgs.golden b/test/testdata/snapshots/TestILikeArgs.golden new file mode 100644 index 00000000..c3f19156 --- /dev/null +++ b/test/testdata/snapshots/TestILikeArgs.golden @@ -0,0 +1,4 @@ +([]interface {}) (len=2) { + (string) (len=12) "%Max.Muster%", + (string) (len=3) "Max" +} diff --git a/test/testdata/snapshots/TestILikeSQL.golden b/test/testdata/snapshots/TestILikeSQL.golden new file mode 100644 index 00000000..9a63f138 --- /dev/null +++ b/test/testdata/snapshots/TestILikeSQL.golden @@ -0,0 +1 @@ +(string) (len=171) "SELECT * FROM \"users\" INNER JOIN app_user_profiles ON app_user_profiles.user_id=users.id WHERE (users.username ILIKE $1) AND (users.app_user_profiles.first_name ILIKE $2);" diff --git a/test/testdata/snapshots/TestLeftOuterJoinArgs.golden b/test/testdata/snapshots/TestLeftOuterJoinArgs.golden new file mode 100644 index 00000000..06a88be1 --- /dev/null +++ b/test/testdata/snapshots/TestLeftOuterJoinArgs.golden @@ -0,0 +1 @@ +([]interface {}) diff --git a/test/testdata/snapshots/TestLeftOuterJoinSQL.golden b/test/testdata/snapshots/TestLeftOuterJoinSQL.golden new file mode 100644 index 00000000..4a9ab588 --- /dev/null +++ b/test/testdata/snapshots/TestLeftOuterJoinSQL.golden @@ -0,0 +1 @@ +(string) (len=88) "SELECT * FROM \"users\" LEFT JOIN app_user_profiles ON app_user_profiles.user_id=users.id;" diff --git a/test/testdata/snapshots/TestLeftOuterJoinWithFilterArgs.golden b/test/testdata/snapshots/TestLeftOuterJoinWithFilterArgs.golden new file mode 100644 index 00000000..485151f5 --- /dev/null +++ b/test/testdata/snapshots/TestLeftOuterJoinWithFilterArgs.golden @@ -0,0 +1,3 @@ +([]interface {}) (len=1) { + (string) (len=3) "Max" +} diff --git a/test/testdata/snapshots/TestLeftOuterJoinWithFilterSQL.golden b/test/testdata/snapshots/TestLeftOuterJoinWithFilterSQL.golden new file mode 100644 index 00000000..2621c3e4 --- /dev/null +++ b/test/testdata/snapshots/TestLeftOuterJoinWithFilterSQL.golden @@ -0,0 +1 @@ +(string) (len=124) "SELECT * FROM \"users\" LEFT JOIN app_user_profiles ON app_user_profiles.user_id=users.id AND app_user_profiles.first_name=$1;" diff --git a/test/testdata/snapshots/TestOrArgs.golden b/test/testdata/snapshots/TestOrArgs.golden new file mode 100644 index 00000000..c5378ec3 --- /dev/null +++ b/test/testdata/snapshots/TestOrArgs.golden @@ -0,0 +1,12 @@ +([]interface {}) (len=7) { + (int) 123, + (string) (len=22) "max.muster@example.org", + (string) (len=3) "Max", + (string) (len=6) "Muster", + (string) (len=7) "Austria", + (*pq.StringArray)((len=2) { + (string) (len=3) "app", + (string) (len=9) "user_info" + }), + (int) 42 +} diff --git a/test/testdata/snapshots/TestOrSQL.golden b/test/testdata/snapshots/TestOrSQL.golden new file mode 100644 index 00000000..ff44ea21 --- /dev/null +++ b/test/testdata/snapshots/TestOrSQL.golden @@ -0,0 +1 @@ +(string) (len=247) "SELECT * FROM \"users\" WHERE (id = $1 OR username = $2 OR (users.profile->>'firstName' = $3 AND users.profile->>'lastName' = $4 AND users.profile->>'country' = $5 AND users.profile->'scopes' <@ to_jsonb($6::text[]) AND users.profile->>'age' = $7));" diff --git a/test/testdata/snapshots/TestParseModel_Success.golden b/test/testdata/snapshots/TestParseModel_Success.golden new file mode 100644 index 00000000..04dc4f9e --- /dev/null +++ b/test/testdata/snapshots/TestParseModel_Success.golden @@ -0,0 +1,89 @@ +(*scaffold.StorageResource)({ + Name: (string) (len=12) "TestResource", + Fields: ([]scaffold.Field) (len=14) { + (scaffold.Field) { + Name: (string) (len=2) "ID", + Type: (scaffold.FieldType) { + Name: (string) (len=6) "string" + } + }, + (scaffold.Field) { + Name: (string) (len=12) "NumericField", + Type: (scaffold.FieldType) { + Name: (string) (len=13) "types.Decimal" + } + }, + (scaffold.Field) { + Name: (string) (len=16) "NumericNullField", + Type: (scaffold.FieldType) { + Name: (string) (len=17) "types.NullDecimal" + } + }, + (scaffold.Field) { + Name: (string) (len=12) "IntegerField", + Type: (scaffold.FieldType) { + Name: (string) (len=3) "int" + } + }, + (scaffold.Field) { + Name: (string) (len=16) "IntegerNullField", + Type: (scaffold.FieldType) { + Name: (string) (len=8) "null.Int" + } + }, + (scaffold.Field) { + Name: (string) (len=9) "BoolField", + Type: (scaffold.FieldType) { + Name: (string) (len=4) "bool" + } + }, + (scaffold.Field) { + Name: (string) (len=13) "BoolNullField", + Type: (scaffold.FieldType) { + Name: (string) (len=9) "null.Bool" + } + }, + (scaffold.Field) { + Name: (string) (len=12) "DecimalField", + Type: (scaffold.FieldType) { + Name: (string) (len=13) "types.Decimal" + } + }, + (scaffold.Field) { + Name: (string) (len=16) "DecimalNullField", + Type: (scaffold.FieldType) { + Name: (string) (len=17) "types.NullDecimal" + } + }, + (scaffold.Field) { + Name: (string) (len=9) "TextField", + Type: (scaffold.FieldType) { + Name: (string) (len=6) "string" + } + }, + (scaffold.Field) { + Name: (string) (len=13) "TextNullField", + Type: (scaffold.FieldType) { + Name: (string) (len=11) "null.String" + } + }, + (scaffold.Field) { + Name: (string) (len=21) "TimtestamptzNullField", + Type: (scaffold.FieldType) { + Name: (string) (len=9) "null.Time" + } + }, + (scaffold.Field) { + Name: (string) (len=9) "CreatedAt", + Type: (scaffold.FieldType) { + Name: (string) (len=9) "time.Time" + } + }, + (scaffold.Field) { + Name: (string) (len=9) "UpdatedAt", + Type: (scaffold.FieldType) { + Name: (string) (len=9) "time.Time" + } + } + } +}) diff --git a/test/testdata/snapshots/TestPostForgotPasswordCompleteSuccess.golden b/test/testdata/snapshots/TestPostForgotPasswordCompleteSuccess.golden new file mode 100644 index 00000000..2d059854 --- /dev/null +++ b/test/testdata/snapshots/TestPostForgotPasswordCompleteSuccess.golden @@ -0,0 +1,6 @@ +(types.PostLoginResponse) { + AccessToken: , + ExpiresIn: (*int64)(86400), + RefreshToken: , + TokenType: (*string)((len=6) "bearer") +} diff --git a/test/testdata/snapshots/TestSnapshot.golden b/test/testdata/snapshots/TestSnapshot.golden new file mode 100644 index 00000000..639b6df0 --- /dev/null +++ b/test/testdata/snapshots/TestSnapshot.golden @@ -0,0 +1,7 @@ +(struct { A string; B int; C bool; D *string }) { + A: (string) (len=3) "foo", + B: (int) 1, + C: (bool) true, + D: (*string)((len=3) "bar") +} +(string) (len=12) "Hello World!" diff --git a/test/testdata/snapshots/TestSnapshotShouldFail.golden b/test/testdata/snapshots/TestSnapshotShouldFail.golden new file mode 100644 index 00000000..639b6df0 --- /dev/null +++ b/test/testdata/snapshots/TestSnapshotShouldFail.golden @@ -0,0 +1,7 @@ +(struct { A string; B int; C bool; D *string }) { + A: (string) (len=3) "foo", + B: (int) 1, + C: (bool) true, + D: (*string)((len=3) "bar") +} +(string) (len=12) "Hello World!" diff --git a/test/testdata/snapshots/TestSnapshotSkipFields.golden b/test/testdata/snapshots/TestSnapshotSkipFields.golden new file mode 100644 index 00000000..eae0b399 --- /dev/null +++ b/test/testdata/snapshots/TestSnapshotSkipFields.golden @@ -0,0 +1,7 @@ +(struct { ID string; A string; B int; C bool; D *string }) { + ID: , + A: (string) (len=3) "foo", + B: (int) 1, + C: (bool) true, + D: (*string)((len=3) "bar") +} diff --git a/test/testdata/snapshots/TestSnapshotWithLabel_A.golden b/test/testdata/snapshots/TestSnapshotWithLabel_A.golden new file mode 100644 index 00000000..66fe1a0d --- /dev/null +++ b/test/testdata/snapshots/TestSnapshotWithLabel_A.golden @@ -0,0 +1,6 @@ +(struct { A string; B int; C bool; D *string }) { + A: (string) (len=3) "foo", + B: (int) 1, + C: (bool) true, + D: (*string)((len=3) "bar") +} diff --git a/test/testdata/snapshots/TestSnapshotWithLabel_B.golden b/test/testdata/snapshots/TestSnapshotWithLabel_B.golden new file mode 100644 index 00000000..ecd59662 --- /dev/null +++ b/test/testdata/snapshots/TestSnapshotWithLabel_B.golden @@ -0,0 +1 @@ +(string) (len=12) "Hello World!" diff --git a/test/testdata/snapshots/TestSnapshotWithReplacer.golden b/test/testdata/snapshots/TestSnapshotWithReplacer.golden new file mode 100644 index 00000000..eae0b399 --- /dev/null +++ b/test/testdata/snapshots/TestSnapshotWithReplacer.golden @@ -0,0 +1,7 @@ +(struct { ID string; A string; B int; C bool; D *string }) { + ID: , + A: (string) (len=3) "foo", + B: (int) 1, + C: (bool) true, + D: (*string)((len=3) "bar") +} diff --git a/test/testdata/snapshots/TestSnapshotWithUpdate.golden b/test/testdata/snapshots/TestSnapshotWithUpdate.golden new file mode 100644 index 00000000..04fbf58c --- /dev/null +++ b/test/testdata/snapshots/TestSnapshotWithUpdate.golden @@ -0,0 +1,7 @@ +(struct { A string; B int; C bool; D *string }) { + A: (string) (len=2) "fo", + B: (int) 1, + C: (bool) true, + D: (*string)((len=3) "bar") +} +(string) (len=12) "Hello World!" diff --git a/test/testdata/snapshots/TestWhereJSONStringArgs.golden b/test/testdata/snapshots/TestWhereJSONStringArgs.golden new file mode 100644 index 00000000..477525da --- /dev/null +++ b/test/testdata/snapshots/TestWhereJSONStringArgs.golden @@ -0,0 +1,3 @@ +([]interface {}) (len=1) { + (string) (len=37) "https://example.org/users/123/profile" +} diff --git a/test/testdata/snapshots/TestWhereJSONStringSQL.golden b/test/testdata/snapshots/TestWhereJSONStringSQL.golden new file mode 100644 index 00000000..50b98d0e --- /dev/null +++ b/test/testdata/snapshots/TestWhereJSONStringSQL.golden @@ -0,0 +1 @@ +(string) (len=55) "SELECT * FROM \"users\" WHERE (users.profile::text = $1);" diff --git a/test/testdata/snapshots/TestWhereJSONStructArgs.golden b/test/testdata/snapshots/TestWhereJSONStructArgs.golden new file mode 100644 index 00000000..cd72e8cd --- /dev/null +++ b/test/testdata/snapshots/TestWhereJSONStructArgs.golden @@ -0,0 +1,16 @@ +([]interface {}) (len=6) { + (string) (len=3) "Max", + (string) (len=6) "Muster", + (string) (len=7) "Austria", + (*pq.StringArray)((len=2) { + (string) (len=3) "app", + (string) (len=9) "user_info" + }), + (int) 42, + (pq.GenericArray) { + A: ([2]string) (len=2) { + (string) (len=15) "+1 206 555 0100", + (string) (len=16) "+44 113 496 0000" + } + } +} diff --git a/test/testdata/snapshots/TestWhereJSONStructCompositionArgs.golden b/test/testdata/snapshots/TestWhereJSONStructCompositionArgs.golden new file mode 100644 index 00000000..229e24a9 --- /dev/null +++ b/test/testdata/snapshots/TestWhereJSONStructCompositionArgs.golden @@ -0,0 +1,10 @@ +([]interface {}) (len=5) { + (string) (len=3) "Max", + (string) (len=6) "Muster", + (string) (len=7) "Austria", + (*pq.StringArray)((len=2) { + (string) (len=3) "app", + (string) (len=9) "user_info" + }), + (int) 42 +} diff --git a/test/testdata/snapshots/TestWhereJSONStructCompositionSQL.golden b/test/testdata/snapshots/TestWhereJSONStructCompositionSQL.golden new file mode 100644 index 00000000..ec734f48 --- /dev/null +++ b/test/testdata/snapshots/TestWhereJSONStructCompositionSQL.golden @@ -0,0 +1 @@ +(string) (len=217) "SELECT * FROM \"users\" WHERE (users.profile->>'firstName' = $1 AND users.profile->>'lastName' = $2 AND users.profile->>'country' = $3 AND users.profile->'scopes' <@ to_jsonb($4::text[]) AND users.profile->>'age' = $5);" diff --git a/test/testdata/snapshots/TestWhereJSONStructSQL.golden b/test/testdata/snapshots/TestWhereJSONStructSQL.golden new file mode 100644 index 00000000..b24188d8 --- /dev/null +++ b/test/testdata/snapshots/TestWhereJSONStructSQL.golden @@ -0,0 +1 @@ +(string) (len=275) "SELECT * FROM \"users\" WHERE (users.profile->>'firstName' = $1 AND users.profile->>'lastName' = $2 AND users.profile->>'country' = $3 AND users.profile->'scopes' <@ to_jsonb($4::text[]) AND users.profile->>'age' = $5 AND users.profile->'phoneNumbers' <@ to_jsonb($6::text[]));" diff --git a/test/testdata/users.sql b/test/testdata/users.sql new file mode 100644 index 00000000..b693f34a --- /dev/null +++ b/test/testdata/users.sql @@ -0,0 +1,90 @@ +-- +-- PostgreSQL database dump +-- +-- Dumped from database version 12.4 +-- Dumped by pg_dump version 12.4 +SET statement_timeout = 0; + +SET lock_timeout = 0; + +SET idle_in_transaction_session_timeout = 0; + +SET client_encoding = 'UTF8'; + +SET standard_conforming_strings = ON; + +SELECT + pg_catalog.set_config('search_path', '', FALSE); + +SET check_function_bodies = FALSE; + +SET xmloption = content; + +SET client_min_messages = warning; + +SET row_security = OFF; + +ALTER TABLE IF EXISTS ONLY public.users + DROP CONSTRAINT IF EXISTS users_username_key; + +ALTER TABLE IF EXISTS ONLY public.users + DROP CONSTRAINT IF EXISTS users_pkey; + +DROP TABLE IF EXISTS public.users; + +DROP EXTENSION IF EXISTS "uuid-ossp"; + +-- +-- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: - +-- +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; + +-- +-- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: +-- +COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)'; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: dbuser +-- +CREATE TABLE public.users ( + id uuid DEFAULT public.uuid_generate_v4 () NOT NULL, + username character varying(255), + password text, + is_active boolean NOT NULL, + scopes text[] NOT NULL, + last_authenticated_at timestamp with time zone, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +ALTER TABLE public.users OWNER TO dbuser; + +-- +-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: dbuser +-- +COPY public.users (id, username, password, is_active, scopes, last_authenticated_at, created_at, updated_at) FROM stdin; +44a4b372-9d45-42a7-a4bd-9ba78b580e09 test_user1@example.com $argon2id$v=19$m=65536,t=1,p=4$bM/6DaUMUQlr8CPYHOcFwA$Cq0p8d3IKEy1+G3VfHRXDRdc15wHJtTx+UGsXD6bbWY t {app} 2020-09-16 14:39:34.098333+00 2020-09-16 12:37:27.034337+00 2020-09-16 14:39:34.098334+00 +738a3a72-2267-4727-beef-44193491c7d0 test_user2@example.com $argon2id$v=19$m=65536,t=1,p=4$goeC9sTUXfQMpLbdqHbIiA$nadmsRl8d0HTGuKOmjg1WGGOfvJkfPUb8aSj48t7upk t {app} 2020-11-10 13:28:11.259994+00 2020-11-10 13:28:11.259998+00 2020-11-10 13:28:11.259998+00 +8c39db83-c355-4e55-b10a-ac9bf0b15e50 test_user3@example.com $argon2id$v=19$m=65536,t=1,p=4$CWvCxZhldc/UmlZaHje7jg$igRKSJjHxlUR/5l21FX1aQIGbm+1J30/L1fLQGduy/U t {app} 2021-02-24 13:57:40.870073+00 2021-02-24 13:57:40.870076+00 2021-02-24 13:57:40.870076+00 +\. + +-- +-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: dbuser +-- +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + +-- +-- Name: users users_username_key; Type: CONSTRAINT; Schema: public; Owner: dbuser +-- +ALTER TABLE ONLY public.users + ADD CONSTRAINT users_username_key UNIQUE (username); + +-- +-- PostgreSQL database dump complete +-- diff --git a/tools.go b/tools.go index 2a4bc4b8..a2db2d9f 100644 --- a/tools.go +++ b/tools.go @@ -1,4 +1,4 @@ -// +build tools +//go:build tools package tools