diff --git a/.chglog/CHANGELOG.tpl.md b/.chglog/CHANGELOG.tpl.md deleted file mode 100755 index 60a67d5bc..000000000 --- a/.chglog/CHANGELOG.tpl.md +++ /dev/null @@ -1,30 +0,0 @@ -{{ range .Versions }} - -## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }}) - -{{ range .CommitGroups -}} -### {{ .Title }} - -{{ range .Commits -}} -* {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} -{{ end }} -{{ end -}} - -{{- if .RevertCommits -}} -### Reverts - -{{ range .RevertCommits -}} -* {{ .Revert.Header }} -{{ end }} -{{ end -}} - -{{- if .NoteGroups -}} -{{ range .NoteGroups -}} -### {{ .Title }} - -{{ range .Notes }} -{{ .Body }} -{{ end }} -{{ end -}} -{{ end -}} -{{ end -}} \ No newline at end of file diff --git a/.chglog/config.yml b/.chglog/config.yml deleted file mode 100755 index 1553f6198..000000000 --- a/.chglog/config.yml +++ /dev/null @@ -1,28 +0,0 @@ -style: github -template: CHANGELOG.tpl.md -info: - title: CHANGELOG - repository_url: https://github.com/go-vela/server -options: - commits: - # filters: - # Type: - # - feat - # - fix - # - perf - # - refactor - commit_groups: - # title_maps: - # feat: Features - # fix: Bug Fixes - # perf: Performance Improvements - # refactor: Code Refactoring - header: - pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$" - pattern_maps: - - Type - - Scope - - Subject - notes: - keywords: - - BREAKING CHANGE \ No newline at end of file diff --git a/.env.example b/.env.example index 8ab90001b..fdffdf154 100644 --- a/.env.example +++ b/.env.example @@ -9,23 +9,32 @@ # # ################## -# These are used by the ui service -# defined in the docker compose stack +# These are used by the ui service defined in the docker compose stack -# customize the location where you want users to provide feedback +# customize the location for the Vela server address # -# default: https://github.com/go-vela/ui/issues/new -# VELA_FEEDBACK_URL= +# Should match the "VELA_ADDR" value in docker-compose.yml when running locally. +VELA_API=http://localhost:8080 # customize the location where users can review documentation # # default: https://go-vela.github.io/docs # VELA_DOCS_URL= -# customize the location for the Vela server address +# customize the location where you want users to provide feedback # -# Should match the "VELA_ADDR" value in docker-compose.yml when running locally. -VELA_API=http://localhost:8080 +# default: https://github.com/go-vela/ui/issues/new +# VELA_FEEDBACK_URL= + +# customize the number of bytes for size of logs the UI will attempt to render +# +# default: 20000 (2 MB) +# VELA_LOG_BYTES_LIMIT= + +# customize the number of concurrent builds for a repo the UI will allow configuring +# +# default: 30 +# VELA_MAX_BUILD_LIMIT= ############################################################ # _______ _______ ______ __ __ _______ ______ # @@ -38,8 +47,7 @@ VELA_API=http://localhost:8080 # # ############################################################ -# These are used by the server service -# defined in the docker compose stack +# These are used by the server service defined in the docker compose stack # github web url (only required if using GitHub Enterprise) # @@ -47,7 +55,19 @@ VELA_API=http://localhost:8080 # VELA_SCM_ADDR= # github client id from oauth application -VELA_SCM_CLIENT= +# VELA_SCM_CLIENT= # github client secret from oauth application -VELA_SCM_SECRET= +# VELA_SCM_SECRET= + +# COMPILER FLAGS +# +# compiler github is whether or not the compiler uses github to pull templates +# +# default: false +# VELA_COMPILER_GITHUB= + +# compiler github url is the url used by the compiler to fetch templates +# +# default: https://github.com +# VELA_COMPILER_GITHUB_URL \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 11345c24d..000000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,74 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to make participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -nationality, personal appearance, race, religion, or sexual identity and -orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -- Using welcoming and inclusive language -- Being respectful of differing viewpoints and experiences -- Gracefully accepting constructive criticism -- Focusing on what is best for the community -- Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -- The use of sexualized language or imagery and unwelcome sexual attention or - advances -- Trolling, insulting/derogatory comments, and personal or political attacks -- Public or private harassment -- Publishing others' private information, such as a physical or electronic - address, without explicit permission -- Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at -[TTS-OpenSource-Office@target.com](mailto:TTS-OpenSource-Office@target.com). All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d146c46af..757c77071 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,29 +1,14 @@ # Contributing -We'd love to accept your contributions to this project! - -There are just a few guidelines you need to follow. - -## Bugs - -Bug reports should be opened up as [issues](https://help.github.com/en/github/managing-your-work-on-github/about-issues) on the [go-vela/community](https://github.com/go-vela/community) repository! - -## Feature Requests - -Feature Requests should be opened up as [issues](https://help.github.com/en/github/managing-your-work-on-github/about-issues) on the [go-vela/community](https://github.com/go-vela/community) repository! - -## Pull Requests - -**NOTE: We recommend you start by opening a new issue describing the bug or feature you're intending to fix. Even if you think it's relatively minor, it's helpful to know what people are working on.** - -We are always open to new PRs! You can follow the below guide for learning how you can contribute to the project! - ## Getting Started +We'd love to accept your contributions to this project! If you are a first time contributor, please review our [Contributing Guidelines](https://go-vela.github.io/docs/community/contributing_guidelines/) before proceeding. + ### Prerequisites -* [Review the commit guide we follow](https://chris.beams.io/posts/git-commit/#seven-rules) - ensure your commits follow our standards * [Review the local development docs](../DOCS.md) - ensures you have the Vela application stack running locally +* [Review the commit guide we follow](https://chris.beams.io/posts/git-commit/#seven-rules) - ensure your commits follow our standards +* [Review our style guide](https://go-vela.github.io/docs/community/contributing_guidelines/#style-guide) to ensure your code is clean and consistent. ### Setup @@ -62,23 +47,6 @@ cd $HOME/go-vela/server ``` * Write your code and tests to implement the changes you desire. - * Please be sure to [follow our commit rules](https://chris.beams.io/posts/git-commit/#seven-rules) - * Please address linter warnings appropriately. If you are intentionally violating a rule that triggers a linter, please annotate the respective code with `nolint` declarations [[docs](https://golangci-lint.run/usage/false-positives/)]. we are using the following format for `nolint` declarations: - - ```go - // nolint: // - ``` - - Example: - - ```go - // nolint:gocyclo // legacy function is complex, needs simplification - func superComplexFunction() error { - // .. - } - ``` - - Check the [documentation for more examples](https://golangci-lint.run/usage/false-positives/). * Run the repository code (ensures your changes perform as you desire): @@ -105,28 +73,9 @@ make clean ```bash # push your code up to your fork -git push fork master +git push fork main ``` -* Open a pull request! - * For the title of the pull request, please use the following format for the title: - - ```text - feat(wobble): add hat wobble - ^--^^------^ ^------------^ - | | | - | | +---> Summary in present tense. - | +---> Scope: a noun describing a section of the codebase (optional) - +---> Type: chore, docs, feat, fix, refactor, or test. - ``` - - * feat: adds a new feature (equivalent to a MINOR in Semantic Versioning) - * fix: fixes a bug (equivalent to a PATCH in Semantic Versioning) - * docs: changes to the documentation - * refactor: refactors production code, eg. renaming a variable; doesn't change public API - * test: adds missing tests, refactors tests; no production code change - * chore: updates something without impacting the user (ex: bump a dependency in package.json or go.mod); no production code change - - If a code change introduces a breaking change, place ! suffix after type, ie. feat(change)!: adds breaking change. correlates with MAJOR in semantic versioning. +* Make sure to follow our [PR process](https://go-vela.github.io/docs/community/contributing_guidelines/#development-workflow) when opening a pull request Thank you for your contribution! diff --git a/.github/README.md b/.github/README.md index 00578e3c3..30a9fd391 100644 --- a/.github/README.md +++ b/.github/README.md @@ -3,7 +3,7 @@ [![license](https://img.shields.io/crates/l/gl.svg)](../LICENSE) [![GoDoc](https://godoc.org/github.com/go-vela/server?status.svg)](https://godoc.org/github.com/go-vela/server) [![Go Report Card](https://goreportcard.com/badge/go-vela/server)](https://goreportcard.com/report/go-vela/server) -[![codecov](https://codecov.io/gh/go-vela/server/branch/master/graph/badge.svg)](https://codecov.io/gh/go-vela/server) +[![codecov](https://codecov.io/gh/go-vela/server/branch/main/graph/badge.svg)](https://codecov.io/gh/go-vela/server) > Vela is in active development and is a pre-release product. > diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bfceca316..bdbb9a8ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,11 +10,18 @@ on: jobs: build: runs-on: ubuntu-latest - container: - image: golang:1.17 + steps: - name: clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: install go + uses: actions/setup-go@v4 + with: + # use version from go.mod file + go-version-file: 'go.mod' + cache: true + check-latest: true - name: build run: | diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b78566d35..ecc65e90c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,10 +13,10 @@ name: "CodeQL" on: push: - branches: [ master ] + branches: [ main ] pull_request: # The branches below must be a subset of the branches above - branches: [ master ] + branches: [ main ] schedule: - cron: '23 1 * * 0' @@ -35,11 +35,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -50,7 +50,7 @@ jobs: # 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 + uses: github/codeql-action/autobuild@v2 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -64,4 +64,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 000000000..655ec6c0e --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,49 @@ +# name of the action +name: integration-test + +# trigger on pull_request events that modify this file or any database files +on: + pull_request: + paths: + - '.github/workflows/integration-test.yml' + - 'database/**' + +# pipeline to execute +jobs: + database: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_DB: vela + POSTGRES_PASSWORD: notARealPassword12345 + POSTGRES_USER: vela + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + env: + POSTGRES_ADDR: postgres://vela:notARealPassword12345@localhost:5432/vela + SQLITE_ADDR: vela.db + + steps: + - name: clone + uses: actions/checkout@v3 + + - name: install go + uses: actions/setup-go@v4 + with: + # use version from go.mod file + go-version-file: 'go.mod' + cache: true + check-latest: true + + - name: test + run: | + make integration-test \ No newline at end of file diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 0ea5d35f5..fc792032f 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -11,15 +11,22 @@ on: jobs: prerelease: runs-on: ubuntu-latest - container: - image: golang:1.17 + steps: - name: clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # ensures we fetch tag history for the repository fetch-depth: 0 + - name: install go + uses: actions/setup-go@v4 + with: + # use version from go.mod file + go-version-file: 'go.mod' + cache: true + check-latest: true + - name: setup run: | # setup git tag in Actions environment @@ -33,7 +40,7 @@ jobs: make build-static-ci - name: publish - uses: elgohr/Publish-Docker-Github-Action@master + uses: elgohr/Publish-Docker-Github-Action@v5 with: name: target/vela-server cache: true @@ -42,7 +49,7 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - name: publish-alpine - uses: elgohr/Publish-Docker-Github-Action@master + uses: elgohr/Publish-Docker-Github-Action@v5 with: name: target/vela-server cache: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 96d3d5e96..f90fb83b0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,24 +1,31 @@ # name of the action name: publish -# trigger on push events with branch master +# trigger on push events with branch main on: push: - branches: [ master ] + branches: [ main ] # pipeline to execute jobs: publish: runs-on: ubuntu-latest - container: - image: golang:1.17 + steps: - name: clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: # ensures we fetch tag history for the repository fetch-depth: 0 + - name: install go + uses: actions/setup-go@v4 + with: + # use version from go.mod file + go-version-file: 'go.mod' + cache: true + check-latest: true + - name: build env: GOOS: linux @@ -27,7 +34,7 @@ jobs: make build-static-ci - name: publish - uses: elgohr/Publish-Docker-Github-Action@master + uses: elgohr/Publish-Docker-Github-Action@v5 with: name: target/vela-server cache: true @@ -35,7 +42,7 @@ jobs: password: ${{ secrets.DOCKER_PASSWORD }} - name: publish-alpine - uses: elgohr/Publish-Docker-Github-Action@master + uses: elgohr/Publish-Docker-Github-Action@v5 with: name: target/vela-server cache: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 1619efbc4..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,53 +0,0 @@ -# name of the action -name: release - -# trigger on push events with `v*` in tag -# ignore push events with `v*-rc*` in tag -on: - push: - tags: - - 'v*' - - '!v*-rc*' - -# pipeline to execute -jobs: - release: - runs-on: ubuntu-latest - container: - image: golang:1.17 - steps: - - name: clone - uses: actions/checkout@v2 - - - name: tags - run: | - git fetch --tags - - - name: version - id: version - run: | - echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} - - - name: install - run: | - go get github.com/git-chglog/git-chglog/cmd/git-chglog - go get github.com/github-release/github-release - - - name: changelog - run: | - # https://github.com/git-chglog/git-chglog#git-chglog - $(go env GOPATH)/bin/git-chglog \ - -o $GITHUB_WORKSPACE/CHANGELOG.md \ - ${{ steps.version.outputs.VERSION }} - - - name: release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # https://github.com/github-release/github-release#how-to-use - $(go env GOPATH)/bin/github-release edit \ - --user go-vela \ - --repo server \ - --tag ${{ steps.version.outputs.VERSION }} \ - --name ${{ steps.version.outputs.VERSION }} \ - --description "$(cat $GITHUB_WORKSPACE/CHANGELOG.md)" diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index 94d21fce1..0a0027c70 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -9,11 +9,18 @@ on: jobs: diff-review: runs-on: ubuntu-latest - container: - image: golang:1.17 + steps: - name: clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: install go + uses: actions/setup-go@v4 + with: + # use version from go.mod file + go-version-file: 'go.mod' + cache: true + check-latest: true - name: golangci-lint uses: reviewdog/action-golangci-lint@v2 @@ -26,11 +33,18 @@ jobs: full-review: runs-on: ubuntu-latest - container: - image: golang:1.17 + steps: - name: clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: install go + uses: actions/setup-go@v4 + with: + # use version from go.mod file + go-version-file: 'go.mod' + cache: true + check-latest: true - name: golangci-lint uses: reviewdog/action-golangci-lint@v2 diff --git a/.github/workflows/spec.yml b/.github/workflows/spec.yml index 0411ccf59..32a3211be 100644 --- a/.github/workflows/spec.yml +++ b/.github/workflows/spec.yml @@ -10,11 +10,18 @@ on: jobs: schema: runs-on: ubuntu-latest - container: - image: golang:1.17 + steps: - name: clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: install go + uses: actions/setup-go@v4 + with: + # use version from go.mod file + go-version-file: 'go.mod' + cache: true + check-latest: true - name: tags run: | @@ -22,7 +29,7 @@ jobs: - name: create spec run: | - make spec-install + sudo make spec-install make spec - name: upload spec diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b2f98b560..cdc0c6aa3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,18 +10,25 @@ on: jobs: test: runs-on: ubuntu-latest - container: - image: golang:1.17 + steps: - name: clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: install go + uses: actions/setup-go@v4 + with: + # use version from go.mod file + go-version-file: 'go.mod' + cache: true + check-latest: true - name: test run: | - go test -race -covermode=atomic -coverprofile=coverage.out ./... + make test - name: coverage - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - file: coverage.out + file: coverage.out \ No newline at end of file diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 374ec0b89..b2dec4a9d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -10,11 +10,18 @@ on: jobs: validate: runs-on: ubuntu-latest - container: - image: golang:1.17 + steps: - name: clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: install go + uses: actions/setup-go@v4 + with: + # use version from go.mod file + go-version-file: 'go.mod' + cache: true + check-latest: true - name: tags run: | @@ -33,5 +40,5 @@ jobs: - name: validate spec run: | - make spec-install + sudo make spec-install make spec \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4ac8d000b..c557bc18b 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,30 @@ secrets.env .env.test # Files to be excluded. -.DS_Store \ No newline at end of file +.DS_Store +api-spec.json + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix +__debug_bin + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index f97da06b6..00c50154f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -28,19 +28,16 @@ linters-settings: # https://github.com/ultraware/funlen funlen: - lines: 100 - statements: 50 + # accounting for comments + lines: 160 + statements: 70 - # https://github.com/tommy-muehle/go-mnd - gomnd: - settings: - mnd: - # don't include the "operation" and "assign" - checks: argument,case,condition,return - - # https://github.com/walle/lll - lll: - line-length: 100 + # https://github.com/denis-tingaikin/go-header + goheader: + template: |- + Copyright (c) {{ YEAR }} Target Brands, Inc. All rights reserved. + + Use of this source code is governed by the LICENSE file in this repository. # https://github.com/client9/misspell misspell: @@ -48,10 +45,10 @@ linters-settings: # https://github.com/golangci/golangci-lint/blob/master/pkg/golinters/nolintlint nolintlint: - allow-leading-space: true # allow non-"machine-readable" format (ie. with leading space) - allow-unused: false # allow nolint directives that don't address a linting issue - require-explanation: true # require an explanation for nolint directives - require-specific: true # require nolint directives to be specific about which linter is being skipped + allow-leading-space: true # allow non-"machine-readable" format (ie. with leading space) + allow-unused: false # allow nolint directives that don't address a linting issue + require-explanation: true # require an explanation for nolint directives + require-specific: true # require nolint directives to be specific about which linter is being skipped # This section provides the configuration for which linters # golangci will execute. Several of them were disabled by @@ -62,57 +59,91 @@ linters: # enable a specific set of linters to run enable: - - bodyclose - - deadcode # enabled by default - - dupl - - errcheck # enabled by default - - funlen - - goconst - - gocyclo - - godot - - gofmt - - goimports - - revive - - gomnd - - goprintffuncname - - gosec - - gosimple # enabled by default - - govet # enabled by default - - ineffassign # enabled by default - - lll - - misspell - - nakedret - - nolintlint - - staticcheck # enabled by default - - structcheck # enabled by default - - stylecheck - - typecheck # enabled by default - - unconvert - - unparam - - unused # enabled by default - - varcheck # enabled by default - - whitespace - + - bidichk # checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - contextcheck # check the function whether use a non-inherited context + - deadcode # finds unused code + - dupl # code clone detection + - errcheck # checks for unchecked errors + - errorlint # find misuses of errors + - exportloopref # check for exported loop vars + - funlen # detects long functions + - goconst # finds repeated strings that could be replaced by a constant + - gocyclo # computes and checks the cyclomatic complexity of functions + - godot # checks if comments end in a period + - gofmt # checks whether code was gofmt-ed + - goheader # checks is file header matches to pattern + - goimports # fixes imports and formats code in same style as gofmt + - gomoddirectives # manage the use of 'replace', 'retract', and 'excludes' directives in go.mod + - goprintffuncname # checks that printf-like functions are named with f at the end + - gosec # inspects code for security problems + - gosimple # linter that specializes in simplifying a code + - govet # reports suspicious constructs, ex. Printf calls whose arguments don't align with the format string + - ineffassign # detects when assignments to existing variables aren't used + - makezero # finds slice declarations with non-zero initial length + - misspell # finds commonly misspelled English words in comments + - nakedret # finds naked returns in functions greater than a specified function length + - nilerr # finds the code that returns nil even if it checks that the error is not nil + - noctx # noctx finds sending http request without context.Context + - nolintlint # reports ill-formed or insufficient nolint directives + - revive # linter for go + - staticcheck # applies static analysis checks, go vet on steroids + - structcheck # finds unused struct fields + - stylecheck # replacement for golint + - tenv # analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 + - typecheck # parses and type-checks go code, like the front-end of a go compiler + - unconvert # remove unnecessary type conversions + - unparam # reports unused function parameters + - unused # checks for unused constants, variables, functions and types + - varcheck # finds unused global variables and constants + - whitespace # detects leading and trailing whitespace + - wsl # forces code to use empty lines + # static list of linters we know golangci can run but we've # chosen to leave disabled for now - # - asciicheck - # - depguard - # - dogsled - # - exhaustive - # - gochecknoinits - # - gochecknoglobals - # - gocognit - # - gocritic - # - godox - # - goerr113 - # - interfacer - # - nestif - # - noctx - # - prealloc - # - rowserrcheck - # - scopelint - # - testpackage - # - wsl + # - asciicheck - non-critical + # - cyclop - unused complexity metric + # - depguard - unused + # - dogsled - blanks allowed + # - durationcheck - unused + # - errname - unused + # - exhaustive - unused + # - exhaustivestruct - style preference + # - forbidigo - unused + # - forcetypeassert - unused + # - gci - use goimports + # - gochecknoinits - unused + # - gochecknoglobals - global variables allowed + # - gocognit - unused complexity metric + # - gocritic - style preference + # - godox - to be used in the future + # - goerr113 - to be used in the future + # - golint - archived, replaced with revive + # - gofumpt - use gofmt + # - gomnd - get too many false-positives + # - gomodguard - unused + # - ifshort - use both styles + # - ireturn - allow interfaces to be returned + # - importas - want flexibility with naming + # - lll - not too concerned about line length + # - interfacer - archived + # - nestif - non-critical + # - nilnil - style preference + # - nlreturn - style preference + # - maligned - archived, replaced with govet 'fieldalignment' + # - paralleltest - false-positives + # - prealloc - don't use + # - predeclared - unused + # - promlinter - style preference + # - rowserrcheck - unused + # - scopelint - deprecated - replaced with exportloopref + # - sqlclosecheck - unused + # - tagliatelle - use a mix of variable naming + # - testpackage - don't use this style of testing + # - thelper - false-positives + # - varnamelen - unused + # - wastedassign - duplicate functionality + # - wrapcheck - style preference # This section provides the configuration for how golangci # will report the issues it finds. @@ -126,6 +157,4 @@ issues: - funlen - goconst - gocyclo - - gomnd - - lll - - revive + - wsl diff --git a/DOCS.md b/DOCS.md index 07e7d224a..940ee02ba 100644 --- a/DOCS.md +++ b/DOCS.md @@ -2,7 +2,7 @@ This document intends to provide information on how to get the Vela application running locally. -For more information, please see our [installation docs](https://go-vela.github.io/docs/install/). +For more information, please see our [administration docs](https://go-vela.github.io/docs/administration/). ## Prerequisites @@ -10,7 +10,7 @@ This section covers the dependencies required to get the Vela application runnin * [Docker](https://docs.docker.com/install/) - building block for local development * [Docker Compose](https://docs.docker.com/compose/install/) - start up local development -* [Github OAuth Client](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) - building block for local development +* [GitHub OAuth Client](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) - building block for local development * [Golang](https://golang.org/dl/) - for source code and [dependency management](https://github.com/golang/go/wiki/Modules) * [Make](https://www.gnu.org/software/make/) - start up local development @@ -34,26 +34,26 @@ git clone git@github.com:go-vela/server.git $HOME/go-vela/server cd $HOME/go-vela/server ``` -* If using GitHub Enterprise (default: `https://github.com/`), add the Web URL to a local `.env` file: +* If using GitHub Enterprise (default: `https://github.com`), add the Web URL to a local `.env` file: ```bash # add Github Enterprise Web URL to local `.env` file for `docker-compose` -echo "VELA_SOURCE_ADDR=" >> .env +echo "VELA_SCM_ADDR=" >> .env ``` * Create an [OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) and obtain secrets for local development: * `Application name` = `Vela - local` (name of the OAuth application shouldn't matter) - * `Homepage URL` = `http://localhost:8080` (base URL of the server) + * `Homepage URL` = `http://localhost:8888` (base URL of the web UI) * `Authorization callback URL` = `http://localhost:8080/authenticate` (authenticate endpoint of the base URL of the server) * Add OAuth client secrets to a local `.env` file: ```bash # add Github Client ID to local `.env` file for `docker-compose` -echo "VELA_SOURCE_CLIENT=" >> .env +echo "VELA_SCM_CLIENT=" >> .env # add Github Client Secret to local `.env` file for `docker-compose` -echo "VELA_SOURCE_SECRET=" >> .env +echo "VELA_SCM_SECRET=" >> .env ``` ## Start @@ -93,15 +93,15 @@ In order to run a build in Vela, you'll need to add a repo to the locally runnin

1. Navigate to the `Source Repositories` page @ http://localhost:8888/account/source-repos - * For conveinence, you can reference our documentation to [learn how to enable a repo](https://go-vela.github.io/docs/usage/enable_repo/). + * For convenience, you can reference our documentation to [learn how to enable a repo](https://go-vela.github.io/docs/usage/enable_repo/). -2. Click the blue drop down arrow on the left side next to the org that contains the repo you want to enable. +2. Click the blue drop-down arrow on the left side next to the org that contains the repo you want to enable. -3. Find the repo you want to enable in the drop down list and click the blue `Enable` button on the right side. - * You should received a `success` message telling you `/ enabled.` +3. Find the repo you want to enable in the drop-down list and click the blue `Enable` button on the right side. + * You should receive a `success` message telling you `/ enabled.` 4. Click the blue `View` button to navigate directly to the repo. - * You should be redirected to http://localhost:8888// + * You should be redirected to http://localhost:8888//

@@ -116,7 +116,7 @@ In order to run a build in Vela, you'll need to add a pipeline to the repo that

1. Create a Vela [pipeline](https://go-vela.github.io/docs/tour/) to define a workflow for Vela to run. - * For convenience, you can reference our documentation to use [one of our example pipelines](https://go-vela.github.io/docs/usage/examples/). + * For convenience, you can reference our documentation to use [one of our example pipelines](https://go-vela.github.io/docs/usage/examples/). 2. Add the pipeline to the repo that was enabled above. @@ -137,8 +137,8 @@ In order to run a build in Vela, you'll need to capture a valid webhook payload 2. Find the [recent delivery](https://developer.github.com/webhooks/testing/#listing-recent-deliveries) for the pipeline that was added to your repo. 3. Create a request locally for http://localhost:8080/webhook and replicate all parts from the recent delivery. - * You should use whatever tool feels most comfortable and natural to you (`curl`, `Postman`, `Insomnia` etc.). - * You should replicate all the request headers and the request body from the recent delivery. + * You should use whatever tool feels most comfortable and natural to you (`curl`, `Postman`, `Insomnia` etc.). + * You should replicate all the request headers and the request body from the recent delivery. 4. Send the request and navigate directly to the repo (http://localhost:8888//) to watch the build run live. @@ -156,19 +156,33 @@ This section covers the different services in the stack when the Vela applicatio The `server` Docker compose service hosts the Vela server and API. -This component is used for processing web requests and managing resources in the database and publishing builds to the FIFO queue. +Known as the brains of the Vela application, this service is responsible for managing the state of application resources. + +This includes managing resources in the system (repositories, users etc.) and storing resource data in the database. + +Additionally, the server responds to event-driven requests (webhooks) which creates new builds to run on a worker. + +For more information, please review [the official documentation](https://go-vela.github.io/docs/administration/server/). ### Worker The `worker` Docker compose service hosts the Vela build daemon. -This component is used for pulling builds from the FIFO queue and executing them based off their configuration. +Known as the brawn of the Vela application, this service is responsible for managing the state of build resources. + +This includes pulling the build, provided by the server, from the queue to be run. + +For more information, please review [the official documentation](https://go-vela.github.io/docs/administration/worker/). ### UI The `ui` Docker compose service hosts the Vela UI. -This component is used for providing a user-friendly interface for triggering actions in the Vela system. +Known as the user interface for the Vela application, often referred to as the Vela UI, this service provides a means for utilizing and interacting with the Vela platform. + +The Vela UI aims to provide users with an easy-to-use toolbox that supplies most of the functionality necessary for managing, investigating, and successfully troubleshooting Vela pipelines. + +For more information, please review [the official documentation](https://go-vela.github.io/docs/administration/ui/). ### Redis diff --git a/Dockerfile b/Dockerfile index 98ecc5101..79beef6fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # # Use of this source code is governed by the LICENSE file in this repository. -FROM alpine as certs +FROM alpine:3.18.2@sha256:25fad2a32ad1f6f510e528448ae1ec69a28ef81916a004d3629874104f8a7f70 as certs RUN apk add --update --no-cache ca-certificates diff --git a/Dockerfile-alpine b/Dockerfile-alpine index f11d181f3..38ce8fded 100644 --- a/Dockerfile-alpine +++ b/Dockerfile-alpine @@ -2,7 +2,7 @@ # # Use of this source code is governed by the LICENSE file in this repository. -FROM alpine +FROM alpine:3.18.2@sha256:25fad2a32ad1f6f510e528448ae1ec69a28ef81916a004d3629874104f8a7f70 RUN apk add --update --no-cache ca-certificates diff --git a/Makefile b/Makefile index ec24f2fd1..f46e05a3f 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,13 @@ fix: @echo "### Fixing Go Code" @go fix ./... +# The `integration-test` target is intended to run all integration tests for the Go source code. +.PHONY: integration-test +integration-test: + @echo + @echo "### Integration Testing" + INTEGRATION=1 go test -run TestDatabase_Integration ./... + # The `test` target is intended to run # the tests for the Go source code. # @@ -99,7 +106,7 @@ fix: test: @echo @echo "### Testing Go Code" - @go test -race ./... + @go test -race -covermode=atomic -coverprofile=coverage.out ./... # The `test-cover` target is intended to run # the tests for the Go source code and then @@ -107,10 +114,7 @@ test: # # Usage: `make test-cover` .PHONY: test-cover -test-cover: - @echo - @echo "### Creating test coverage report" - @go test -race -covermode=atomic -coverprofile=coverage.out ./... +test-cover: test @echo @echo "### Opening test coverage report" @go tool cover -html=coverage.out @@ -267,7 +271,7 @@ spec-install: @apt-get update @apt-get install -y jq moreutils @echo "### Downloading and installing go-swagger" - @curl -o /usr/local/bin/swagger -L "https://github.com/go-swagger/go-swagger/releases/download/v0.27.0/swagger_linux_amd64" + @curl -o /usr/local/bin/swagger -L "https://github.com/go-swagger/go-swagger/releases/download/v0.30.2/swagger_linux_amd64" @chmod +x /usr/local/bin/swagger # The `spec-gen` target is intended to create an api-spec @@ -310,4 +314,14 @@ spec-version-update: # # Usage: `make spec` .PHONY: spec -spec: spec-gen spec-version-update spec-validate \ No newline at end of file +spec: spec-gen spec-version-update spec-validate + +# The `lint` target is intended to lint the +# Go source code with golangci-lint. +# +# Usage: `make lint` +.PHONY: lint +lint: + @echo + @echo "### Linting Go Code" + @golangci-lint run ./... \ No newline at end of file diff --git a/api/admin/build.go b/api/admin/build.go index c61e1995c..2be805ef2 100644 --- a/api/admin/build.go +++ b/api/admin/build.go @@ -2,7 +2,7 @@ // // Use of this source code is governed by the LICENSE file in this repository. -// nolint: dupl // ignore similar code +//nolint:dupl // ignore similar code package admin import ( @@ -20,45 +20,6 @@ import ( "github.com/sirupsen/logrus" ) -// swagger:operation GET /api/v1/admin/builds admin AdminAllBuilds -// -// Get all of the builds in the database -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved all builds from the database -// schema: -// type: array -// items: -// "$ref": "#/definitions/Build" -// '500': -// description: Unable to retrieve all builds from the database -// schema: -// "$ref": "#/definitions/Error" - -// AllBuilds represents the API handler to -// captures all builds stored in the database. -func AllBuilds(c *gin.Context) { - logrus.Info("Admin: reading all builds") - - // send API call to capture all builds - b, err := database.FromContext(c).GetBuildList() - if err != nil { - retErr := fmt.Errorf("unable to capture all builds: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, b) -} - // swagger:operation GET /api/v1/admin/builds/queue admin AllBuildsQueue // // Get all of the running and pending builds in the database @@ -89,14 +50,16 @@ func AllBuilds(c *gin.Context) { // AllBuildsQueue represents the API handler to // captures all running and pending builds stored in the database. func AllBuildsQueue(c *gin.Context) { + // capture middleware values + ctx := c.Request.Context() + logrus.Info("Admin: reading running and pending builds") // default timestamp to 24 hours ago if user did not provide it as query parameter - // nolint: gomnd // ignore magic number after := c.DefaultQuery("after", strconv.FormatInt(time.Now().UTC().Add(-24*time.Hour).Unix(), 10)) // send API call to capture pending and running builds - b, err := database.FromContext(c).GetPendingAndRunningBuilds(after) + b, err := database.FromContext(c).ListPendingAndRunningBuilds(ctx, after) if err != nil { retErr := fmt.Errorf("unable to capture all running and pending builds: %w", err) @@ -143,6 +106,9 @@ func AllBuildsQueue(c *gin.Context) { func UpdateBuild(c *gin.Context) { logrus.Info("Admin: updating build in database") + // capture middleware values + ctx := c.Request.Context() + // capture body from API request input := new(library.Build) @@ -156,7 +122,7 @@ func UpdateBuild(c *gin.Context) { } // send API call to update the build - err = database.FromContext(c).UpdateBuild(input) + b, err := database.FromContext(c).UpdateBuild(ctx, input) if err != nil { retErr := fmt.Errorf("unable to update build %d: %w", input.GetID(), err) @@ -165,5 +131,5 @@ func UpdateBuild(c *gin.Context) { return } - c.JSON(http.StatusOK, input) + c.JSON(http.StatusOK, b) } diff --git a/api/admin/clean.go b/api/admin/clean.go new file mode 100644 index 000000000..89884c8d5 --- /dev/null +++ b/api/admin/clean.go @@ -0,0 +1,138 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package admin + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types" + "github.com/go-vela/types/constants" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/admin/clean admin AdminCleanResources +// +// Update pending build resources to error status before a given time +// +// --- +// produces: +// - application/json +// parameters: +// - in: query +// name: before +// description: filter pending resources created before a certain time +// required: true +// type: integer +// - in: body +// name: body +// description: Payload containing error message +// required: true +// schema: +// "$ref": "#/definitions/Error" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated pending resources with error message +// schema: +// type: string +// '400': +// description: Unable to update resources — bad request +// schema: +// "$ref": "#/definitions/Error" +// '401': +// description: Unable to update resources — unauthorized +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update resources +// schema: +// "$ref": "#/definitions/Error" + +// CleanResources represents the API handler to +// update any user stored in the database. +func CleanResources(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + ctx := c.Request.Context() + + logrus.Infof("platform admin %s: updating pending resources in database", u.GetName()) + + // default error message + msg := "build cleaned by platform admin" + + // capture body from API request + input := new(types.Error) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for error message: %w", err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // if a message is provided, set the error message to that + if input.Message != nil { + msg = *input.Message + } + + // capture before query parameter, default to max build timeout + before, err := strconv.ParseInt(c.DefaultQuery("before", fmt.Sprint((time.Now().Add(-(time.Minute * (constants.BuildTimeoutMax + 5)))).Unix())), 10, 64) + if err != nil { + retErr := fmt.Errorf("unable to convert before query parameter %s to int64: %w", c.Query("before"), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to clean builds + builds, err := database.FromContext(c).CleanBuilds(ctx, msg, before) + if err != nil { + retErr := fmt.Errorf("unable to update builds: %w", err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + logrus.Infof("platform admin %s: cleaned %d builds in database", u.GetName(), builds) + + // clean services + services, err := database.FromContext(c).CleanServices(msg, before) + if err != nil { + retErr := fmt.Errorf("%d builds cleaned. unable to update services: %w", builds, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + logrus.Infof("platform admin %s: cleaned %d services in database", u.GetName(), services) + + // clean steps + steps, err := database.FromContext(c).CleanSteps(msg, before) + if err != nil { + retErr := fmt.Errorf("%d builds cleaned. %d services cleaned. unable to update steps: %w", builds, services, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + logrus.Infof("platform admin %s: cleaned %d steps in database", u.GetName(), steps) + + c.JSON(http.StatusOK, fmt.Sprintf("%d builds cleaned. %d services cleaned. %d steps cleaned.", builds, services, steps)) +} diff --git a/api/admin/deployment.go b/api/admin/deployment.go index 8d3f2b367..063407b6d 100644 --- a/api/admin/deployment.go +++ b/api/admin/deployment.go @@ -10,27 +10,6 @@ import ( "github.com/gin-gonic/gin" ) -// swagger:operation GET /api/v1/admin/deployments admin AdminAllDeployments -// -// Get all of the deployments in the database (Not Implemented) -// -// --- -// produces: -// - application/json -// parameters: -// responses: -// '501': -// description: This endpoint is not implemented -// schema: -// type: string - -// AllDeployments represents the API handler to -// captures all deployments stored in the database. -func AllDeployments(c *gin.Context) { - // nolint: lll // ignore long line length due to return message - c.JSON(http.StatusNotImplemented, "The server does not support the functionality required to fulfill the request.") -} - // swagger:operation PUT /api/v1/admin/deployment admin AdminUpdateDeployment // // Get All (Not Implemented) @@ -48,6 +27,5 @@ func AllDeployments(c *gin.Context) { // UpdateDeployment represents the API handler to // update any deployment stored in the database. func UpdateDeployment(c *gin.Context) { - // nolint: lll // ignore long line length due to return message c.JSON(http.StatusNotImplemented, "The server does not support the functionality required to fulfill the request.") } diff --git a/api/admin/doc.go b/api/admin/doc.go index 414fca7e4..565520dbf 100644 --- a/api/admin/doc.go +++ b/api/admin/doc.go @@ -6,5 +6,5 @@ // // Usage: // -// import "github.com/go-vela/server/api/admin" +// import "github.com/go-vela/server/api/admin" package admin diff --git a/api/admin/hook.go b/api/admin/hook.go index 55fa8ccad..789a74a25 100644 --- a/api/admin/hook.go +++ b/api/admin/hook.go @@ -2,61 +2,20 @@ // // Use of this source code is governed by the LICENSE file in this repository. -// nolint: dupl // ignore similar code +//nolint:dupl // ignore similar code package admin import ( "fmt" "net/http" + "github.com/gin-gonic/gin" "github.com/go-vela/server/database" "github.com/go-vela/server/util" - "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) -// swagger:operation GET /api/v1/admin/hooks admin AdminAllHooks -// -// Get all of the webhooks stored in the database -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved all hooks from the database -// schema: -// type: array -// items: -// "$ref": "#/definitions/Webhook" -// '500': -// description: Unable to retrieve all hooks -// schema: -// "$ref": "#/definitions/Error" - -// AllHooks represents the API handler to -// captures all hooks stored in the database. -func AllHooks(c *gin.Context) { - logrus.Info("Admin: reading all hooks") - - // send API call to capture all hooks - r, err := database.FromContext(c).GetHookList() - if err != nil { - retErr := fmt.Errorf("unable to capture all hooks: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, r) -} - // swagger:operation PUT /api/v1/admin/hook admin AdminUpdateHook // // Update a hook in the database @@ -105,7 +64,7 @@ func UpdateHook(c *gin.Context) { } // send API call to update the hook - err = database.FromContext(c).UpdateHook(input) + h, err := database.FromContext(c).UpdateHook(input) if err != nil { retErr := fmt.Errorf("unable to update hook %d: %w", input.GetID(), err) @@ -114,5 +73,5 @@ func UpdateHook(c *gin.Context) { return } - c.JSON(http.StatusOK, input) + c.JSON(http.StatusOK, h) } diff --git a/api/admin/repo.go b/api/admin/repo.go index 81916a05f..b24fb7013 100644 --- a/api/admin/repo.go +++ b/api/admin/repo.go @@ -2,7 +2,7 @@ // // Use of this source code is governed by the LICENSE file in this repository. -// nolint: dupl // ignore similar code +//nolint:dupl // ignore similar code package admin import ( @@ -18,45 +18,6 @@ import ( "github.com/sirupsen/logrus" ) -// swagger:operation GET /api/v1/admin/repos admin AdminAllRepos -// -// Get all of the repos in the database -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved all repos from the database -// schema: -// type: array -// items: -// "$ref": "#/definitions/Repo" -// '500': -// description: Unable to retrieve all repos from the database -// schema: -// "$ref": "#/definitions/Error" - -// AllRepos represents the API handler to -// captures all repos stored in the database. -func AllRepos(c *gin.Context) { - logrus.Info("Admin: reading all repos") - - // send API call to capture all repos - r, err := database.FromContext(c).GetRepoList() - if err != nil { - retErr := fmt.Errorf("unable to capture all repos: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, r) -} - // swagger:operation PUT /api/v1/admin/repo admin AdminUpdateRepo // // Update a repo in the database @@ -92,6 +53,9 @@ func AllRepos(c *gin.Context) { func UpdateRepo(c *gin.Context) { logrus.Info("Admin: updating repo in database") + // capture middleware values + ctx := c.Request.Context() + // capture body from API request input := new(library.Repo) @@ -105,7 +69,7 @@ func UpdateRepo(c *gin.Context) { } // send API call to update the repo - err = database.FromContext(c).UpdateRepo(input) + r, err := database.FromContext(c).UpdateRepo(ctx, input) if err != nil { retErr := fmt.Errorf("unable to update repo %d: %w", input.GetID(), err) @@ -114,5 +78,5 @@ func UpdateRepo(c *gin.Context) { return } - c.JSON(http.StatusOK, input) + c.JSON(http.StatusOK, r) } diff --git a/api/admin/secret.go b/api/admin/secret.go index 5ac5d57ca..c9e1d8e73 100644 --- a/api/admin/secret.go +++ b/api/admin/secret.go @@ -2,7 +2,7 @@ // // Use of this source code is governed by the LICENSE file in this repository. -// nolint: dupl // ignore similar code +//nolint:dupl // ignore similar code package admin import ( @@ -18,45 +18,6 @@ import ( "github.com/sirupsen/logrus" ) -// swagger:operation GET /api/v1/admin/secrets admin AdminAllSecrets -// -// Get all of the secrets in the database -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved all secrets from the database -// schema: -// type: array -// items: -// "$ref": "#/definitions/Secret" -// '500': -// description: Unable to retrieve all secrets from the database -// schema: -// "$ref": "#/definitions/Error" - -// AllSecrets represents the API handler to -// captures all secrets stored in the database. -func AllSecrets(c *gin.Context) { - logrus.Info("Admin: reading all secrets") - - // send API call to capture all secrets - s, err := database.FromContext(c).GetSecretList() - if err != nil { - retErr := fmt.Errorf("unable to capture all secrets: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, s) -} - // swagger:operation PUT /api/v1/admin/secret admin AdminUpdateSecret // // Update a secret in the database @@ -105,7 +66,7 @@ func UpdateSecret(c *gin.Context) { } // send API call to update the secret - err = database.FromContext(c).UpdateSecret(input) + s, err := database.FromContext(c).UpdateSecret(input) if err != nil { retErr := fmt.Errorf("unable to update secret %d: %w", input.GetID(), err) @@ -114,5 +75,5 @@ func UpdateSecret(c *gin.Context) { return } - c.JSON(http.StatusOK, input) + c.JSON(http.StatusOK, s) } diff --git a/api/admin/service.go b/api/admin/service.go index f44e7c6a3..24ddae3dc 100644 --- a/api/admin/service.go +++ b/api/admin/service.go @@ -2,7 +2,7 @@ // // Use of this source code is governed by the LICENSE file in this repository. -// nolint: dupl // ignore similar code +//nolint:dupl // ignore similar code package admin import ( @@ -18,45 +18,6 @@ import ( "github.com/sirupsen/logrus" ) -// swagger:operation GET /api/v1/admin/services admin AdminAllServices -// -// Get all of the services in the database -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved all services from the database -// schema: -// type: array -// items: -// "$ref": "#/definitions/Service" -// '500': -// description: Unable to retrieve all services from the database -// schema: -// "$ref": "#/definitions/Error" - -// AllServices represents the API handler to -// captures all services stored in the database. -func AllServices(c *gin.Context) { - logrus.Info("Admin: reading all services") - - // send API call to capture all services - s, err := database.FromContext(c).GetServiceList() - if err != nil { - retErr := fmt.Errorf("unable to capture all services: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, s) -} - // swagger:operation PUT /api/v1/admin/service admin AdminUpdateService // // Update a hook in the database @@ -106,7 +67,7 @@ func UpdateService(c *gin.Context) { } // send API call to update the service - err = database.FromContext(c).UpdateService(input) + s, err := database.FromContext(c).UpdateService(input) if err != nil { retErr := fmt.Errorf("unable to update service %d: %w", input.GetID(), err) @@ -115,5 +76,5 @@ func UpdateService(c *gin.Context) { return } - c.JSON(http.StatusOK, input) + c.JSON(http.StatusOK, s) } diff --git a/api/admin/step.go b/api/admin/step.go index fe097e2f0..2da12ece1 100644 --- a/api/admin/step.go +++ b/api/admin/step.go @@ -2,7 +2,7 @@ // // Use of this source code is governed by the LICENSE file in this repository. -// nolint: dupl // ignore similar code +//nolint:dupl // ignore similar code package admin import ( @@ -18,45 +18,6 @@ import ( "github.com/sirupsen/logrus" ) -// swagger:operation GET /api/v1/admin/steps admin AdminAllSteps -// -// Get all of the steps in the database -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved all steps from the database -// schema: -// type: array -// items: -// "$ref": "#/definitions/Step" -// '500': -// description: Unable to retrieve all steps from the database -// schema: -// "$ref": "#/definitions/Error" - -// AllSteps represents the API handler to -// captures all steps stored in the database. -func AllSteps(c *gin.Context) { - logrus.Info("Admin: reading all steps") - - // send API call to capture all steps - s, err := database.FromContext(c).GetStepList() - if err != nil { - retErr := fmt.Errorf("unable to capture all steps: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, s) -} - // swagger:operation PUT /api/v1/admin/step admin AdminUpdateStep // // Update a step in the database @@ -105,7 +66,7 @@ func UpdateStep(c *gin.Context) { } // send API call to update the step - err = database.FromContext(c).UpdateStep(input) + s, err := database.FromContext(c).UpdateStep(input) if err != nil { retErr := fmt.Errorf("unable to update step %d: %w", input.GetID(), err) @@ -114,5 +75,5 @@ func UpdateStep(c *gin.Context) { return } - c.JSON(http.StatusOK, input) + c.JSON(http.StatusOK, s) } diff --git a/api/admin/user.go b/api/admin/user.go index e8729b628..f7ef70ba3 100644 --- a/api/admin/user.go +++ b/api/admin/user.go @@ -2,7 +2,7 @@ // // Use of this source code is governed by the LICENSE file in this repository. -// nolint: dupl // ignore similar code +//nolint:dupl // ignore similar code package admin import ( @@ -18,45 +18,6 @@ import ( "github.com/sirupsen/logrus" ) -// swagger:operation GET /api/v1/admin/users admin AdminAllUsers -// -// Get all of the users in the database -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved all users from the database -// schema: -// type: array -// items: -// "$ref": "#/definitions/User" -// '500': -// description: Unable to retrieve all users from the database -// schema: -// type: string - -// AllUsers represents the API handler to -// captures all users stored in the database. -func AllUsers(c *gin.Context) { - logrus.Info("Admin: reading all users") - - // send API call to capture all users - u, err := database.FromContext(c).GetUserList() - if err != nil { - retErr := fmt.Errorf("unable to capture all users: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, u) -} - // swagger:operation PUT /api/v1/admin/user admin AdminUpdateUser // // Update a user in the database diff --git a/api/admin/worker.go b/api/admin/worker.go new file mode 100644 index 000000000..8c0f9978a --- /dev/null +++ b/api/admin/worker.go @@ -0,0 +1,73 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package admin + +import ( + "fmt" + "net/http" + + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/admin/workers/{worker}/register-token admin RegisterToken +// +// Get a worker registration token +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: worker +// description: Hostname of the worker +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully generated registration token +// schema: +// "$ref": "#/definitions/Token" +// '401': +// description: Unauthorized +// schema: +// "$ref": "#/definitions/Error" + +// RegisterToken represents the API handler to +// generate a registration token for onboarding a worker. +func RegisterToken(c *gin.Context) { + // retrieve user from context + u := user.Retrieve(c) + + logrus.Infof("Platform admin %s: generating registration token", u.GetName()) + + host := util.PathParameter(c, "worker") + + tm := c.MustGet("token-manager").(*token.Manager) + rmto := &token.MintTokenOpts{ + Hostname: host, + TokenType: constants.WorkerRegisterTokenType, + TokenDuration: tm.WorkerRegisterTokenDuration, + } + + rt, err := tm.MintToken(rmto) + if err != nil { + retErr := fmt.Errorf("unable to generate registration token: %w", err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, library.Token{Token: &rt}) +} diff --git a/api/auth/doc.go b/api/auth/doc.go new file mode 100644 index 000000000..c200b5faf --- /dev/null +++ b/api/auth/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package auth provides the auth handlers (authenticate, login, ...) for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/auth" +package auth diff --git a/api/auth/get_token.go b/api/auth/get_token.go new file mode 100644 index 000000000..29829f072 --- /dev/null +++ b/api/auth/get_token.go @@ -0,0 +1,168 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package auth + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" +) + +// swagger:operation GET /authenticate authenticate GetAuthToken +// +// Start OAuth flow or exchange tokens +// +// --- +// produces: +// - application/json +// parameters: +// - in: query +// name: code +// description: the code received after identity confirmation +// type: string +// - in: query +// name: state +// description: a random string +// type: string +// - in: query +// name: redirect_uri +// description: the url where the user will be sent after authorization +// type: string +// responses: +// '200': +// description: Successfully authenticated +// headers: +// Set-Cookie: +// type: string +// schema: +// "$ref": "#/definitions/Token" +// '307': +// description: Redirected for authentication +// '401': +// description: Unable to authenticate +// schema: +// "$ref": "#/definitions/Error" +// '503': +// description: Service unavailable +// schema: +// "$ref": "#/definitions/Error" + +// GetAuthToken represents the API handler to +// process a user logging in to Vela from +// the API or UI. +func GetAuthToken(c *gin.Context) { + var err error + + tm := c.MustGet("token-manager").(*token.Manager) + + // capture the OAuth state if present + oAuthState := c.Request.FormValue("state") + + // capture the OAuth code if present + code := c.Request.FormValue("code") + if len(code) == 0 { + // start the initial OAuth workflow + oAuthState, err = scm.FromContext(c).Login(c.Writer, c.Request) + if err != nil { + retErr := fmt.Errorf("unable to login user: %w", err) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + } + + // complete the OAuth workflow and authenticates the user + newUser, err := scm.FromContext(c).Authenticate(c.Writer, c.Request, oAuthState) + if err != nil { + retErr := fmt.Errorf("unable to authenticate user: %w", err) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + // this will happen if the user is redirected by the + // source provider as part of the authorization workflow. + if newUser == nil { + return + } + + // send API call to capture the user logging in + u, err := database.FromContext(c).GetUserForName(newUser.GetName()) + // create a new user account + if len(u.GetName()) == 0 || err != nil { + // create the user account + u := new(library.User) + u.SetName(newUser.GetName()) + u.SetToken(newUser.GetToken()) + u.SetActive(true) + u.SetAdmin(false) + + // compose jwt tokens for user + rt, at, err := tm.Compose(c, u) + if err != nil { + retErr := fmt.Errorf("unable to compose token for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + + return + } + + // store the refresh token with the user object + u.SetRefreshToken(rt) + + // send API call to create the user in the database + err = database.FromContext(c).CreateUser(u) + if err != nil { + retErr := fmt.Errorf("unable to create user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + + return + } + + // return the jwt access token + c.JSON(http.StatusOK, library.Token{Token: &at}) + + return + } + + // update the user account + u.SetToken(newUser.GetToken()) + u.SetActive(true) + + // compose jwt tokens for user + rt, at, err := tm.Compose(c, u) + if err != nil { + retErr := fmt.Errorf("unable to compose token for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + + return + } + + // store the refresh token with the user object + u.SetRefreshToken(rt) + + // send API call to update the user in the database + err = database.FromContext(c).UpdateUser(u) + if err != nil { + retErr := fmt.Errorf("unable to update user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + + return + } + + // return the user with their jwt access token + c.JSON(http.StatusOK, library.Token{Token: &at}) +} diff --git a/api/login.go b/api/auth/login.go similarity index 90% rename from api/login.go rename to api/auth/login.go index 1aa816730..36b977009 100644 --- a/api/login.go +++ b/api/auth/login.go @@ -1,17 +1,17 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. -package api +package auth import ( "fmt" "net/http" "net/url" - "github.com/go-vela/types" - "github.com/gin-gonic/gin" + "github.com/go-vela/server/util" + "github.com/go-vela/types" "github.com/sirupsen/logrus" ) @@ -43,8 +43,8 @@ func Login(c *gin.Context) { m := c.MustGet("metadata").(*types.Metadata) // capture query params - t := c.Request.FormValue("type") - p := c.Request.FormValue("port") + t := util.FormParameter(c, "type") + p := util.FormParameter(c, "port") // temp variable to hold redirect destination r := "" diff --git a/api/logout.go b/api/auth/logout.go similarity index 96% rename from api/logout.go rename to api/auth/logout.go index 5d5d39556..8accdf42e 100644 --- a/api/logout.go +++ b/api/auth/logout.go @@ -1,8 +1,8 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. -package api +package auth import ( "fmt" diff --git a/api/auth/post_token.go b/api/auth/post_token.go new file mode 100644 index 000000000..8b386bfcf --- /dev/null +++ b/api/auth/post_token.go @@ -0,0 +1,92 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package auth + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" +) + +// swagger:operation POST /authenticate/token authenticate PostAuthToken +// +// Authenticate to Vela via personal access token +// +// --- +// produces: +// - application/json +// parameters: +// - in: header +// name: Token +// type: string +// required: true +// description: > +// scopes: repo, repo:status, user:email, read:user, and read:org +// responses: +// '200': +// description: Successfully authenticated +// schema: +// "$ref": "#/definitions/Token" +// '401': +// description: Unable to authenticate +// schema: +// "$ref": "#/definitions/Error" +// '503': +// description: Service unavailable +// schema: +// "$ref": "#/definitions/Error" + +// PostAuthToken represents the API handler to +// process a user logging in using PAT to Vela from +// the API. +func PostAuthToken(c *gin.Context) { + // attempt to get user from source + u, err := scm.FromContext(c).AuthenticateToken(c.Request) + if err != nil { + retErr := fmt.Errorf("unable to authenticate user: %w", err) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + // check if the user exists + u, err = database.FromContext(c).GetUserForName(u.GetName()) + if err != nil { + retErr := fmt.Errorf("user %s not found", u.GetName()) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + // We don't need refresh token for this scenario + // We only need access token and are configured based on the config defined + tm := c.MustGet("token-manager").(*token.Manager) + + // mint token options for access token + amto := &token.MintTokenOpts{ + User: u, + TokenType: constants.UserAccessTokenType, + TokenDuration: tm.UserAccessTokenDuration, + } + at, err := tm.MintToken(amto) + + if err != nil { + retErr := fmt.Errorf("unable to compose token for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + } + + // return the user with their jwt access token + c.JSON(http.StatusOK, library.Token{Token: &at}) +} diff --git a/api/auth/redirect.go b/api/auth/redirect.go new file mode 100644 index 000000000..32b486393 --- /dev/null +++ b/api/auth/redirect.go @@ -0,0 +1,102 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package auth + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/util" + "github.com/go-vela/types" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /authenticate/web authenticate GetAuthenticateTypeWeb +// +// Authentication entrypoint that builds the right post-auth +// redirect URL for web authentication requests +// and redirects to /authenticate after +// +// --- +// produces: +// - application/json +// parameters: +// - in: query +// name: code +// description: the code received after identity confirmation +// type: string +// - in: query +// name: state +// description: a random string +// type: string +// responses: +// '307': +// description: Redirected for authentication + +// swagger:operation GET /authenticate/cli/{port} authenticate GetAuthenticateTypeCLI +// +// Authentication entrypoint that builds the right post-auth +// redirect URL for CLI authentication requests +// and redirects to /authenticate after +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: port +// required: true +// description: the port number +// type: integer +// - in: query +// name: code +// description: the code received after identity confirmation +// type: string +// - in: query +// name: state +// description: a random string +// type: string +// responses: +// '307': +// description: Redirected for authentication + +// GetAuthRedirect handles cases where the OAuth callback was +// overridden by supplying a redirect_uri in the login process. +// It will send the user to the destination to handle the last leg +// in the auth flow - exchanging "code" and "state" for a token. +// This will only handle non-headless flows (ie. web or cli). +func GetAuthRedirect(c *gin.Context) { + // load the metadata + m := c.MustGet("metadata").(*types.Metadata) + + logrus.Info("redirecting for final auth flow destination") + + // capture the path elements + t := util.PathParameter(c, "type") + p := util.PathParameter(c, "port") + + // capture the current query parameters - + // they should contain the "code" and "state" values + q := c.Request.URL.Query() + + // default redirect location if a user ended up here + // by providing an unsupported type + r := fmt.Sprintf("%s/authenticate", m.Vela.Address) + + switch t { + // cli auth flow + case "cli": + r = fmt.Sprintf("http://127.0.0.1:%s", p) + // web auth flow + case "web": + r = fmt.Sprintf("%s%s", m.Vela.WebAddress, m.Vela.WebOauthCallbackPath) + } + + // append the code and state values + r = fmt.Sprintf("%s?%s", r, q.Encode()) + + c.Redirect(http.StatusTemporaryRedirect, r) +} diff --git a/api/token.go b/api/auth/refresh.go similarity index 74% rename from api/token.go rename to api/auth/refresh.go index 1225f149f..0c12425cc 100644 --- a/api/token.go +++ b/api/auth/refresh.go @@ -1,19 +1,18 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. -package api +package auth import ( "fmt" "net/http" - "github.com/go-vela/server/router/middleware/token" + "github.com/gin-gonic/gin" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/auth" "github.com/go-vela/server/util" - "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" ) // swagger:operation GET /token-refresh authenticate GetRefreshAccessToken @@ -29,7 +28,7 @@ import ( // '200': // description: Successfully refreshed a token // schema: -// "$ref": "#/definitions/Login" +// "$ref": "#/definitions/Token" // '401': // description: Unauthorized // schema: @@ -41,7 +40,7 @@ func RefreshAccessToken(c *gin.Context) { // capture the refresh token // TODO: move this into token package and do it internally // since we are already passsing context - rt, err := token.RetrieveRefreshToken(c.Request) + rt, err := auth.RetrieveRefreshToken(c.Request) if err != nil { retErr := fmt.Errorf("refresh token error: %w", err) @@ -50,8 +49,10 @@ func RefreshAccessToken(c *gin.Context) { return } + tm := c.MustGet("token-manager").(*token.Manager) + // validate the refresh token and return a new access token - newAccessToken, err := token.Refresh(c, rt) + newAccessToken, err := tm.Refresh(c, rt) if err != nil { retErr := fmt.Errorf("unable to refresh token: %w", err) @@ -60,5 +61,5 @@ func RefreshAccessToken(c *gin.Context) { return } - c.JSON(http.StatusOK, library.Login{Token: &newAccessToken}) + c.JSON(http.StatusOK, library.Token{Token: &newAccessToken}) } diff --git a/api/auth/validate.go b/api/auth/validate.go new file mode 100644 index 000000000..d78057193 --- /dev/null +++ b/api/auth/validate.go @@ -0,0 +1,50 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package auth + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/claims" + "github.com/go-vela/server/util" +) + +// swagger:operation GET /validate-token authenticate ValidateServerToken +// +// Validate a server token +// +// --- +// produces: +// - application/json +// security: +// - CookieAuth: [] +// responses: +// '200': +// description: Successfully validated a token +// schema: +// type: string +// '401': +// description: Unauthorized +// schema: +// "$ref": "#/definitions/Error" + +// ValidateServerToken will validate if a token was issued by the server +// if it is provided in the auth header. +func ValidateServerToken(c *gin.Context) { + cl := claims.Retrieve(c) + + if !strings.EqualFold(cl.Subject, "vela-server") { + retErr := fmt.Errorf("token is not a valid server token") + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + c.JSON(http.StatusOK, "valid server token") +} diff --git a/api/authenticate.go b/api/authenticate.go deleted file mode 100644 index b484a4643..000000000 --- a/api/authenticate.go +++ /dev/null @@ -1,343 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "encoding/base64" - "fmt" - "net/http" - - "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/token" - "github.com/go-vela/server/scm" - "github.com/go-vela/server/util" - - "github.com/go-vela/types" - "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/sirupsen/logrus" -) - -// swagger:operation GET /authenticate authenticate GetAuthenticate -// -// Start OAuth flow or exchange tokens -// -// --- -// produces: -// - application/json -// parameters: -// - in: query -// name: code -// description: the code received after identity confirmation -// type: string -// - in: query -// name: state -// description: a random string -// type: string -// - in: query -// name: redirect_uri -// description: the url where the user will be sent after authorization -// type: string -// responses: -// '200': -// description: Successfully authenticated -// headers: -// Set-Cookie: -// type: string -// schema: -// "$ref": "#/definitions/Login" -// '307': -// description: Redirected for authentication -// '401': -// description: Unable to authenticate -// schema: -// "$ref": "#/definitions/Error" -// '503': -// description: Service unavailable -// schema: -// "$ref": "#/definitions/Error" - -// Authenticate represents the API handler to -// process a user logging in to Vela from -// the API or UI. -// -// nolint: funlen // ignore function length due to comments -func Authenticate(c *gin.Context) { - var err error - - // capture the OAuth state if present - oAuthState := c.Request.FormValue("state") - - // capture the OAuth code if present - code := c.Request.FormValue("code") - if len(code) == 0 { - // start the initial OAuth workflow - oAuthState, err = scm.FromContext(c).Login(c.Writer, c.Request) - if err != nil { - retErr := fmt.Errorf("unable to login user: %w", err) - - util.HandleError(c, http.StatusUnauthorized, retErr) - - return - } - } - - // complete the OAuth workflow and authenticates the user - newUser, err := scm.FromContext(c).Authenticate(c.Writer, c.Request, oAuthState) - if err != nil { - retErr := fmt.Errorf("unable to authenticate user: %w", err) - - util.HandleError(c, http.StatusUnauthorized, retErr) - - return - } - - // this will happen if the user is redirected by the - // source provider as part of the authorization workflow. - if newUser == nil { - return - } - - // send API call to capture the user logging in - u, err := database.FromContext(c).GetUserName(newUser.GetName()) - // create a new user account - if len(u.GetName()) == 0 || err != nil { - // create unique id for the user - uid, err := uuid.NewRandom() - if err != nil { - retErr := fmt.Errorf("unable to create UID for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - // create the user account - u := new(library.User) - u.SetName(newUser.GetName()) - u.SetToken(newUser.GetToken()) - u.SetHash( - base64.StdEncoding.EncodeToString( - []byte(uid.String()), - ), - ) - u.SetActive(true) - u.SetAdmin(false) - - // compose jwt tokens for user - rt, at, err := token.Compose(c, u) - if err != nil { - retErr := fmt.Errorf("unable to compose token for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - // store the refresh token with the user object - u.SetRefreshToken(rt) - - // send API call to create the user in the database - err = database.FromContext(c).CreateUser(u) - if err != nil { - retErr := fmt.Errorf("unable to create user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - // return the jwt access token - c.JSON(http.StatusOK, library.Login{Token: &at}) - - return - } - - // update the user account - u.SetToken(newUser.GetToken()) - u.SetActive(true) - - // compose jwt tokens for user - rt, at, err := token.Compose(c, u) - if err != nil { - retErr := fmt.Errorf("unable to compose token for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - // store the refresh token with the user object - u.SetRefreshToken(rt) - - // send API call to update the user in the database - err = database.FromContext(c).UpdateUser(u) - if err != nil { - retErr := fmt.Errorf("unable to update user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - // return the user with their jwt access token - c.JSON(http.StatusOK, library.Login{Token: &at}) -} - -// swagger:operation GET /authenticate/web authenticate GetAuthenticateTypeWeb -// -// Authentication entrypoint that builds the right post-auth -// redirect URL for web authentication requests -// and redirects to /authenticate after -// -// --- -// produces: -// - application/json -// parameters: -// - in: query -// name: code -// description: the code received after identity confirmation -// type: string -// - in: query -// name: state -// description: a random string -// type: string -// responses: -// '307': -// description: Redirected for authentication - -// swagger:operation GET /authenticate/cli/{port} authenticate GetAuthenticateTypeCLI -// -// Authentication entrypoint that builds the right post-auth -// redirect URL for CLI authentication requests -// and redirects to /authenticate after -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: port -// required: true -// description: the port number -// type: integer -// - in: query -// name: code -// description: the code received after identity confirmation -// type: string -// - in: query -// name: state -// description: a random string -// type: string -// responses: -// '307': -// description: Redirected for authentication - -// AuthenticateType handles cases where the OAuth callback was -// overridden by supplying a redirect_uri in the login process. -// It will send the user to the destination to handle the last leg -// in the auth flow - exchanging "code" and "state" for a token. -// This will only handle non-headless flows (ie. web or cli). -func AuthenticateType(c *gin.Context) { - // load the metadata - m := c.MustGet("metadata").(*types.Metadata) - - logrus.Info("redirecting for final auth flow destination") - - // capture the path elements - t := c.Param("type") - p := c.Param("port") - - // capture the current query parameters - - // they should contain the "code" and "state" values - q := c.Request.URL.Query() - - // default redirect location if a user ended up here - // by providing an unsupported type - r := fmt.Sprintf("%s/authenticate", m.Vela.Address) - - switch t { - // cli auth flow - case "cli": - r = fmt.Sprintf("http://127.0.0.1:%s", p) - // web auth flow - case "web": - r = fmt.Sprintf("%s%s", m.Vela.WebAddress, m.Vela.WebOauthCallbackPath) - } - - // append the code and state values - r = fmt.Sprintf("%s?%s", r, q.Encode()) - - c.Redirect(http.StatusTemporaryRedirect, r) -} - -// swagger:operation POST /authenticate/token authenticate PostAuthenticateToken -// -// Authenticate to Vela via personal access token -// -// --- -// produces: -// - application/json -// parameters: -// - in: header -// name: Token -// type: string -// required: true -// description: > -// scopes: repo, repo:status, user:email, read:user, and read:org -// responses: -// '200': -// description: Successfully authenticated -// schema: -// "$ref": "#/definitions/Login" -// '401': -// description: Unable to authenticate -// schema: -// "$ref": "#/definitions/Error" -// '503': -// description: Service unavailable -// schema: -// "$ref": "#/definitions/Error" - -// AuthenticateToken represents the API handler to -// process a user logging in using PAT to Vela from -// the API. -func AuthenticateToken(c *gin.Context) { - // attempt to get user from source - u, err := scm.FromContext(c).AuthenticateToken(c.Request) - if err != nil { - retErr := fmt.Errorf("unable to authenticate user: %w", err) - - util.HandleError(c, http.StatusUnauthorized, retErr) - - return - } - - // check if the user exists - u, err = database.FromContext(c).GetUserName(u.GetName()) - if err != nil { - retErr := fmt.Errorf("user %s not found", u.GetName()) - - util.HandleError(c, http.StatusUnauthorized, retErr) - - return - } - - // We don't need refresh token for this scenario - // We only need access token and are configured based on the config defined - m := c.MustGet("metadata").(*types.Metadata) - at, err := token.CreateAccessToken(u, m.Vela.AccessTokenDuration) - - if err != nil { - retErr := fmt.Errorf("unable to compose token for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - } - - // return the user with their jwt access token - c.JSON(http.StatusOK, library.Login{Token: &at}) -} diff --git a/api/badge.go b/api/badge.go index 79ef4e008..bb84d2c48 100644 --- a/api/badge.go +++ b/api/badge.go @@ -7,13 +7,12 @@ package api import ( "net/http" + "github.com/gin-gonic/gin" "github.com/go-vela/server/database" "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" - + "github.com/go-vela/server/util" "github.com/go-vela/types/constants" - - "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) @@ -47,7 +46,9 @@ func GetBadge(c *gin.Context) { // capture middleware values o := org.Retrieve(c) r := repo.Retrieve(c) - branch := c.DefaultQuery("branch", r.GetBranch()) + ctx := c.Request.Context() + + branch := util.QueryParameter(c, "branch", r.GetBranch()) // update engine logger with API metadata // @@ -58,7 +59,7 @@ func GetBadge(c *gin.Context) { }).Infof("creating latest build badge for repo %s on branch %s", r.GetFullName(), branch) // send API call to capture the last build for the repo and branch - b, err := database.FromContext(c).GetLastBuildByBranch(r, branch) + b, err := database.FromContext(c).LastBuildForRepo(ctx, r, branch) if err != nil { c.String(http.StatusOK, constants.BadgeUnknown) return diff --git a/api/build.go b/api/build.go deleted file mode 100644 index 0c5fb03fc..000000000 --- a/api/build.go +++ /dev/null @@ -1,1643 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strconv" - "strings" - "time" - - "github.com/go-vela/server/router/middleware/org" - - "github.com/go-vela/server/compiler" - "github.com/go-vela/server/database" - "github.com/go-vela/server/queue" - "github.com/go-vela/server/router/middleware/build" - "github.com/go-vela/server/router/middleware/executors" - "github.com/go-vela/server/router/middleware/repo" - "github.com/go-vela/server/router/middleware/user" - "github.com/go-vela/server/scm" - "github.com/go-vela/server/util" - - "github.com/go-vela/types" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/go-vela/types/pipeline" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -// swagger:operation POST /api/v1/repos/{org}/{repo}/builds builds CreateBuild -// -// Create a build in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: body -// name: body -// description: Payload containing the build to update -// required: true -// schema: -// "$ref": "#/definitions/Build" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Request processed but build was skipped -// schema: -// type: string -// '201': -// description: Successfully created the build -// type: json -// schema: -// "$ref": "#/definitions/Build" -// '400': -// description: Unable to create the build -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to create the build -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to create the build -// schema: -// "$ref": "#/definitions/Error" - -// CreateBuild represents the API handler to -// create a build in the configured backend. -// -// nolint: funlen // ignore function length due to comments -func CreateBuild(c *gin.Context) { - // capture middleware values - m := c.MustGet("metadata").(*types.Metadata) - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logger := logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }) - - logger.Infof("creating new build for repo %s", r.GetFullName()) - - // capture body from API request - input := new(library.Build) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for new build for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // verify the build has a valid event and the repo allows that event type - if (input.GetEvent() == constants.EventPush && !r.GetAllowPush()) || - (input.GetEvent() == constants.EventPull && !r.GetAllowPull()) || - (input.GetEvent() == constants.EventTag && !r.GetAllowTag()) || - (input.GetEvent() == constants.EventDeploy && !r.GetAllowDeploy()) { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to create new build: %s does not have %s events enabled", r.GetFullName(), input.GetEvent()) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // send API call to capture the repo owner - u, err = database.FromContext(c).GetUser(r.GetUserID()) - if err != nil { - retErr := fmt.Errorf("unable to get owner for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // create SQL filters for querying pending and running builds for repo - filters := map[string]interface{}{ - "status": []string{constants.StatusPending, constants.StatusRunning}, - } - - // send API call to capture the number of pending or running builds for the repo - builds, err := database.FromContext(c).GetRepoBuildCount(r, filters) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to create new build: unable to get count of builds for repo %s", r.GetFullName()) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // check if the number of pending and running builds exceeds the limit for the repo - if builds >= r.GetBuildLimit() { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to create new build: repo %s has exceeded the concurrent build limit of %d", r.GetFullName(), r.GetBuildLimit()) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // send API call to capture the last build for the repo - lastBuild, err := database.FromContext(c).GetLastBuild(r) - if err != nil { - retErr := fmt.Errorf("unable to get last build for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // update fields in build object - input.SetRepoID(r.GetID()) - input.SetStatus(constants.StatusPending) - input.SetCreated(time.Now().UTC().Unix()) - input.SetNumber(1) - input.SetParent(input.GetNumber()) - - if lastBuild != nil { - input.SetNumber( - lastBuild.GetNumber() + 1, - ) - input.SetParent(lastBuild.GetNumber()) - } - - // populate the build link if a web address is provided - if len(m.Vela.WebAddress) > 0 { - input.SetLink( - fmt.Sprintf("%s/%s/%d", m.Vela.WebAddress, r.GetFullName(), input.GetNumber()), - ) - } - - // variable to store changeset files - var files []string - // check if the build event is not pull_request - if !strings.EqualFold(input.GetEvent(), constants.EventPull) { - // send API call to capture list of files changed for the commit - files, err = scm.FromContext(c).Changeset(u, r, input.GetCommit()) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to create new build: failed to get changeset for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - } - - // handle getting changeset from a pull_request - if strings.EqualFold(input.GetEvent(), constants.EventPull) { - // capture number from build - number, err := getPRNumberFromBuild(input) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to create new build: failed to get pull_request number for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture list of files changed for the pull request - files, err = scm.FromContext(c).ChangesetPR(u, r, number) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to create new build: failed to get changeset for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - } - - // send API call to capture the pipeline configuration file - config, err := scm.FromContext(c).ConfigBackoff(u, r, input.GetCommit()) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to get pipeline configuration for %s/%d: %w", r.GetFullName(), input.GetNumber(), err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - // parse and compile the pipeline configuration file - p, err := compiler.FromContext(c). - WithBuild(input). - WithFiles(files). - WithMetadata(m). - WithRepo(r). - WithUser(u). - Compile(config) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to compile pipeline configuration for %s/%d: %w", r.GetFullName(), input.GetNumber(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // skip the build if only the init or clone steps are found - skip := skipEmptyBuild(p) - if skip != "" { - // set build to successful status - input.SetStatus(constants.StatusSuccess) - - // send API call to set the status on the commit - err = scm.FromContext(c).Status(u, input, r.GetOrg(), r.GetName()) - if err != nil { - // nolint: lll // ignore long line length due to error message - logger.Errorf("unable to set commit status for %s/%d: %v", r.GetFullName(), input.GetNumber(), err) - } - - c.JSON(http.StatusOK, skip) - return - } - - // create the objects from the pipeline in the database - err = planBuild(database.FromContext(c), p, input, r) - if err != nil { - util.HandleError(c, http.StatusInternalServerError, err) - - return - } - - // send API call to capture the created build - input, _ = database.FromContext(c).GetBuild(input.GetNumber(), r) - - c.JSON(http.StatusCreated, input) - - // send API call to set the status on the commit - err = scm.FromContext(c).Status(u, input, r.GetOrg(), r.GetName()) - if err != nil { - // nolint: lll // ignore long line length due to error message - logger.Errorf("unable to set commit status for build %s/%d: %v", r.GetFullName(), input.GetNumber(), err) - } - - // publish the build to the queue - go publishToQueue( - queue.FromGinContext(c), - database.FromContext(c), - p, - input, - r, - u, - ) -} - -// skipEmptyBuild checks if the build should be skipped due to it -// not containing any steps besides init or clone. -// -// nolint: goconst // ignore init and clone constants -func skipEmptyBuild(p *pipeline.Build) string { - if len(p.Stages) == 1 { - if p.Stages[0].Name == "init" { - return "skipping build since only init stage found" - } - } - - // nolint: gomnd // ignore magic number - if len(p.Stages) == 2 { - if p.Stages[0].Name == "init" && p.Stages[1].Name == "clone" { - return "skipping build since only init and clone stages found" - } - } - - if len(p.Steps) == 1 { - if p.Steps[0].Name == "init" { - return "skipping build since only init step found" - } - } - - // nolint: gomnd // ignore magic number - if len(p.Steps) == 2 { - if p.Steps[0].Name == "init" && p.Steps[1].Name == "clone" { - return "skipping build since only init and clone steps found" - } - } - - return "" -} - -// swagger:operation GET /api/v1/repos/{org}/{repo}/builds builds GetBuilds -// -// Get builds from the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: query -// name: event -// description: Filter by build event -// type: string -// enum: -// - push -// - pull_request -// - tag -// - deployment -// - comment -// - in: query -// name: commit -// description: Filter builds based on the commit hash -// type: string -// - in: query -// name: branch -// description: Filter builds by branch -// type: string -// - in: query -// name: status -// description: Filter by build status -// type: string -// enum: -// - canceled -// - error -// - failure -// - killed -// - pending -// - running -// - success -// - in: query -// name: page -// description: The page of results to retrieve -// type: integer -// default: 1 -// - in: query -// name: per_page -// description: How many results per page to return -// type: integer -// maximum: 100 -// default: 10 -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the builds -// schema: -// type: array -// items: -// "$ref": "#/definitions/Build" -// headers: -// X-Total-Count: -// description: Total number of results -// type: integer -// Link: -// description: see https://tools.ietf.org/html/rfc5988 -// type: string -// '400': -// description: Unable to retrieve the list of builds -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the list of builds -// schema: -// "$ref": "#/definitions/Error" - -// GetBuilds represents the API handler to capture a -// list of builds for a repo from the configured backend. -// -// nolint: funlen // ignore function length due to comments -func GetBuilds(c *gin.Context) { - // variables that will hold the build list, build list filters and total count - var ( - filters = map[string]interface{}{} - b []*library.Build - t int64 - ) - - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading builds for repo %s", r.GetFullName()) - - // capture the branch name parameter - branch := c.Query("branch") - // capture the event type parameter - event := c.Query("event") - // capture the status type parameter - status := c.Query("status") - // capture the commit hash parameter - commit := c.Query("commit") - - // check if branch filter was provided - if len(branch) > 0 { - // add branch to filters map - filters["branch"] = branch - } - // check if event filter was provided - if len(event) > 0 { - // verify the event provided is a valid event type - if event != constants.EventComment && event != constants.EventDeploy && - event != constants.EventPush && event != constants.EventPull && - event != constants.EventTag { - retErr := fmt.Errorf("unable to process event %s: invalid event type provided", event) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // add event to filters map - filters["event"] = event - } - // check if status filter was provided - if len(status) > 0 { - // verify the status provided is a valid status type - if status != constants.StatusCanceled && status != constants.StatusError && - status != constants.StatusFailure && status != constants.StatusKilled && - status != constants.StatusPending && status != constants.StatusRunning && - status != constants.StatusSuccess { - retErr := fmt.Errorf("unable to process status %s: invalid status type provided", status) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // add status to filters map - filters["status"] = status - } - - // check if commit hash filter was provided - if len(commit) > 0 { - // add commit to filters map - filters["commit"] = commit - } - - // capture page query parameter if present - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to convert page query parameter for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // capture per_page query parameter if present - perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to convert per_page query parameter for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // ensure per_page isn't above or below allowed values - // - // nolint: gomnd // ignore magic number - perPage = util.MaxInt(1, util.MinInt(100, perPage)) - - b, t, err = database.FromContext(c).GetRepoBuildList(r, filters, page, perPage) - if err != nil { - retErr := fmt.Errorf("unable to get builds for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // create pagination object - pagination := Pagination{ - Page: page, - PerPage: perPage, - Total: t, - } - // set pagination headers - pagination.SetHeaderLink(c) - - c.JSON(http.StatusOK, b) -} - -// swagger:operation GET /api/v1/repos/{org} builds GetOrgBuilds -// -// Get a list of builds by org in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: query -// name: page -// description: The page of results to retrieve -// type: integer -// default: 1 -// - in: query -// name: per_page -// description: How many results per page to return -// type: integer -// maximum: 100 -// default: 10 -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved build list -// schema: -// type: array -// items: -// "$ref": "#/definitions/Build" -// headers: -// X-Total-Count: -// description: Total number of results -// type: integer -// Link: -// description: see https://tools.ietf.org/html/rfc5988 -// type: string -// '400': -// description: Unable to retrieve the list of builds -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the list of builds -// schema: -// "$ref": "#/definitions/Error" - -// GetOrgBuilds represents the API handler to capture a -// list of builds associated with an org from the configured backend. -// -// nolint: funlen // ignore function length due to comments -func GetOrgBuilds(c *gin.Context) { - // variables that will hold the build list, build list filters and total count - var ( - filters = map[string]interface{}{} - b []*library.Build - t int64 - ) - - // capture middleware values - o := org.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "user": u.GetName(), - }).Infof("reading builds for org %s", o) - - // capture the branch name parameter - branch := c.Query("branch") - // capture the event type parameter - event := c.Query("event") - // capture the status type parameter - status := c.Query("status") - - // check if branch filter was provided - if len(branch) > 0 { - // add branch to filters map - filters["branch"] = branch - } - // check if event filter was provided - if len(event) > 0 { - // verify the event provided is a valid event type - if event != constants.EventComment && event != constants.EventDeploy && - event != constants.EventPush && event != constants.EventPull && - event != constants.EventTag { - retErr := fmt.Errorf("unable to process event %s: invalid event type provided", event) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // add event to filters map - filters["event"] = event - } - // check if status filter was provided - if len(status) > 0 { - // verify the status provided is a valid status type - if status != constants.StatusCanceled && status != constants.StatusError && - status != constants.StatusFailure && status != constants.StatusKilled && - status != constants.StatusPending && status != constants.StatusRunning && - status != constants.StatusSuccess { - retErr := fmt.Errorf("unable to process status %s: invalid status type provided", status) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // add status to filters map - filters["status"] = status - } - - // capture page query parameter if present - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) - if err != nil { - retErr := fmt.Errorf("unable to convert page query parameter for org %s: %w", o, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // capture per_page query parameter if present - perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) - if err != nil { - retErr := fmt.Errorf("unable to convert per_page query parameter for Org %s: %w", o, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // ensure per_page isn't above or below allowed values - // - // nolint: gomnd // ignore magic number - perPage = util.MaxInt(1, util.MinInt(100, perPage)) - - // See if the user is an org admin to bypass individual permission checks - perm, err := scm.FromContext(c).OrgAccess(u, o) - if err != nil { - logrus.Errorf("unable to get user %s access level for org %s", u.GetName(), o) - } - // Only show public repos to non-admins - // - // nolint: goconst // ignore admin constant - if perm != "admin" { - filters["visibility"] = constants.VisibilityPublic - } - - // send API call to capture the list of builds for the org (and event type if passed in) - b, t, err = database.FromContext(c).GetOrgBuildList(o, filters, page, perPage) - - if err != nil { - retErr := fmt.Errorf("unable to get builds for org %s: %w", o, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // create pagination object - pagination := Pagination{ - Page: page, - PerPage: perPage, - Total: t, - } - // set pagination headers - pagination.SetHeaderLink(c) - - c.JSON(http.StatusOK, b) -} - -// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build} builds GetBuild -// -// Get a build in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number to retrieve -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the build -// type: json -// schema: -// "$ref": "#/definitions/Build" - -// GetBuild represents the API handler to capture -// a build for a repo from the configured backend. -func GetBuild(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading build %s/%d", r.GetFullName(), b.GetNumber()) - - c.JSON(http.StatusOK, b) -} - -// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build} builds RestartBuild -// -// Restart a build in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number to restart -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Request processed but build was skipped -// schema: -// type: string -// '201': -// description: Successfully restarted the build -// schema: -// "$ref": "#/definitions/Build" -// '400': -// description: Unable to restart the build -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to restart the build -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to restart the build -// schema: -// "$ref": "#/definitions/Error" - -// RestartBuild represents the API handler to -// restart an existing build in the configured backend. -// -// nolint: funlen // ignore function length due to comments -func RestartBuild(c *gin.Context) { - // capture middleware values - m := c.MustGet("metadata").(*types.Metadata) - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logger := logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }) - - logger.Infof("restarting build %s", entry) - - // send API call to capture the repo owner - u, err := database.FromContext(c).GetUser(r.GetUserID()) - if err != nil { - retErr := fmt.Errorf("unable to get owner for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // create SQL filters for querying pending and running builds for repo - filters := map[string]interface{}{ - "status": []string{constants.StatusPending, constants.StatusRunning}, - } - - // send API call to capture the number of pending or running builds for the repo - builds, err := database.FromContext(c).GetRepoBuildCount(r, filters) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to restart build: unable to get count of builds for repo %s", r.GetFullName()) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // check if the number of pending and running builds exceeds the limit for the repo - if builds >= r.GetBuildLimit() { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to restart build: repo %s has exceeded the concurrent build limit of %d", r.GetFullName(), r.GetBuildLimit()) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // send API call to capture the last build for the repo - lastBuild, err := database.FromContext(c).GetLastBuild(r) - if err != nil { - retErr := fmt.Errorf("unable to get last build for %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // update the build numbers based off repo counter - inc := r.GetCounter() + 1 - - r.SetCounter(inc) - b.SetNumber(inc) - - // update fields in build object - b.SetID(0) - b.SetParent(lastBuild.GetNumber()) - b.SetCreated(time.Now().UTC().Unix()) - b.SetEnqueued(0) - b.SetStarted(0) - b.SetFinished(0) - b.SetStatus(constants.StatusPending) - b.SetHost("") - b.SetRuntime("") - b.SetDistribution("") - - // populate the build link if a web address is provided - if len(m.Vela.WebAddress) > 0 { - b.SetLink( - fmt.Sprintf("%s/%s/%d", m.Vela.WebAddress, r.GetFullName(), b.GetNumber()), - ) - } - - // variable to store changeset files - var files []string - // check if the build event is not pull_request - if !strings.EqualFold(b.GetEvent(), constants.EventPull) { - // send API call to capture list of files changed for the commit - files, err = scm.FromContext(c).Changeset(u, r, b.GetCommit()) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to process webhook: failed to get changeset for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - } - - // handle getting changeset from a pull_request - if strings.EqualFold(b.GetEvent(), constants.EventPull) { - // capture number from build - number, err := getPRNumberFromBuild(b) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to restart build: failed to get pull_request number for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture list of files changed for the pull request - files, err = scm.FromContext(c).ChangesetPR(u, r, number) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to restart build: failed to get changeset for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - } - - // send API call to capture the pipeline configuration file - config, err := scm.FromContext(c).ConfigBackoff(u, r, b.GetCommit()) - if err != nil { - retErr := fmt.Errorf("unable to get pipeline configuration for %s: %w", entry, err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - // parse and compile the pipeline configuration file - p, err := compiler.FromContext(c). - WithBuild(b). - WithFiles(files). - WithMetadata(m). - WithRepo(r). - WithUser(u). - Compile(config) - if err != nil { - retErr := fmt.Errorf("unable to compile pipeline configuration for %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // skip the build if only the init or clone steps are found - skip := skipEmptyBuild(p) - if skip != "" { - // set build to successful status - b.SetStatus(constants.StatusSkipped) - - // send API call to set the status on the commit - err = scm.FromContext(c).Status(u, b, r.GetOrg(), r.GetName()) - if err != nil { - logger.Errorf("unable to set commit status for %s: %v", entry, err) - } - - c.JSON(http.StatusOK, skip) - return - } - - // create the objects from the pipeline in the database - err = planBuild(database.FromContext(c), p, b, r) - if err != nil { - util.HandleError(c, http.StatusInternalServerError, err) - - return - } - - // send API call to update repo for ensuring counter is incremented - err = database.FromContext(c).UpdateRepo(r) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to restart build: failed to update repo %s: %v", r.GetFullName(), err) - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // send API call to capture the restarted build - b, _ = database.FromContext(c).GetBuild(b.GetNumber(), r) - - c.JSON(http.StatusCreated, b) - - // send API call to set the status on the commit - err = scm.FromContext(c).Status(u, b, r.GetOrg(), r.GetName()) - if err != nil { - logger.Errorf("unable to set commit status for build %s: %v", entry, err) - } - - // publish the build to the queue - go publishToQueue( - queue.FromGinContext(c), - database.FromContext(c), - p, - b, - r, - u, - ) -} - -// swagger:operation PUT /api/v1/repos/{org}/{repo}/builds/{build} builds UpdateBuild -// -// Updates a build in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number to update -// required: true -// type: integer -// - in: body -// name: body -// description: Payload containing the build to update -// required: true -// schema: -// "$ref": "#/definitions/Build" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully updated the build -// schema: -// "$ref": "#/definitions/Build" -// '404': -// description: Unable to update the build -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to update the build -// schema: -// "$ref": "#/definitions/Error" - -// UpdateBuild represents the API handler to update -// a build for a repo in the configured backend. -// nolint: funlen // ignore long function line length -func UpdateBuild(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("updating build %s", entry) - - // capture body from API request - input := new(library.Build) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for build %s: %w", entry, err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - // update build fields if provided - if len(input.GetStatus()) > 0 { - // update status if set - b.SetStatus(input.GetStatus()) - } - - if len(input.GetError()) > 0 { - // update error if set - b.SetError(input.GetError()) - } - - if input.GetEnqueued() > 0 { - // update enqueued if set - b.SetEnqueued(input.GetEnqueued()) - } - - if input.GetStarted() > 0 { - // update started if set - b.SetStarted(input.GetStarted()) - } - - if input.GetFinished() > 0 { - // update finished if set - b.SetFinished(input.GetFinished()) - } - - if len(input.GetTitle()) > 0 { - // update title if set - b.SetTitle(input.GetTitle()) - } - - if len(input.GetMessage()) > 0 { - // update message if set - b.SetMessage(input.GetMessage()) - } - - if len(input.GetHost()) > 0 { - // update host if set - b.SetHost(input.GetHost()) - } - - if len(input.GetRuntime()) > 0 { - // update runtime if set - b.SetRuntime(input.GetRuntime()) - } - - if len(input.GetDistribution()) > 0 { - // update distribution if set - b.SetDistribution(input.GetDistribution()) - } - - // send API call to update the build - err = database.FromContext(c).UpdateBuild(b) - if err != nil { - retErr := fmt.Errorf("unable to update build %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated build - b, _ = database.FromContext(c).GetBuild(b.GetNumber(), r) - - c.JSON(http.StatusOK, b) - - // check if the build is in a "final" state - if b.GetStatus() == constants.StatusSuccess || - b.GetStatus() == constants.StatusFailure || - b.GetStatus() == constants.StatusCanceled || - b.GetStatus() == constants.StatusKilled || - b.GetStatus() == constants.StatusError { - // send API call to capture the repo owner - u, err := database.FromContext(c).GetUser(r.GetUserID()) - if err != nil { - logrus.Errorf("unable to get owner for build %s: %v", entry, err) - } - - // send API call to set the status on the commit - err = scm.FromContext(c).Status(u, b, r.GetOrg(), r.GetName()) - if err != nil { - logrus.Errorf("unable to set commit status for build %s: %v", entry, err) - } - } -} - -// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build} builds DeleteBuild -// -// Delete a build in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number to delete -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully deleted the build -// schema: -// type: string -// '400': -// description: Unable to delete the build -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to delete the build -// schema: -// "$ref": "#/definitions/Error" - -// DeleteBuild represents the API handler to remove -// a build for a repo from the configured backend. -func DeleteBuild(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("deleting build %s", entry) - - // send API call to remove the build - err := database.FromContext(c).DeleteBuild(b.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to delete build %s: %v", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, fmt.Sprintf("build %s deleted", entry)) -} - -// getPRNumberFromBuild is a helper function to -// extract the pull request number from a Build. -func getPRNumberFromBuild(b *library.Build) (int, error) { - // parse out pull request number from base ref - // - // pattern: refs/pull/1/head - var parts []string - if strings.HasPrefix(b.GetRef(), "refs/pull/") { - parts = strings.Split(b.GetRef(), "/") - } - - // just being safe to avoid out of range index errors - // - // nolint:gomnd // magic number of 3 used once - if len(parts) < 3 { - return 0, fmt.Errorf("invalid ref: %s", b.GetRef()) - } - - // return the results of converting number to string - return strconv.Atoi(parts[2]) -} - -// planBuild is a helper function to plan the build for -// execution. This creates all resources, like steps -// and services, for the build in the configured backend. -// -// nolint: lll // ignore long line length due to variable names -func planBuild(database database.Service, p *pipeline.Build, b *library.Build, r *library.Repo) error { - // update fields in build object - b.SetCreated(time.Now().UTC().Unix()) - - // send API call to create the build - err := database.CreateBuild(b) - if err != nil { - // clean up the objects from the pipeline in the database - cleanBuild(database, b, nil, nil) - - return fmt.Errorf("unable to create new build for %s: %v", r.GetFullName(), err) - } - - // send API call to capture the created build - b, _ = database.GetBuild(b.GetNumber(), r) - - // plan all services for the build - services, err := planServices(database, p, b) - if err != nil { - // clean up the objects from the pipeline in the database - cleanBuild(database, b, services, nil) - - return err - } - - // plan all steps for the build - steps, err := planSteps(database, p, b) - if err != nil { - // clean up the objects from the pipeline in the database - cleanBuild(database, b, services, steps) - - return err - } - - return nil -} - -// cleanBuild is a helper function to kill the build -// without execution. This will kill all resources, -// like steps and services, for the build in the -// configured backend. -// -// nolint: lll // ignore long line length due to variable names -func cleanBuild(database database.Service, b *library.Build, services []*library.Service, steps []*library.Step) { - // update fields in build object - b.SetError("unable to publish build to queue") - b.SetStatus(constants.StatusError) - b.SetFinished(time.Now().UTC().Unix()) - - // send API call to update the build - err := database.UpdateBuild(b) - if err != nil { - logrus.Errorf("unable to kill build %d: %v", b.GetNumber(), err) - } - - for _, s := range services { - // update fields in service object - s.SetStatus(constants.StatusKilled) - s.SetFinished(time.Now().UTC().Unix()) - - // send API call to update the service - err := database.UpdateService(s) - if err != nil { - logrus.Errorf("unable to kill service %s for build %d: %v", s.GetName(), b.GetNumber(), err) - } - } - - for _, s := range steps { - // update fields in step object - s.SetStatus(constants.StatusKilled) - s.SetFinished(time.Now().UTC().Unix()) - - // send API call to update the step - err := database.UpdateStep(s) - if err != nil { - logrus.Errorf("unable to kill step %s for build %d: %v", s.GetName(), b.GetNumber(), err) - } - } -} - -// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build}/cancel builds CancelBuild -// -// Cancel a running build -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: build -// description: Build number to cancel -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully canceled the build -// schema: -// type: string -// '400': -// description: Unable to cancel build -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to cancel build -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to cancel build -// schema: -// "$ref": "#/definitions/Error" - -// CancelBuild represents the API handler to -// cancel a running build. -// -// nolint: funlen // ignore function length due to comments -func CancelBuild(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - e := executors.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("canceling build %s", entry) - - // TODO: add support for removing builds from the queue - // - // check to see if build is not running - if !strings.EqualFold(b.GetStatus(), constants.StatusRunning) { - retErr := fmt.Errorf("found build %s but its status was %s", entry, b.GetStatus()) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // retrieve the worker info - w, err := database.FromContext(c).GetWorker(b.GetHost()) - if err != nil { - retErr := fmt.Errorf("unable to get worker for build %s: %w", entry, err) - util.HandleError(c, http.StatusNotFound, retErr) - return - } - - for _, executor := range e { - // check each executor on the worker running the build - // to see if it's running the build we want to cancel - // - // nolint:whitespace // ignore leading newline to improve readability - if strings.EqualFold(executor.Repo.GetFullName(), r.GetFullName()) && - *executor.GetBuild().Number == b.GetNumber() { - - // prepare the request to the worker - client := http.DefaultClient - client.Timeout = 30 * time.Second - - // set the API endpoint path we send the request to - u := fmt.Sprintf("%s/api/v1/executors/%d/build/cancel", w.GetAddress(), executor.GetID()) - req, err := http.NewRequest("DELETE", u, nil) - if err != nil { - retErr := fmt.Errorf("unable to form a request to %s: %w", u, err) - util.HandleError(c, http.StatusBadRequest, retErr) - return - } - - // add the token to authenticate to the worker - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.MustGet("secret").(string))) - - // perform the request to the worker - resp, err := client.Do(req) - if err != nil { - retErr := fmt.Errorf("unable to connect to %s: %w", u, err) - util.HandleError(c, http.StatusBadRequest, retErr) - return - } - defer resp.Body.Close() - - // Read Response Body - respBody, err := ioutil.ReadAll(resp.Body) - if err != nil { - retErr := fmt.Errorf("unable to read response from %s: %w", u, err) - util.HandleError(c, http.StatusBadRequest, retErr) - return - } - - err = json.Unmarshal(respBody, b) - if err != nil { - retErr := fmt.Errorf("unable to parse response from %s: %w", u, err) - util.HandleError(c, http.StatusBadRequest, retErr) - return - } - - c.JSON(resp.StatusCode, b) - return - } - } - - // build has been abandoned - // update the status in the build table - b.SetStatus(constants.StatusCanceled) - err = database.FromContext(c).UpdateBuild(b) - if err != nil { - retErr := fmt.Errorf("unable to update status for build %s: %w", entry, err) - util.HandleError(c, http.StatusInternalServerError, retErr) - return - } - - // retrieve the steps for the build from the step table - steps := []*library.Step{} - page := 1 - perPage := 100 - for page > 0 { - // retrieve build steps (per page) from the database - stepsPart, err := database.FromContext(c).GetBuildStepList(b, page, perPage) - if err != nil { - retErr := fmt.Errorf("unable to retrieve steps for build %s: %w", entry, err) - util.HandleError(c, http.StatusNotFound, retErr) - return - } - - // add page of steps to list steps - steps = append(steps, stepsPart...) - - // assume no more pages exist if under 100 results are returned - // - // nolint: gomnd // ignore magic number - if len(stepsPart) < 100 { - page = 0 - } else { - page++ - } - } - - // iterate over each step for the build - // setting anything running or pending to canceled - for _, step := range steps { - if step.GetStatus() == constants.StatusRunning || - step.GetStatus() == constants.StatusPending { - step.SetStatus(constants.StatusCanceled) - err = database.FromContext(c).UpdateStep(step) - if err != nil { - retErr := fmt.Errorf("unable to update step %s for build %s: %w", step.GetName(), entry, err) - util.HandleError(c, http.StatusNotFound, retErr) - return - } - } - } - - // retrieve the services for the build from the service table - services := []*library.Service{} - page = 1 - for page > 0 { - // retrieve build services (per page) from the database - servicesPart, err := database.FromContext(c).GetBuildServiceList(b, page, perPage) - if err != nil { - retErr := fmt.Errorf("unable to retrieve services for build %s: %w", entry, err) - util.HandleError(c, http.StatusNotFound, retErr) - return - } - - // add page of services to the list of services - services = append(services, servicesPart...) - - // assume no more pages exist if under 100 results are returned - // - // nolint: gomnd // ignore magic number - if len(servicesPart) < 100 { - page = 0 - } else { - page++ - } - } - - // iterate over each service for the build - // setting anything running or pending to canceled - for _, service := range services { - if service.GetStatus() == constants.StatusRunning || - service.GetStatus() == constants.StatusPending { - service.SetStatus(constants.StatusCanceled) - err = database.FromContext(c).UpdateService(service) - if err != nil { - retErr := fmt.Errorf("unable to update service %s for build %s: %w", - service.GetName(), - entry, - err, - ) - util.HandleError(c, http.StatusNotFound, retErr) - return - } - } - } - - c.JSON(http.StatusOK, b) -} diff --git a/api/build/cancel.go b/api/build/cancel.go new file mode 100644 index 000000000..55dc7cecf --- /dev/null +++ b/api/build/cancel.go @@ -0,0 +1,290 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/executors" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build}/cancel builds CancelBuild +// +// Cancel a running build +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: build +// description: Build number to cancel +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully canceled the build +// schema: +// type: string +// '400': +// description: Unable to cancel build +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to cancel build +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to cancel build +// schema: +// "$ref": "#/definitions/Error" + +// CancelBuild represents the API handler to cancel a running build. +// +//nolint:funlen // ignore statement count +func CancelBuild(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + e := executors.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("canceling build %s", entry) + + switch b.GetStatus() { + case constants.StatusRunning: + // retrieve the worker info + w, err := database.FromContext(c).GetWorkerForHostname(b.GetHost()) + if err != nil { + retErr := fmt.Errorf("unable to get worker for build %s: %w", entry, err) + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + for _, executor := range e { + // check each executor on the worker running the build to see if it's running the build we want to cancel + if strings.EqualFold(executor.Repo.GetFullName(), r.GetFullName()) && *executor.GetBuild().Number == b.GetNumber() { + // prepare the request to the worker + client := http.DefaultClient + client.Timeout = 30 * time.Second + + // set the API endpoint path we send the request to + u := fmt.Sprintf("%s/api/v1/executors/%d/build/cancel", w.GetAddress(), executor.GetID()) + + req, err := http.NewRequestWithContext(context.Background(), "DELETE", u, nil) + if err != nil { + retErr := fmt.Errorf("unable to form a request to %s: %w", u, err) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + tm := c.MustGet("token-manager").(*token.Manager) + + // set mint token options + mto := &token.MintTokenOpts{ + Hostname: "vela-server", + TokenType: constants.WorkerAuthTokenType, + TokenDuration: time.Minute * 1, + } + + // mint token + tkn, err := tm.MintToken(mto) + if err != nil { + retErr := fmt.Errorf("unable to generate auth token: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // add the token to authenticate to the worker + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) + + // perform the request to the worker + resp, err := client.Do(req) + if err != nil { + retErr := fmt.Errorf("unable to connect to %s: %w", u, err) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + defer resp.Body.Close() + + // Read Response Body + respBody, err := io.ReadAll(resp.Body) + if err != nil { + retErr := fmt.Errorf("unable to read response from %s: %w", u, err) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + err = json.Unmarshal(respBody, b) + if err != nil { + retErr := fmt.Errorf("unable to parse response from %s: %w", u, err) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + c.JSON(resp.StatusCode, b) + + return + } + } + case constants.StatusPending: + break + + default: + retErr := fmt.Errorf("found build %s but its status was %s", entry, b.GetStatus()) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // build has been abandoned + // update the status in the build table + b.SetStatus(constants.StatusCanceled) + + b, err := database.FromContext(c).UpdateBuild(ctx, b) + if err != nil { + retErr := fmt.Errorf("unable to update status for build %s: %w", entry, err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // retrieve the steps for the build from the step table + steps := []*library.Step{} + page := 1 + perPage := 100 + + for page > 0 { + // retrieve build steps (per page) from the database + stepsPart, _, err := database.FromContext(c).ListStepsForBuild(b, map[string]interface{}{}, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to retrieve steps for build %s: %w", entry, err) + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // add page of steps to list steps + steps = append(steps, stepsPart...) + + // assume no more pages exist if under 100 results are returned + if len(stepsPart) < 100 { + page = 0 + } else { + page++ + } + } + + // iterate over each step for the build + // setting anything running or pending to canceled + for _, step := range steps { + if step.GetStatus() == constants.StatusRunning || step.GetStatus() == constants.StatusPending { + step.SetStatus(constants.StatusCanceled) + + _, err = database.FromContext(c).UpdateStep(step) + if err != nil { + retErr := fmt.Errorf("unable to update step %s for build %s: %w", step.GetName(), entry, err) + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + } + } + + // retrieve the services for the build from the service table + services := []*library.Service{} + page = 1 + + for page > 0 { + // retrieve build services (per page) from the database + servicesPart, _, err := database.FromContext(c).ListServicesForBuild(b, map[string]interface{}{}, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to retrieve services for build %s: %w", entry, err) + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // add page of services to the list of services + services = append(services, servicesPart...) + + // assume no more pages exist if under 100 results are returned + if len(servicesPart) < 100 { + page = 0 + } else { + page++ + } + } + + // iterate over each service for the build + // setting anything running or pending to canceled + for _, service := range services { + if service.GetStatus() == constants.StatusRunning || service.GetStatus() == constants.StatusPending { + service.SetStatus(constants.StatusCanceled) + + _, err = database.FromContext(c).UpdateService(service) + if err != nil { + retErr := fmt.Errorf("unable to update service %s for build %s: %w", + service.GetName(), + entry, + err, + ) + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + } + } + + c.JSON(http.StatusOK, b) +} diff --git a/api/build/clean.go b/api/build/clean.go new file mode 100644 index 000000000..0dd9ea81e --- /dev/null +++ b/api/build/clean.go @@ -0,0 +1,57 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "fmt" + "time" + + "github.com/go-vela/server/database" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// cleanBuild is a helper function to kill the build +// without execution. This will kill all resources, +// like steps and services, for the build in the +// configured backend. +func CleanBuild(ctx context.Context, database database.Interface, b *library.Build, services []*library.Service, steps []*library.Step, e error) { + // update fields in build object + b.SetError(fmt.Sprintf("unable to publish to queue: %s", e.Error())) + b.SetStatus(constants.StatusError) + b.SetFinished(time.Now().UTC().Unix()) + + // send API call to update the build + b, err := database.UpdateBuild(ctx, b) + if err != nil { + logrus.Errorf("unable to kill build %d: %v", b.GetNumber(), err) + } + + for _, s := range services { + // update fields in service object + s.SetStatus(constants.StatusKilled) + s.SetFinished(time.Now().UTC().Unix()) + + // send API call to update the service + _, err := database.UpdateService(s) + if err != nil { + logrus.Errorf("unable to kill service %s for build %d: %v", s.GetName(), b.GetNumber(), err) + } + } + + for _, s := range steps { + // update fields in step object + s.SetStatus(constants.StatusKilled) + s.SetFinished(time.Now().UTC().Unix()) + + // send API call to update the step + _, err := database.UpdateStep(s) + if err != nil { + logrus.Errorf("unable to kill step %s for build %d: %v", s.GetName(), b.GetNumber(), err) + } + } +} diff --git a/api/build/create.go b/api/build/create.go new file mode 100644 index 000000000..ee0d0f64b --- /dev/null +++ b/api/build/create.go @@ -0,0 +1,383 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/compiler" + "github.com/go-vela/server/database" + "github.com/go-vela/server/queue" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/repos/{org}/{repo}/builds builds CreateBuild +// +// Create a build in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: body +// name: body +// description: Payload containing the build to create +// required: true +// schema: +// "$ref": "#/definitions/Build" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Request processed but build was skipped +// schema: +// type: string +// '201': +// description: Successfully created the build +// type: json +// schema: +// "$ref": "#/definitions/Build" +// '400': +// description: Unable to create the build +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to create the build +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the build +// schema: +// "$ref": "#/definitions/Error" + +// CreateBuild represents the API handler to create a build in the configured backend. +// +//nolint:funlen,gocyclo // ignore function length and cyclomatic complexity +func CreateBuild(c *gin.Context) { + // capture middleware values + m := c.MustGet("metadata").(*types.Metadata) + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logger := logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }) + + logger.Infof("creating new build for repo %s", r.GetFullName()) + + // capture body from API request + input := new(library.Build) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for new build for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // verify the build has a valid event and the repo allows that event type + if (input.GetEvent() == constants.EventPush && !r.GetAllowPush()) || + (input.GetEvent() == constants.EventPull && !r.GetAllowPull()) || + (input.GetEvent() == constants.EventTag && !r.GetAllowTag()) || + (input.GetEvent() == constants.EventDeploy && !r.GetAllowDeploy()) { + retErr := fmt.Errorf("unable to create new build: %s does not have %s events enabled", r.GetFullName(), input.GetEvent()) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture the repo owner + u, err = database.FromContext(c).GetUser(r.GetUserID()) + if err != nil { + retErr := fmt.Errorf("unable to get owner for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // create SQL filters for querying pending and running builds for repo + filters := map[string]interface{}{ + "status": []string{constants.StatusPending, constants.StatusRunning}, + } + + // send API call to capture the number of pending or running builds for the repo + builds, err := database.FromContext(c).CountBuildsForRepo(ctx, r, filters) + if err != nil { + retErr := fmt.Errorf("unable to create new build: unable to get count of builds for repo %s", r.GetFullName()) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // check if the number of pending and running builds exceeds the limit for the repo + if builds >= r.GetBuildLimit() { + retErr := fmt.Errorf("unable to create new build: repo %s has exceeded the concurrent build limit of %d", r.GetFullName(), r.GetBuildLimit()) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update fields in build object + input.SetRepoID(r.GetID()) + input.SetStatus(constants.StatusPending) + input.SetCreated(time.Now().UTC().Unix()) + + // set the parent equal to the current repo counter + input.SetParent(r.GetCounter()) + // check if the parent is set to 0 + if input.GetParent() == 0 { + // parent should be "1" if it's the first build ran + input.SetParent(1) + } + + // update the build numbers based off repo counter + inc := r.GetCounter() + 1 + r.SetCounter(inc) + input.SetNumber(inc) + + // populate the build link if a web address is provided + if len(m.Vela.WebAddress) > 0 { + input.SetLink( + fmt.Sprintf("%s/%s/%d", m.Vela.WebAddress, r.GetFullName(), input.GetNumber()), + ) + } + + // variable to store changeset files + var files []string + // check if the build event is not issue_comment or pull_request + if !strings.EqualFold(input.GetEvent(), constants.EventComment) && + !strings.EqualFold(input.GetEvent(), constants.EventPull) { + // send API call to capture list of files changed for the commit + files, err = scm.FromContext(c).Changeset(u, r, input.GetCommit()) + if err != nil { + retErr := fmt.Errorf("unable to create new build: failed to get changeset for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + // check if the build event is a pull_request + if strings.EqualFold(input.GetEvent(), constants.EventPull) { + // capture number from build + number, err := getPRNumberFromBuild(input) + if err != nil { + retErr := fmt.Errorf("unable to create new build: failed to get pull_request number for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // send API call to capture list of files changed for the pull request + files, err = scm.FromContext(c).ChangesetPR(u, r, number) + if err != nil { + retErr := fmt.Errorf("unable to create new build: failed to get changeset for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + var ( + // variable to store the raw pipeline configuration + config []byte + // variable to store executable pipeline + p *pipeline.Build + // variable to store pipeline configuration + pipeline *library.Pipeline + // variable to store the pipeline type for the repository + pipelineType = r.GetPipelineType() + ) + + // send API call to attempt to capture the pipeline + pipeline, err = database.FromContext(c).GetPipelineForRepo(ctx, input.GetCommit(), r) + if err != nil { // assume the pipeline doesn't exist in the database yet + // send API call to capture the pipeline configuration file + config, err = scm.FromContext(c).ConfigBackoff(u, r, input.GetCommit()) + if err != nil { + retErr := fmt.Errorf("unable to create new build: failed to get pipeline configuration for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + } else { + config = pipeline.GetData() + } + + // ensure we use the expected pipeline type when compiling + // + // The pipeline type for a repo can change at any time which can break compiling + // existing pipelines in the system for that repo. To account for this, we update + // the repo pipeline type to match what was defined for the existing pipeline + // before compiling. After we're done compiling, we reset the pipeline type. + if len(pipeline.GetType()) > 0 { + r.SetPipelineType(pipeline.GetType()) + } + + var compiled *library.Pipeline + // parse and compile the pipeline configuration file + p, compiled, err = compiler.FromContext(c). + Duplicate(). + WithBuild(input). + WithFiles(files). + WithMetadata(m). + WithRepo(r). + WithUser(u). + Compile(config) + if err != nil { + retErr := fmt.Errorf("unable to compile pipeline configuration for %s/%d: %w", r.GetFullName(), input.GetNumber(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + // reset the pipeline type for the repo + // + // The pipeline type for a repo can change at any time which can break compiling + // existing pipelines in the system for that repo. To account for this, we update + // the repo pipeline type to match what was defined for the existing pipeline + // before compiling. After we're done compiling, we reset the pipeline type. + r.SetPipelineType(pipelineType) + + // skip the build if only the init or clone steps are found + skip := SkipEmptyBuild(p) + if skip != "" { + // set build to successful status + input.SetStatus(constants.StatusSuccess) + + // send API call to set the status on the commit + err = scm.FromContext(c).Status(u, input, r.GetOrg(), r.GetName()) + if err != nil { + logger.Errorf("unable to set commit status for %s/%d: %v", r.GetFullName(), input.GetNumber(), err) + } + + c.JSON(http.StatusOK, skip) + + return + } + + // check if the pipeline did not already exist in the database + // + //nolint:dupl // ignore duplicate code + if pipeline == nil { + pipeline = compiled + pipeline.SetRepoID(r.GetID()) + pipeline.SetCommit(input.GetCommit()) + pipeline.SetRef(input.GetRef()) + + // send API call to create the pipeline + pipeline, err = database.FromContext(c).CreatePipeline(ctx, pipeline) + if err != nil { + retErr := fmt.Errorf("unable to create new build: failed to create pipeline for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + } + + input.SetPipelineID(pipeline.GetID()) + + // create the objects from the pipeline in the database + err = PlanBuild(ctx, database.FromContext(c), p, input, r) + if err != nil { + util.HandleError(c, http.StatusInternalServerError, err) + + return + } + + // send API call to update repo for ensuring counter is incremented + r, err = database.FromContext(c).UpdateRepo(ctx, r) + if err != nil { + retErr := fmt.Errorf("unable to create new build: failed to update repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture the created build + input, _ = database.FromContext(c).GetBuildForRepo(ctx, r, input.GetNumber()) + + c.JSON(http.StatusCreated, input) + + // send API call to set the status on the commit + err = scm.FromContext(c).Status(u, input, r.GetOrg(), r.GetName()) + if err != nil { + logger.Errorf("unable to set commit status for build %s/%d: %v", r.GetFullName(), input.GetNumber(), err) + } + + // publish the build to the queue + go PublishToQueue( + ctx, + queue.FromGinContext(c), + database.FromContext(c), + p, + input, + r, + u, + ) +} + +// getPRNumberFromBuild is a helper function to +// extract the pull request number from a Build. +func getPRNumberFromBuild(b *library.Build) (int, error) { + // parse out pull request number from base ref + // + // pattern: refs/pull/1/head + var parts []string + if strings.HasPrefix(b.GetRef(), "refs/pull/") { + parts = strings.Split(b.GetRef(), "/") + } + + // just being safe to avoid out of range index errors + if len(parts) < 3 { + return 0, fmt.Errorf("invalid ref: %s", b.GetRef()) + } + + // return the results of converting number to string + return strconv.Atoi(parts[2]) +} diff --git a/api/build/delete.go b/api/build/delete.go new file mode 100644 index 000000000..ffc9a7ac4 --- /dev/null +++ b/api/build/delete.go @@ -0,0 +1,93 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build} builds DeleteBuild +// +// Delete a build in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number to delete +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted the build +// schema: +// type: string +// '400': +// description: Unable to delete the build +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to delete the build +// schema: +// "$ref": "#/definitions/Error" + +// DeleteBuild represents the API handler to remove +// a build for a repo from the configured backend. +func DeleteBuild(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("deleting build %s", entry) + + // send API call to remove the build + err := database.FromContext(c).DeleteBuild(ctx, b) + if err != nil { + retErr := fmt.Errorf("unable to delete build %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("build %s deleted", entry)) +} diff --git a/api/build/doc.go b/api/build/doc.go new file mode 100644 index 000000000..94b6571e3 --- /dev/null +++ b/api/build/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package build provides the build handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/build" +package build diff --git a/api/build/executable.go b/api/build/executable.go new file mode 100644 index 000000000..9df63467d --- /dev/null +++ b/api/build/executable.go @@ -0,0 +1,94 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/claims" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/executable builds GetBuildExecutable +// +// Get a build executable in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number to retrieve +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the build executable +// type: json +// schema: +// "$ref": "#/definitions/Build" +// '400': +// description: Bad request +// schema: +// "$ref": "#/definitions/Error" +// '401': +// description: Unauthorized +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Could not retrieve build executable +// schema: +// "$ref": "#/definitions/Error" + +// GetBuildExecutable represents the API handler to capture +// a build executable for a repo from the configured backend. +func GetBuildExecutable(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + cl := claims.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "subject": cl.Subject, + }).Infof("reading build executable %s/%d", r.GetFullName(), b.GetNumber()) + + bExecutable, err := database.FromContext(c).PopBuildExecutable(ctx, b.GetID()) + if err != nil { + retErr := fmt.Errorf("unable to pop build executable: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, bExecutable) +} diff --git a/api/build/get.go b/api/build/get.go new file mode 100644 index 000000000..7e476441e --- /dev/null +++ b/api/build/get.go @@ -0,0 +1,70 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build} builds GetBuild +// +// Get a build in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number to retrieve +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the build +// type: json +// schema: +// "$ref": "#/definitions/Build" + +// GetBuild represents the API handler to capture +// a build for a repo from the configured backend. +func GetBuild(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("reading build %s/%d", r.GetFullName(), b.GetNumber()) + + c.JSON(http.StatusOK, b) +} diff --git a/api/build/get_id.go b/api/build/get_id.go new file mode 100644 index 000000000..439cd9b81 --- /dev/null +++ b/api/build/get_id.go @@ -0,0 +1,119 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/search/builds/{id} builds GetBuildByID +// +// Get a single build by its id in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: id +// description: build id +// required: true +// type: number +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved build +// schema: +// "$ref": "#/definitions/Build" +// '400': +// description: Unable to retrieve the build +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the build +// schema: +// "$ref": "#/definitions/Error" + +// GetBuildByID represents the API handler to capture a +// build by its id from the configured backend. +func GetBuildByID(c *gin.Context) { + // Variables that will hold the library types of the build and repo + var ( + b *library.Build + r *library.Repo + ) + + // Capture user from middleware + u := user.Retrieve(c) + ctx := c.Request.Context() + + // Parse build ID from path + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + + if err != nil { + retErr := fmt.Errorf("unable to parse build id: %w", err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": id, + "user": u.GetName(), + }).Infof("reading build %d", id) + + // Get build from database + b, err = database.FromContext(c).GetBuild(ctx, id) + if err != nil { + retErr := fmt.Errorf("unable to get build: %w", err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // Get repo from database using repo ID field from build + r, err = database.FromContext(c).GetRepo(ctx, b.GetRepoID()) + if err != nil { + retErr := fmt.Errorf("unable to get repo: %w", err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // Capture user access from SCM. We do this in order to ensure user has access and is not + // just retrieving any build using a random id number. + perm, err := scm.FromContext(c).RepoAccess(u, u.GetToken(), r.GetOrg(), r.GetName()) + if err != nil { + logrus.Errorf("unable to get user %s access level for repo %s", u.GetName(), r.GetFullName()) + } + + // Ensure that user has at least read access to repo to return the build + if perm == "none" && !u.GetAdmin() { + retErr := fmt.Errorf("unable to retrieve build %d: user does not have read access to repo", id) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + c.JSON(http.StatusOK, b) +} diff --git a/api/build/list_org.go b/api/build/list_org.go new file mode 100644 index 000000000..3ab3bf9d3 --- /dev/null +++ b/api/build/list_org.go @@ -0,0 +1,223 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/builds builds ListBuildsForOrg +// +// Get a list of builds by org in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: query +// name: event +// description: Filter by build event +// type: string +// enum: +// - comment +// - deployment +// - pull_request +// - push +// - schedule +// - tag +// - in: query +// name: branch +// description: Filter builds by branch +// type: string +// - in: query +// name: status +// description: Filter by build status +// type: string +// enum: +// - canceled +// - error +// - failure +// - killed +// - pending +// - running +// - success +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved build list +// schema: +// type: array +// items: +// "$ref": "#/definitions/Build" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve the list of builds +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the list of builds +// schema: +// "$ref": "#/definitions/Error" + +// ListBuildsForOrg represents the API handler to capture a +// list of builds associated with an org from the configured backend. +func ListBuildsForOrg(c *gin.Context) { + // variables that will hold the build list, build list filters and total count + var ( + filters = map[string]interface{}{} + b []*library.Build + t int64 + ) + + // capture middleware values + o := org.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "user": u.GetName(), + }).Infof("listing builds for org %s", o) + + // capture the branch name parameter + branch := c.Query("branch") + // capture the event type parameter + event := c.Query("event") + // capture the status type parameter + status := c.Query("status") + + // check if branch filter was provided + if len(branch) > 0 { + // add branch to filters map + filters["branch"] = branch + } + // check if event filter was provided + if len(event) > 0 { + // verify the event provided is a valid event type + if event != constants.EventComment && event != constants.EventDeploy && + event != constants.EventPush && event != constants.EventPull && + event != constants.EventTag && event != constants.EventSchedule { + retErr := fmt.Errorf("unable to process event %s: invalid event type provided", event) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // add event to filters map + filters["event"] = event + } + // check if status filter was provided + if len(status) > 0 { + // verify the status provided is a valid status type + if status != constants.StatusCanceled && status != constants.StatusError && + status != constants.StatusFailure && status != constants.StatusKilled && + status != constants.StatusPending && status != constants.StatusRunning && + status != constants.StatusSuccess { + retErr := fmt.Errorf("unable to process status %s: invalid status type provided", status) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // add status to filters map + filters["status"] = status + } + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + retErr := fmt.Errorf("unable to convert page query parameter for org %s: %w", o, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for Org %s: %w", o, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // See if the user is an org admin to bypass individual permission checks + perm, err := scm.FromContext(c).OrgAccess(u, o) + if err != nil { + logrus.Errorf("unable to get user %s access level for org %s", u.GetName(), o) + } + // Only show public repos to non-admins + if perm != "admin" { + filters["visibility"] = constants.VisibilityPublic + } + + // send API call to capture the list of builds for the org (and event type if passed in) + b, t, err = database.FromContext(c).ListBuildsForOrg(ctx, o, filters, page, perPage) + + if err != nil { + retErr := fmt.Errorf("unable to list builds for org %s: %w", o, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, b) +} diff --git a/api/build/list_repo.go b/api/build/list_repo.go new file mode 100644 index 000000000..1fb701dc5 --- /dev/null +++ b/api/build/list_repo.go @@ -0,0 +1,261 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds builds ListBuildsForRepo +// +// Get builds from the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: query +// name: event +// description: Filter by build event +// type: string +// enum: +// - comment +// - deployment +// - pull_request +// - push +// - schedule +// - tag +// - in: query +// name: commit +// description: Filter builds based on the commit hash +// type: string +// - in: query +// name: branch +// description: Filter builds by branch +// type: string +// - in: query +// name: status +// description: Filter by build status +// type: string +// enum: +// - canceled +// - error +// - failure +// - killed +// - pending +// - running +// - success +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// - in: query +// name: before +// description: filter builds created before a certain time +// type: integer +// default: 1 +// - in: query +// name: after +// description: filter builds created after a certain time +// type: integer +// default: 0 +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the builds +// schema: +// type: array +// items: +// "$ref": "#/definitions/Build" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve the list of builds +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the list of builds +// schema: +// "$ref": "#/definitions/Error" + +// ListBuildsForRepo represents the API handler to capture a +// list of builds for a repo from the configured backend. +func ListBuildsForRepo(c *gin.Context) { + // variables that will hold the build list, build list filters and total count + var ( + filters = map[string]interface{}{} + b []*library.Build + t int64 + ) + + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("listing builds for repo %s", r.GetFullName()) + + // capture the branch name parameter + branch := c.Query("branch") + // capture the event type parameter + event := c.Query("event") + // capture the status type parameter + status := c.Query("status") + // capture the commit hash parameter + commit := c.Query("commit") + + // check if branch filter was provided + if len(branch) > 0 { + // add branch to filters map + filters["branch"] = branch + } + // check if event filter was provided + if len(event) > 0 { + // verify the event provided is a valid event type + if event != constants.EventComment && event != constants.EventDeploy && + event != constants.EventPush && event != constants.EventPull && + event != constants.EventTag && event != constants.EventSchedule { + retErr := fmt.Errorf("unable to process event %s: invalid event type provided", event) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // add event to filters map + filters["event"] = event + } + // check if status filter was provided + if len(status) > 0 { + // verify the status provided is a valid status type + if status != constants.StatusCanceled && status != constants.StatusError && + status != constants.StatusFailure && status != constants.StatusKilled && + status != constants.StatusPending && status != constants.StatusRunning && + status != constants.StatusSuccess { + retErr := fmt.Errorf("unable to process status %s: invalid status type provided", status) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // add status to filters map + filters["status"] = status + } + + // check if commit hash filter was provided + if len(commit) > 0 { + // add commit to filters map + filters["commit"] = commit + } + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + retErr := fmt.Errorf("unable to convert page query parameter for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // capture before query parameter if present, default to now + before, err := strconv.ParseInt(c.DefaultQuery("before", strconv.FormatInt(time.Now().UTC().Unix(), 10)), 10, 64) + if err != nil { + retErr := fmt.Errorf("unable to convert before query parameter for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture after query parameter if present, default to 0 + after, err := strconv.ParseInt(c.DefaultQuery("after", "0"), 10, 64) + if err != nil { + retErr := fmt.Errorf("unable to convert after query parameter for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + b, t, err = database.FromContext(c).ListBuildsForRepo(ctx, r, filters, before, after, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to list builds for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, b) +} diff --git a/api/build/plan.go b/api/build/plan.go new file mode 100644 index 000000000..13080b40c --- /dev/null +++ b/api/build/plan.go @@ -0,0 +1,63 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "fmt" + "time" + + "github.com/go-vela/server/api/service" + "github.com/go-vela/server/api/step" + "github.com/go-vela/server/database" + "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" +) + +// PlanBuild is a helper function to plan the build for +// execution. This creates all resources, like steps +// and services, for the build in the configured backend. +// TODO: +// - return build and error. +func PlanBuild(ctx context.Context, database database.Interface, p *pipeline.Build, b *library.Build, r *library.Repo) error { + // update fields in build object + b.SetCreated(time.Now().UTC().Unix()) + + // send API call to create the build + // TODO: return created build and error instead of just error + b, err := database.CreateBuild(ctx, b) + if err != nil { + // clean up the objects from the pipeline in the database + // TODO: + // - even if it was created, we need to get the new build id + // otherwise it will be 0, which attempts to INSERT instead + // of UPDATE-ing the existing build - which results in + // a constraint error (repo_id, number) + // - do we want to update the build or just delete it? + CleanBuild(ctx, database, b, nil, nil, err) + + return fmt.Errorf("unable to create new build for %s: %w", r.GetFullName(), err) + } + + // plan all services for the build + services, err := service.PlanServices(database, p, b) + if err != nil { + // clean up the objects from the pipeline in the database + CleanBuild(ctx, database, b, services, nil, err) + + return err + } + + // plan all steps for the build + steps, err := step.PlanSteps(database, p, b) + if err != nil { + // clean up the objects from the pipeline in the database + CleanBuild(ctx, database, b, services, steps, err) + + return err + } + + return nil +} diff --git a/api/build/publish.go b/api/build/publish.go new file mode 100644 index 000000000..51f94cff2 --- /dev/null +++ b/api/build/publish.go @@ -0,0 +1,98 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "encoding/json" + "time" + + "github.com/go-vela/server/database" + "github.com/go-vela/server/queue" + "github.com/go-vela/types" + "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" + "github.com/sirupsen/logrus" +) + +// PublishToQueue is a helper function that creates +// a build item and publishes it to the queue. +func PublishToQueue(ctx context.Context, queue queue.Service, db database.Interface, p *pipeline.Build, b *library.Build, r *library.Repo, u *library.User) { + byteExecutable, err := json.Marshal(p) + if err != nil { + logrus.Errorf("Failed to marshal build executable %d for %s: %v", b.GetNumber(), r.GetFullName(), err) + + // error out the build + CleanBuild(ctx, db, b, nil, nil, err) + + return + } + + bExecutable := new(library.BuildExecutable) + bExecutable.SetBuildID(b.GetID()) + bExecutable.SetData(byteExecutable) + + err = db.CreateBuildExecutable(ctx, bExecutable) + if err != nil { + logrus.Errorf("Failed to publish build executable to database %d for %s: %v", b.GetNumber(), r.GetFullName(), err) + + // error out the build + CleanBuild(ctx, db, b, nil, nil, err) + + return + } + + item := types.ToItem(b, r, u) + + logrus.Infof("Converting queue item to json for build %d for %s", b.GetNumber(), r.GetFullName()) + + byteItem, err := json.Marshal(item) + if err != nil { + logrus.Errorf("Failed to convert item to json for build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) + + // error out the build + CleanBuild(ctx, db, b, nil, nil, err) + + return + } + + logrus.Infof("Establishing route for build %d for %s", b.GetNumber(), r.GetFullName()) + + route, err := queue.Route(&p.Worker) + if err != nil { + logrus.Errorf("unable to set route for build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) + + // error out the build + CleanBuild(ctx, db, b, nil, nil, err) + + return + } + + logrus.Infof("Publishing item for build %d for %s to queue %s", b.GetNumber(), r.GetFullName(), route) + + err = queue.Push(context.Background(), route, byteItem) + if err != nil { + logrus.Errorf("Retrying; Failed to publish build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) + + err = queue.Push(context.Background(), route, byteItem) + if err != nil { + logrus.Errorf("Failed to publish build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) + + // error out the build + CleanBuild(ctx, db, b, nil, nil, err) + + return + } + } + + // update fields in build object + b.SetEnqueued(time.Now().UTC().Unix()) + + // update the build in the db to reflect the time it was enqueued + _, err = db.UpdateBuild(ctx, b) + if err != nil { + logrus.Errorf("Failed to update build %d during publish to queue for %s: %v", b.GetNumber(), r.GetFullName(), err) + } +} diff --git a/api/build/restart.go b/api/build/restart.go new file mode 100644 index 000000000..804ad88bd --- /dev/null +++ b/api/build/restart.go @@ -0,0 +1,353 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/compiler" + "github.com/go-vela/server/database" + "github.com/go-vela/server/queue" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/claims" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build} builds RestartBuild +// +// Restart a build in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number to restart +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Request processed but build was skipped +// schema: +// type: string +// '201': +// description: Successfully restarted the build +// schema: +// "$ref": "#/definitions/Build" +// '400': +// description: Unable to restart the build +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to restart the build +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to restart the build +// schema: +// "$ref": "#/definitions/Error" + +// RestartBuild represents the API handler to restart an existing build in the configured backend. +// +//nolint:funlen // ignore statement count +func RestartBuild(c *gin.Context) { + // capture middleware values + m := c.MustGet("metadata").(*types.Metadata) + cl := claims.Retrieve(c) + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logger := logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }) + + logger.Infof("restarting build %s", entry) + + // send API call to capture the repo owner + u, err := database.FromContext(c).GetUser(r.GetUserID()) + if err != nil { + retErr := fmt.Errorf("unable to get owner for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // create SQL filters for querying pending and running builds for repo + filters := map[string]interface{}{ + "status": []string{constants.StatusPending, constants.StatusRunning}, + } + + // send API call to capture the number of pending or running builds for the repo + builds, err := database.FromContext(c).CountBuildsForRepo(ctx, r, filters) + if err != nil { + retErr := fmt.Errorf("unable to restart build: unable to get count of builds for repo %s", r.GetFullName()) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // check if the number of pending and running builds exceeds the limit for the repo + if builds >= r.GetBuildLimit() { + retErr := fmt.Errorf("unable to restart build: repo %s has exceeded the concurrent build limit of %d", r.GetFullName(), r.GetBuildLimit()) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update fields in build object + b.SetID(0) + b.SetCreated(time.Now().UTC().Unix()) + b.SetEnqueued(0) + b.SetStarted(0) + b.SetFinished(0) + b.SetStatus(constants.StatusPending) + b.SetHost("") + b.SetRuntime("") + b.SetDistribution("") + b.SetSender(cl.Subject) + + // update the PR event action if action was never set + // for backwards compatibility with pre-0.14 releases. + if b.GetEvent() == constants.EventPull && b.GetEventAction() == "" { + // technically, the action could have been opened or synchronize. + // will not affect behavior of the pipeline since we did not + // support actions for builds where this would be the case. + b.SetEventAction(constants.ActionOpened) + } + + // set the parent equal to the restarted build number + b.SetParent(b.GetNumber()) + // update the build numbers based off repo counter + inc := r.GetCounter() + 1 + r.SetCounter(inc) + b.SetNumber(inc) + + // populate the build link if a web address is provided + if len(m.Vela.WebAddress) > 0 { + b.SetLink( + fmt.Sprintf("%s/%s/%d", m.Vela.WebAddress, r.GetFullName(), b.GetNumber()), + ) + } + + // variable to store changeset files + var files []string + // check if the build event is not issue_comment or pull_request + if !strings.EqualFold(b.GetEvent(), constants.EventComment) && + !strings.EqualFold(b.GetEvent(), constants.EventPull) { + // send API call to capture list of files changed for the commit + files, err = scm.FromContext(c).Changeset(u, r, b.GetCommit()) + if err != nil { + retErr := fmt.Errorf("unable to restart build: failed to get changeset for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + // check if the build event is a pull_request + if strings.EqualFold(b.GetEvent(), constants.EventPull) { + // capture number from build + number, err := getPRNumberFromBuild(b) + if err != nil { + retErr := fmt.Errorf("unable to restart build: failed to get pull_request number for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // send API call to capture list of files changed for the pull request + files, err = scm.FromContext(c).ChangesetPR(u, r, number) + if err != nil { + retErr := fmt.Errorf("unable to restart build: failed to get changeset for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + // variables to store pipeline configuration + var ( + // variable to store the raw pipeline configuration + config []byte + // variable to store executable pipeline + p *pipeline.Build + // variable to store pipeline configuration + pipeline *library.Pipeline + // variable to store the pipeline type for the repository + pipelineType = r.GetPipelineType() + ) + + // send API call to attempt to capture the pipeline + pipeline, err = database.FromContext(c).GetPipelineForRepo(ctx, b.GetCommit(), r) + if err != nil { // assume the pipeline doesn't exist in the database yet (before pipeline support was added) + // send API call to capture the pipeline configuration file + config, err = scm.FromContext(c).ConfigBackoff(u, r, b.GetCommit()) + if err != nil { + retErr := fmt.Errorf("unable to get pipeline configuration for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + } else { + config = pipeline.GetData() + } + + // ensure we use the expected pipeline type when compiling + // + // The pipeline type for a repo can change at any time which can break compiling + // existing pipelines in the system for that repo. To account for this, we update + // the repo pipeline type to match what was defined for the existing pipeline + // before compiling. After we're done compiling, we reset the pipeline type. + if len(pipeline.GetType()) > 0 { + r.SetPipelineType(pipeline.GetType()) + } + + var compiled *library.Pipeline + // parse and compile the pipeline configuration file + p, compiled, err = compiler.FromContext(c). + Duplicate(). + WithBuild(b). + WithFiles(files). + WithMetadata(m). + WithRepo(r). + WithUser(u). + Compile(config) + if err != nil { + retErr := fmt.Errorf("unable to compile pipeline configuration for %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + // reset the pipeline type for the repo + // + // The pipeline type for a repo can change at any time which can break compiling + // existing pipelines in the system for that repo. To account for this, we update + // the repo pipeline type to match what was defined for the existing pipeline + // before compiling. After we're done compiling, we reset the pipeline type. + r.SetPipelineType(pipelineType) + + // skip the build if only the init or clone steps are found + skip := SkipEmptyBuild(p) + if skip != "" { + // set build to successful status + b.SetStatus(constants.StatusSkipped) + + // send API call to set the status on the commit + err = scm.FromContext(c).Status(u, b, r.GetOrg(), r.GetName()) + if err != nil { + logrus.Errorf("unable to set commit status for %s/%d: %v", r.GetFullName(), b.GetNumber(), err) + } + + c.JSON(http.StatusOK, skip) + + return + } + + // check if the pipeline did not already exist in the database + // + //nolint:dupl // ignore duplicate code + if pipeline == nil { + pipeline = compiled + pipeline.SetRepoID(r.GetID()) + pipeline.SetCommit(b.GetCommit()) + pipeline.SetRef(b.GetRef()) + + // send API call to create the pipeline + pipeline, err = database.FromContext(c).CreatePipeline(ctx, pipeline) + if err != nil { + retErr := fmt.Errorf("unable to create pipeline for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + } + + b.SetPipelineID(pipeline.GetID()) + + // create the objects from the pipeline in the database + err = PlanBuild(ctx, database.FromContext(c), p, b, r) + if err != nil { + util.HandleError(c, http.StatusInternalServerError, err) + + return + } + + // send API call to update repo for ensuring counter is incremented + r, err = database.FromContext(c).UpdateRepo(ctx, r) + if err != nil { + retErr := fmt.Errorf("unable to restart build: failed to update repo %s: %w", r.GetFullName(), err) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture the restarted build + b, _ = database.FromContext(c).GetBuildForRepo(ctx, r, b.GetNumber()) + + c.JSON(http.StatusCreated, b) + + // send API call to set the status on the commit + err = scm.FromContext(c).Status(u, b, r.GetOrg(), r.GetName()) + if err != nil { + logger.Errorf("unable to set commit status for build %s: %v", entry, err) + } + + // publish the build to the queue + go PublishToQueue( + ctx, + queue.FromGinContext(c), + database.FromContext(c), + p, + b, + r, + u, + ) +} diff --git a/api/build/skip.go b/api/build/skip.go new file mode 100644 index 000000000..4d5cf8af9 --- /dev/null +++ b/api/build/skip.go @@ -0,0 +1,41 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "github.com/go-vela/types/pipeline" +) + +// SkipEmptyBuild checks if the build should be skipped due to it +// not containing any steps besides init or clone. +// +//nolint:goconst // ignore init and clone constants +func SkipEmptyBuild(p *pipeline.Build) string { + if len(p.Stages) == 1 { + if p.Stages[0].Name == "init" { + return "skipping build since only init stage found" + } + } + + if len(p.Stages) == 2 { + if p.Stages[0].Name == "init" && p.Stages[1].Name == "clone" { + return "skipping build since only init and clone stages found" + } + } + + if len(p.Steps) == 1 { + if p.Steps[0].Name == "init" { + return "skipping build since only init step found" + } + } + + if len(p.Steps) == 2 { + if p.Steps[0].Name == "init" && p.Steps[1].Name == "clone" { + return "skipping build since only init and clone steps found" + } + } + + return "" +} diff --git a/api/build_test.go b/api/build/skip_test.go similarity index 80% rename from api/build_test.go rename to api/build/skip_test.go index 43253c108..ca805a84e 100644 --- a/api/build_test.go +++ b/api/build/skip_test.go @@ -1,4 +1,8 @@ -package api +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build import ( "testing" @@ -6,10 +10,11 @@ import ( "github.com/go-vela/types/pipeline" ) -func Test_skipEmptyBuild(t *testing.T) { +func Test_SkipEmptyBuild(t *testing.T) { type args struct { p *pipeline.Build } + tests := []struct { name string args args @@ -64,10 +69,11 @@ func Test_skipEmptyBuild(t *testing.T) { }, }}}, ""}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := skipEmptyBuild(tt.args.p); got != tt.want { - t.Errorf("skipEmptyBuild() = %v, want %v", got, tt.want) + if got := SkipEmptyBuild(tt.args.p); got != tt.want { + t.Errorf("SkipEmptyBuild() = %v, want %v", got, tt.want) } }) } diff --git a/api/build/token.go b/api/build/token.go new file mode 100644 index 000000000..da18a0322 --- /dev/null +++ b/api/build/token.go @@ -0,0 +1,119 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/claims" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/token builds GetBuildToken +// +// Get a build token +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved build token +// schema: +// "$ref": "#/definitions/Token" +// '400': +// description: Bad request +// schema: +// "$ref": "#/definitions/Error" +// '409': +// description: Conflict (requested build token for build not in pending state) +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to generate build token +// schema: +// "$ref": "#/definitions/Error" + +// GetBuildToken represents the API handler to generate a build token. +func GetBuildToken(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + cl := claims.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "user": cl.Subject, + }).Infof("generating build token for build %s/%d", r.GetFullName(), b.GetNumber()) + + // if build is not in a pending state, then a build token should not be needed - conflict + if !strings.EqualFold(b.GetStatus(), constants.StatusPending) { + retErr := fmt.Errorf("unable to mint build token: build is not in pending state") + util.HandleError(c, http.StatusConflict, retErr) + + return + } + + // retrieve token manager from context + tm := c.MustGet("token-manager").(*token.Manager) + + // set expiration to repo timeout plus configurable buffer + exp := (time.Duration(r.GetTimeout()) * time.Minute) + tm.BuildTokenBufferDuration + + // set mint token options + bmto := &token.MintTokenOpts{ + Hostname: cl.Subject, + BuildID: b.GetID(), + Repo: r.GetFullName(), + TokenType: constants.WorkerBuildTokenType, + TokenDuration: exp, + } + + // mint token + bt, err := tm.MintToken(bmto) + if err != nil { + retErr := fmt.Errorf("unable to generate build token: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, library.Token{Token: &bt}) +} diff --git a/api/build/update.go b/api/build/update.go new file mode 100644 index 000000000..d042fdd82 --- /dev/null +++ b/api/build/update.go @@ -0,0 +1,184 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/claims" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/repos/{org}/{repo}/builds/{build} builds UpdateBuild +// +// Updates a build in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number to update +// required: true +// type: integer +// - in: body +// name: body +// description: Payload containing the build to update +// required: true +// schema: +// "$ref": "#/definitions/Build" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the build +// schema: +// "$ref": "#/definitions/Build" +// '404': +// description: Unable to update the build +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update the build +// schema: +// "$ref": "#/definitions/Error" + +// UpdateBuild represents the API handler to update +// a build for a repo in the configured backend. +func UpdateBuild(c *gin.Context) { + // capture middleware values + cl := claims.Retrieve(c) + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + ctx := c.Request.Context() + + entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "user": cl.Subject, + }).Infof("updating build %s", entry) + + // capture body from API request + input := new(library.Build) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for build %s: %w", entry, err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // update build fields if provided + if len(input.GetStatus()) > 0 { + // update status if set + b.SetStatus(input.GetStatus()) + } + + if len(input.GetError()) > 0 { + // update error if set + b.SetError(input.GetError()) + } + + if input.GetEnqueued() > 0 { + // update enqueued if set + b.SetEnqueued(input.GetEnqueued()) + } + + if input.GetStarted() > 0 { + // update started if set + b.SetStarted(input.GetStarted()) + } + + if input.GetFinished() > 0 { + // update finished if set + b.SetFinished(input.GetFinished()) + } + + if len(input.GetTitle()) > 0 { + // update title if set + b.SetTitle(input.GetTitle()) + } + + if len(input.GetMessage()) > 0 { + // update message if set + b.SetMessage(input.GetMessage()) + } + + if len(input.GetHost()) > 0 { + // update host if set + b.SetHost(input.GetHost()) + } + + if len(input.GetRuntime()) > 0 { + // update runtime if set + b.SetRuntime(input.GetRuntime()) + } + + if len(input.GetDistribution()) > 0 { + // update distribution if set + b.SetDistribution(input.GetDistribution()) + } + + // send API call to update the build + b, err = database.FromContext(c).UpdateBuild(ctx, b) + if err != nil { + retErr := fmt.Errorf("unable to update build %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, b) + + // check if the build is in a "final" state + if b.GetStatus() == constants.StatusSuccess || + b.GetStatus() == constants.StatusFailure || + b.GetStatus() == constants.StatusCanceled || + b.GetStatus() == constants.StatusKilled || + b.GetStatus() == constants.StatusError { + // send API call to capture the repo owner + u, err := database.FromContext(c).GetUser(r.GetUserID()) + if err != nil { + logrus.Errorf("unable to get owner for build %s: %v", entry, err) + } + + // send API call to set the status on the commit + err = scm.FromContext(c).Status(u, b, r.GetOrg(), r.GetName()) + if err != nil { + logrus.Errorf("unable to set commit status for build %s: %v", entry, err) + } + } +} diff --git a/api/deployment.go b/api/deployment.go deleted file mode 100644 index 33f43b6f8..000000000 --- a/api/deployment.go +++ /dev/null @@ -1,343 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "fmt" - "net/http" - "strconv" - - "github.com/go-vela/server/router/middleware/org" - - "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/repo" - "github.com/go-vela/server/router/middleware/user" - "github.com/go-vela/server/scm" - "github.com/go-vela/server/util" - - "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -// swagger:operation POST /api/v1/deployments/{org}/{repo} deployment CreateDeployment -// -// Create a deployment for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '201': -// description: Successfully created the deployment -// schema: -// "$ref": "#/definitions/Deployment" -// '400': -// description: Unable to create the deployment -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to create the deployment -// schema: -// "$ref": "#/definitions/Error" - -// CreateDeployment represents the API handler to -// create a deployment in the configured backend. -func CreateDeployment(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("creating new deployment for repo %s", r.GetFullName()) - - // capture body from API request - input := new(library.Deployment) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for new deployment for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update fields in deployment object - input.SetRepoID(r.GetID()) - input.SetUser(u.GetName()) - - if len(input.GetDescription()) == 0 { - input.SetDescription("Deployment request from Vela") - } - - if len(input.GetTask()) == 0 { - input.SetTask("deploy:vela") - } - - // send API call to create the deployment - err = scm.FromContext(c).CreateDeployment(u, r, input) - if err != nil { - retErr := fmt.Errorf("unable to create new deployment for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusCreated, input) -} - -// swagger:operation GET /api/v1/deployments/{org}/{repo} deployment GetDeployments -// -// Get a list of deployments for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: query -// name: page -// description: The page of results to retrieve -// type: integer -// default: 1 -// - in: query -// name: per_page -// description: How many results per page to return -// type: integer -// maximum: 100 -// default: 10 -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the list of deployments -// schema: -// type: array -// items: -// "$ref": "#/definitions/Deployment" -// headers: -// X-Total-Count: -// description: Total number of results -// type: integer -// Link: -// description: see https://tools.ietf.org/html/rfc5988 -// type: string -// '400': -// description: Unable to retrieve the list of deployments -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the list of deployments -// schema: -// "$ref": "#/definitions/Error" - -// GetDeployments represents the API handler to capture -// a list of deployments from the configured backend. -func GetDeployments(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading deployments for repo %s", r.GetFullName()) - - // capture page query parameter if present - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) - if err != nil { - retErr := fmt.Errorf("unable to convert page query parameter for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // capture per_page query parameter if present - perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to convert per_page query parameter for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // ensure per_page isn't above or below allowed values - // - // nolint: gomnd // ignore magic number - perPage = util.MaxInt(1, util.MinInt(100, perPage)) - - // send API call to capture the total number of deployments for the repo - t, err := scm.FromContext(c).GetDeploymentCount(u, r) - if err != nil { - retErr := fmt.Errorf("unable to get deployment count for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the list of steps for the build - d, err := scm.FromContext(c).GetDeploymentList(u, r, page, perPage) - if err != nil { - retErr := fmt.Errorf("unable to get deployments for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - dWithBs := []*library.Deployment{} - for _, deployment := range d { - b, err := database.FromContext(c).GetDeploymentBuildList(*deployment.URL) - if err != nil { - retErr := fmt.Errorf("unable to get builds for deployment %d: %w", deployment.GetID(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - builds := []library.Build{} - for _, build := range b { - builds = append(builds, *build) - } - - deployment.SetBuilds(builds) - - dWithBs = append(dWithBs, deployment) - } - - // create pagination object - pagination := Pagination{ - Page: page, - PerPage: perPage, - Total: t, - } - // set pagination headers - pagination.SetHeaderLink(c) - - c.JSON(http.StatusOK, dWithBs) -} - -// swagger:operation GET /api/v1/deployments/{org}/{repo}/{deployment} deployment GetDeployment -// -// Get a deployment from the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: deployment -// description: Name of the org -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the deployment -// schema: -// "$ref": "#/definitions/Deployment" -// '400': -// description: Unable to retrieve the deployment -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the deployment -// schema: -// "$ref": "#/definitions/Error" - -// GetDeployment represents the API handler to -// capture a deployment from the configured backend. -func GetDeployment(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - deployment := c.Param("deployment") - - entry := fmt.Sprintf("%s/%s", r.GetFullName(), deployment) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading deployment %s", entry) - - number, err := strconv.Atoi(deployment) - if err != nil { - retErr := fmt.Errorf("invalid deployment parameter provided: %s", deployment) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // send API call to capture the deployment - d, err := scm.FromContext(c).GetDeployment(u, r, int64(number)) - if err != nil { - retErr := fmt.Errorf("unable to get deployment %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, d) -} diff --git a/api/deployment/create.go b/api/deployment/create.go new file mode 100644 index 000000000..1ad549c07 --- /dev/null +++ b/api/deployment/create.go @@ -0,0 +1,107 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package deployment + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/deployments/{org}/{repo} deployments CreateDeployment +// +// Create a deployment for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: Successfully created the deployment +// schema: +// "$ref": "#/definitions/Deployment" +// '400': +// description: Unable to create the deployment +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the deployment +// schema: +// "$ref": "#/definitions/Error" + +// CreateDeployment represents the API handler to +// create a deployment in the configured backend. +func CreateDeployment(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("creating new deployment for repo %s", r.GetFullName()) + + // capture body from API request + input := new(library.Deployment) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for new deployment for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update fields in deployment object + input.SetRepoID(r.GetID()) + input.SetUser(u.GetName()) + + if len(input.GetDescription()) == 0 { + input.SetDescription("Deployment request from Vela") + } + + if len(input.GetTask()) == 0 { + input.SetTask("deploy:vela") + } + + // send API call to create the deployment + err = scm.FromContext(c).CreateDeployment(u, r, input) + if err != nil { + retErr := fmt.Errorf("unable to create new deployment for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusCreated, input) +} diff --git a/api/deployment/doc.go b/api/deployment/doc.go new file mode 100644 index 000000000..c6118b8f3 --- /dev/null +++ b/api/deployment/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package deployment provides the deployment handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/deployment" +package deployment diff --git a/api/deployment/get.go b/api/deployment/get.go new file mode 100644 index 000000000..ff6827d3a --- /dev/null +++ b/api/deployment/get.go @@ -0,0 +1,100 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package deployment + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/deployments/{org}/{repo}/{deployment} deployments GetDeployment +// +// Get a deployment from the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: deployment +// description: Number of the deployment +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the deployment +// schema: +// "$ref": "#/definitions/Deployment" +// '400': +// description: Unable to retrieve the deployment +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the deployment +// schema: +// "$ref": "#/definitions/Error" + +// GetDeployment represents the API handler to +// capture a deployment from the configured backend. +func GetDeployment(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + deployment := util.PathParameter(c, "deployment") + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), deployment) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("reading deployment %s", entry) + + number, err := strconv.Atoi(deployment) + if err != nil { + retErr := fmt.Errorf("invalid deployment parameter provided: %s", deployment) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture the deployment + d, err := scm.FromContext(c).GetDeployment(u, r, int64(number)) + if err != nil { + retErr := fmt.Errorf("unable to get deployment %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, d) +} diff --git a/api/deployment/list.go b/api/deployment/list.go new file mode 100644 index 000000000..cb2ec2010 --- /dev/null +++ b/api/deployment/list.go @@ -0,0 +1,171 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package deployment + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/deployments/{org}/{repo} deployments ListDeployments +// +// Get a list of deployments for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the list of deployments +// schema: +// type: array +// items: +// "$ref": "#/definitions/Deployment" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve the list of deployments +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the list of deployments +// schema: +// "$ref": "#/definitions/Error" + +// ListDeployments represents the API handler to capture +// a list of deployments from the configured backend. +func ListDeployments(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("reading deployments for repo %s", r.GetFullName()) + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + retErr := fmt.Errorf("unable to convert page query parameter for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // send API call to capture the total number of deployments for the repo + t, err := scm.FromContext(c).GetDeploymentCount(u, r) + if err != nil { + retErr := fmt.Errorf("unable to get deployment count for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // send API call to capture the list of deployments for the repo + d, err := scm.FromContext(c).GetDeploymentList(u, r, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to get deployments for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + dWithBs := []*library.Deployment{} + + for _, deployment := range d { + b, _, err := database.FromContext(c).ListBuildsForDeployment(ctx, deployment, nil, 1, 3) + if err != nil { + retErr := fmt.Errorf("unable to get builds for deployment %d: %w", deployment.GetID(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + builds := []library.Build{} + for _, build := range b { + builds = append(builds, *build) + } + + deployment.SetBuilds(builds) + + dWithBs = append(dWithBs, deployment) + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, dWithBs) +} diff --git a/api/doc.go b/api/doc.go index e1fe11d97..4f9d47c48 100644 --- a/api/doc.go +++ b/api/doc.go @@ -6,5 +6,5 @@ // // Usage: // -// import "github.com/go-vela/server/api" +// import "github.com/go-vela/server/api" package api diff --git a/api/hook.go b/api/hook.go deleted file mode 100644 index f1b7da600..000000000 --- a/api/hook.go +++ /dev/null @@ -1,594 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "fmt" - "net/http" - "strconv" - "time" - - "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/router/middleware/user" - - "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/repo" - "github.com/go-vela/server/util" - - "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -// swagger:operation POST /api/v1/hooks/{org}/{repo} webhook CreateHook -// -// Create a webhook for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: body -// name: body -// description: Webhook payload that we expect from the user or VCS -// required: true -// schema: -// "$ref": "#/definitions/Webhook" -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '201': -// description: The webhook has been created -// schema: -// "$ref": "#/definitions/Webhook" -// '400': -// description: The webhook was unable to be created -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: The webhook was unable to be created -// schema: -// "$ref": "#/definitions/Error" - -// CreateHook represents the API handler to create -// a webhook in the configured backend. -func CreateHook(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("creating new hook for repo %s", r.GetFullName()) - - // capture body from API request - input := new(library.Hook) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for new hook for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // send API call to capture the last hook for the repo - lastHook, err := database.FromContext(c).GetLastHook(r) - if err != nil { - retErr := fmt.Errorf("unable to get last hook for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // update fields in webhook object - input.SetRepoID(r.GetID()) - input.SetNumber(1) - - if input.GetCreated() == 0 { - input.SetCreated(time.Now().UTC().Unix()) - } - - if lastHook != nil { - input.SetNumber( - lastHook.GetNumber() + 1, - ) - } - - // send API call to create the webhook - err = database.FromContext(c).CreateHook(input) - if err != nil { - retErr := fmt.Errorf("unable to create hook for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the created webhook - h, _ := database.FromContext(c).GetHook(input.GetNumber(), r) - - c.JSON(http.StatusCreated, h) -} - -// swagger:operation GET /api/v1/hooks/{org}/{repo} webhook GetHooks -// -// Retrieve the webhooks for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: query -// name: page -// description: The page of results to retrieve -// type: integer -// default: 1 -// - in: query -// name: per_page -// description: How many results per page to return -// type: integer -// maximum: 100 -// default: 10 -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved webhooks -// schema: -// type: array -// items: -// "$ref": "#/definitions/Webhook" -// headers: -// X-Total-Count: -// description: Total number of results -// type: integer -// Link: -// description: see https://tools.ietf.org/html/rfc5988 -// type: string -// '400': -// description: Unable to retrieve webhooks -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve webhooks -// schema: -// "$ref": "#/definitions/Error" - -// GetHooks represents the API handler to capture a list -// of webhooks from the configured backend. -func GetHooks(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading hooks for repo %s", r.GetFullName()) - - // capture page query parameter if present - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to convert page query parameter for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // capture per_page query parameter if present - perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to convert per_page query parameter for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // ensure per_page isn't above or below allowed values - // - // nolint: gomnd // ignore magic number - perPage = util.MaxInt(1, util.MinInt(100, perPage)) - - // send API call to capture the total number of webhooks for the repo - t, err := database.FromContext(c).GetRepoHookCount(r) - if err != nil { - retErr := fmt.Errorf("unable to get hooks count for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the list of steps for the build - h, err := database.FromContext(c).GetRepoHookList(r, page, perPage) - if err != nil { - retErr := fmt.Errorf("unable to get hooks for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // create pagination object - pagination := Pagination{ - Page: page, - PerPage: perPage, - Total: t, - } - // set pagination headers - pagination.SetHeaderLink(c) - - c.JSON(http.StatusOK, h) -} - -// swagger:operation GET /api/v1/hooks/{org}/{repo}/{hook} webhook GetHook -// -// Retrieve a webhook for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: hook -// description: Number of the hook -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the webhook -// schema: -// "$ref": "#/definitions/Webhook" -// '400': -// description: Unable to retrieve the webhook -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the webhook -// schema: -// "$ref": "#/definitions/Error" - -// GetHook represents the API handler to capture a -// webhook from the configured backend. -func GetHook(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - hook := c.Param("hook") - - entry := fmt.Sprintf("%s/%s", r.GetFullName(), hook) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "hook": hook, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading hook %s", entry) - - number, err := strconv.Atoi(hook) - if err != nil { - retErr := fmt.Errorf("invalid hook parameter provided: %s", hook) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // send API call to capture the webhook - h, err := database.FromContext(c).GetHook(number, r) - if err != nil { - retErr := fmt.Errorf("unable to get hook %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, h) -} - -// swagger:operation PUT /api/v1/hooks/{org}/{repo}/{hook} webhook UpdateHook -// -// Update a webhook for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: hook -// description: Number of the hook -// required: true -// type: integer -// - in: body -// name: body -// description: Webhook payload that we expect from the user or VCS -// required: true -// schema: -// "$ref": "#/definitions/Webhook" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully updated the webhook -// schema: -// "$ref": "#/definitions/Webhook" -// '400': -// description: The webhook was unable to be updated -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: The webhook was unable to be updated -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: The webhook was unable to be updated -// schema: -// "$ref": "#/definitions/Error" - -// UpdateHook represents the API handler to update -// a webhook in the configured backend. -func UpdateHook(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - hook := c.Param("hook") - - entry := fmt.Sprintf("%s/%s", r.GetFullName(), hook) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "hook": hook, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("updating hook %s", entry) - - // capture body from API request - input := new(library.Hook) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for hook %s: %w", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - number, err := strconv.Atoi(hook) - if err != nil { - retErr := fmt.Errorf("invalid hook parameter provided: %s", hook) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // send API call to capture the webhook - h, err := database.FromContext(c).GetHook(number, r) - if err != nil { - retErr := fmt.Errorf("unable to get hook %s: %w", entry, err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - // update webhook fields if provided - if input.GetCreated() > 0 { - // update created if set - h.SetCreated(input.GetCreated()) - } - - if len(input.GetHost()) > 0 { - // update host if set - h.SetHost(input.GetHost()) - } - - if len(input.GetEvent()) > 0 { - // update event if set - h.SetEvent(input.GetEvent()) - } - - if len(input.GetBranch()) > 0 { - // update branch if set - h.SetBranch(input.GetBranch()) - } - - if len(input.GetError()) > 0 { - // update error if set - h.SetError(input.GetError()) - } - - if len(input.GetStatus()) > 0 { - // update status if set - h.SetStatus(input.GetStatus()) - } - - if len(input.GetLink()) > 0 { - // update link if set - h.SetLink(input.GetLink()) - } - - // send API call to update the webhook - err = database.FromContext(c).UpdateHook(h) - if err != nil { - retErr := fmt.Errorf("unable to update hook %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated user - h, _ = database.FromContext(c).GetHook(h.GetNumber(), r) - - c.JSON(http.StatusOK, h) -} - -// swagger:operation DELETE /api/v1/hooks/{org}/{repo}/{hook} webhook DeleteHook -// -// Delete a webhook for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: hook -// description: Number of the hook -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully deleted the webhook -// schema: -// type: string -// '400': -// description: The webhook was unable to be deleted -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: The webhook was unable to be deleted -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: The webhook was unable to be deleted -// schema: -// "$ref": "#/definitions/Error" - -// DeleteHook represents the API handler to remove -// a webhook from the configured backend. -func DeleteHook(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - hook := c.Param("hook") - - entry := fmt.Sprintf("%s/%s", r.GetFullName(), hook) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "hook": hook, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("deleting hook %s", entry) - - number, err := strconv.Atoi(hook) - if err != nil { - retErr := fmt.Errorf("invalid hook parameter provided: %s", hook) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // send API call to capture the webhook - h, err := database.FromContext(c).GetHook(number, r) - if err != nil { - retErr := fmt.Errorf("unable to get hook %s: %w", hook, err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - // send API call to remove the webhook - err = database.FromContext(c).DeleteHook(h.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to delete hook %s: %w", hook, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, fmt.Sprintf("hook %s deleted", entry)) -} diff --git a/api/hook/create.go b/api/hook/create.go new file mode 100644 index 000000000..b33d9fdfb --- /dev/null +++ b/api/hook/create.go @@ -0,0 +1,126 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/hooks/{org}/{repo} webhook CreateHook +// +// Create a webhook for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: Webhook payload that we expect from the user or VCS +// required: true +// schema: +// "$ref": "#/definitions/Webhook" +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: The webhook has been created +// schema: +// "$ref": "#/definitions/Webhook" +// '400': +// description: The webhook was unable to be created +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: The webhook was unable to be created +// schema: +// "$ref": "#/definitions/Error" + +// CreateHook represents the API handler to create +// a webhook in the configured backend. +func CreateHook(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("creating new hook for repo %s", r.GetFullName()) + + // capture body from API request + input := new(library.Hook) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for new hook for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture the last hook for the repo + lastHook, err := database.FromContext(c).LastHookForRepo(r) + if err != nil { + retErr := fmt.Errorf("unable to get last hook for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // update fields in webhook object + input.SetRepoID(r.GetID()) + input.SetNumber(1) + + if input.GetCreated() == 0 { + input.SetCreated(time.Now().UTC().Unix()) + } + + if lastHook != nil { + input.SetNumber( + lastHook.GetNumber() + 1, + ) + } + + // send API call to create the webhook + h, err := database.FromContext(c).CreateHook(input) + if err != nil { + retErr := fmt.Errorf("unable to create hook for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusCreated, h) +} diff --git a/api/hook/delete.go b/api/hook/delete.go new file mode 100644 index 000000000..7ada1f0fb --- /dev/null +++ b/api/hook/delete.go @@ -0,0 +1,115 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/hooks/{org}/{repo}/{hook} webhook DeleteHook +// +// Delete a webhook for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: hook +// description: Number of the hook +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted the webhook +// schema: +// type: string +// '400': +// description: The webhook was unable to be deleted +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: The webhook was unable to be deleted +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: The webhook was unable to be deleted +// schema: +// "$ref": "#/definitions/Error" + +// DeleteHook represents the API handler to remove +// a webhook from the configured backend. +func DeleteHook(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + hook := util.PathParameter(c, "hook") + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), hook) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "hook": hook, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("deleting hook %s", entry) + + number, err := strconv.Atoi(hook) + if err != nil { + retErr := fmt.Errorf("invalid hook parameter provided: %s", hook) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture the webhook + h, err := database.FromContext(c).GetHookForRepo(r, number) + if err != nil { + retErr := fmt.Errorf("unable to get hook %s: %w", hook, err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // send API call to remove the webhook + err = database.FromContext(c).DeleteHook(h) + if err != nil { + retErr := fmt.Errorf("unable to delete hook %s: %w", hook, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("hook %s deleted", entry)) +} diff --git a/api/hook/get.go b/api/hook/get.go new file mode 100644 index 000000000..458183dcb --- /dev/null +++ b/api/hook/get.go @@ -0,0 +1,101 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/hooks/{org}/{repo}/{hook} webhook GetHook +// +// Retrieve a webhook for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: hook +// description: Number of the hook +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the webhook +// schema: +// "$ref": "#/definitions/Webhook" +// '400': +// description: Unable to retrieve the webhook +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the webhook +// schema: +// "$ref": "#/definitions/Error" + +// GetHook represents the API handler to capture a +// webhook from the configured backend. +func GetHook(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + hook := util.PathParameter(c, "hook") + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), hook) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "hook": hook, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("reading hook %s", entry) + + number, err := strconv.Atoi(hook) + if err != nil { + retErr := fmt.Errorf("invalid hook parameter provided: %s", hook) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture the webhook + h, err := database.FromContext(c).GetHookForRepo(r, number) + if err != nil { + retErr := fmt.Errorf("unable to get hook %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, h) +} diff --git a/api/hook/list.go b/api/hook/list.go new file mode 100644 index 000000000..2cb6d05fe --- /dev/null +++ b/api/hook/list.go @@ -0,0 +1,136 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/hooks/{org}/{repo} webhook ListHooks +// +// Retrieve the webhooks for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved webhooks +// schema: +// type: array +// items: +// "$ref": "#/definitions/Webhook" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve webhooks +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve webhooks +// schema: +// "$ref": "#/definitions/Error" + +// ListHooks represents the API handler to capture a list +// of webhooks from the configured backend. +func ListHooks(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("reading hooks for repo %s", r.GetFullName()) + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + retErr := fmt.Errorf("unable to convert page query parameter for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // send API call to capture the list of steps for the build + h, t, err := database.FromContext(c).ListHooksForRepo(r, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to get hooks for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, h) +} diff --git a/api/hook/redeliver.go b/api/hook/redeliver.go new file mode 100644 index 000000000..4afcb8a79 --- /dev/null +++ b/api/hook/redeliver.go @@ -0,0 +1,115 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/hooks/{org}/{repo}/{hook}/redeliver webhook RedeliverHook +// +// Redeliver a webhook from the SCM +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: hook +// description: Number of the hook +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully redelivered the webhook +// schema: +// "$ref": "#/definitions/Webhook" +// '400': +// description: The webhook was unable to be redelivered +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: The webhook was unable to be redelivered +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: The webhook was unable to be redelivered +// schema: +// "$ref": "#/definitions/Error" + +// RedeliverHook represents the API handler to redeliver +// a webhook from the SCM. +func RedeliverHook(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + hook := util.PathParameter(c, "hook") + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), hook) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "hook": hook, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("redelivering hook %s", entry) + + number, err := strconv.Atoi(hook) + if err != nil { + retErr := fmt.Errorf("invalid hook parameter provided: %s", hook) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture the webhook + h, err := database.FromContext(c).GetHookForRepo(r, number) + if err != nil { + retErr := fmt.Errorf("unable to get hook %s: %w", entry, err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + err = scm.FromContext(c).RedeliverWebhook(c, u, r, h) + if err != nil { + retErr := fmt.Errorf("unable to redeliver hook %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("hook %s redelivered", entry)) +} diff --git a/api/hook/update.go b/api/hook/update.go new file mode 100644 index 000000000..2c5cdcfb7 --- /dev/null +++ b/api/hook/update.go @@ -0,0 +1,170 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/hooks/{org}/{repo}/{hook} webhook UpdateHook +// +// Update a webhook for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: hook +// description: Number of the hook +// required: true +// type: integer +// - in: body +// name: body +// description: Webhook payload that we expect from the user or VCS +// required: true +// schema: +// "$ref": "#/definitions/Webhook" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the webhook +// schema: +// "$ref": "#/definitions/Webhook" +// '400': +// description: The webhook was unable to be updated +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: The webhook was unable to be updated +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: The webhook was unable to be updated +// schema: +// "$ref": "#/definitions/Error" + +// UpdateHook represents the API handler to update +// a webhook in the configured backend. +func UpdateHook(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + hook := util.PathParameter(c, "hook") + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), hook) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "hook": hook, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("updating hook %s", entry) + + // capture body from API request + input := new(library.Hook) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for hook %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + number, err := strconv.Atoi(hook) + if err != nil { + retErr := fmt.Errorf("invalid hook parameter provided: %s", hook) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture the webhook + h, err := database.FromContext(c).GetHookForRepo(r, number) + if err != nil { + retErr := fmt.Errorf("unable to get hook %s: %w", entry, err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // update webhook fields if provided + if input.GetCreated() > 0 { + // update created if set + h.SetCreated(input.GetCreated()) + } + + if len(input.GetHost()) > 0 { + // update host if set + h.SetHost(input.GetHost()) + } + + if len(input.GetEvent()) > 0 { + // update event if set + h.SetEvent(input.GetEvent()) + } + + if len(input.GetBranch()) > 0 { + // update branch if set + h.SetBranch(input.GetBranch()) + } + + if len(input.GetError()) > 0 { + // update error if set + h.SetError(input.GetError()) + } + + if len(input.GetStatus()) > 0 { + // update status if set + h.SetStatus(input.GetStatus()) + } + + if len(input.GetLink()) > 0 { + // update link if set + h.SetLink(input.GetLink()) + } + + // send API call to update the webhook + h, err = database.FromContext(c).UpdateHook(h) + if err != nil { + retErr := fmt.Errorf("unable to update hook %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, h) +} diff --git a/api/log.go b/api/log.go deleted file mode 100644 index eafc9d0db..000000000 --- a/api/log.go +++ /dev/null @@ -1,875 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "fmt" - "net/http" - - "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/router/middleware/user" - - "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/build" - "github.com/go-vela/server/router/middleware/repo" - "github.com/go-vela/server/router/middleware/service" - "github.com/go-vela/server/router/middleware/step" - "github.com/go-vela/server/util" - - "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/logs builds GetBuildLogs -// -// Get logs for a build in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved logs for the build -// schema: -// type: array -// items: -// "$ref": "#/definitions/Log" -// '500': -// description: Unable to retrieve logs for the build -// schema: -// "$ref": "#/definitions/Error" - -// GetBuildLogs represents the API handler to capture a -// list of logs for a build from the configured backend. -func GetBuildLogs(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading logs for build %s", entry) - - // send API call to capture the list of logs for the build - l, err := database.FromContext(c).GetBuildLogs(b.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to get logs for build %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, l) -} - -// nolint: lll // ignore long line length due to API path -// -// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/services/{service}/logs services CreateServiceLogs -// -// Create the logs for a service -// -// --- -// deprecated: true -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: service -// description: ID of the service -// required: true -// type: integer -// - in: body -// name: body -// description: Payload containing the log to create -// required: true -// schema: -// "$ref": "#/definitions/Log" -// security: -// - ApiKeyAuth: [] -// responses: -// '201': -// description: Successfully created the service logs -// schema: -// "$ref": "#/definitions/Log" -// '400': -// description: Unable to create the service logs -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to create the service logs -// schema: -// "$ref": "#/definitions/Error" - -// CreateServiceLog represents the API handler to create -// the logs for a service in the configured backend. -// -// nolint: dupl // ignore similar code with step -func CreateServiceLog(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := service.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "service": s.GetNumber(), - "user": u.GetName(), - }).Infof("creating logs for service %s", entry) - - // capture body from API request - input := new(library.Log) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for service %s: %w", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update fields in log object - input.SetServiceID(s.GetID()) - input.SetBuildID(b.GetID()) - input.SetRepoID(r.GetID()) - - // send API call to create the logs - err = database.FromContext(c).CreateLog(input) - if err != nil { - retErr := fmt.Errorf("unable to create logs for service %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the created log - l, _ := database.FromContext(c).GetServiceLog(s.GetID()) - - c.JSON(http.StatusCreated, l) -} - -// nolint: lll // ignore long line length due to API path -// -// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/services/{service}/logs services GetServiceLogs -// -// Retrieve the logs for a service -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: service -// description: ID of the service -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the service logs -// schema: -// "$ref": "#/definitions/Log" -// '500': -// description: Unable to retrieve the service logs -// schema: -// "$ref": "#/definitions/Error" - -// GetServiceLog represents the API handler to capture -// the logs for a service from the configured backend. -func GetServiceLog(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := service.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "service": s.GetNumber(), - "user": u.GetName(), - }).Infof("reading logs for service %s", entry) - - // send API call to capture the service logs - l, err := database.FromContext(c).GetServiceLog(s.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to get logs for service %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, l) -} - -// nolint: lll // ignore long line length due to API path -// -// swagger:operation PUT /api/v1/repos/{org}/{repo}/builds/{build}/services/{service}/logs services UpdateServiceLog -// -// Update the logs for a service -// -// --- -// deprecated: true -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: service -// description: ID of the service -// required: true -// type: integer -// - in: body -// name: body -// description: Payload containing the log to update -// required: true -// schema: -// "$ref": "#/definitions/Log" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully updated the service logs -// schema: -// "$ref": "#/definitions/Log" -// '400': -// description: Unable to updated the service logs -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to updates the service logs -// schema: -// "$ref": "#/definitions/Error" - -// UpdateServiceLog represents the API handler to update -// the logs for a service in the configured backend. -func UpdateServiceLog(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := service.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "service": s.GetNumber(), - "user": u.GetName(), - }).Infof("updating logs for service %s", entry) - - // send API call to capture the service logs - l, err := database.FromContext(c).GetServiceLog(s.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to get logs for service %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // capture body from API request - input := new(library.Log) - - err = c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for service %s: %w", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update log fields if provided - if len(input.GetData()) > 0 { - // update data if set - l.SetData(input.GetData()) - } - - // send API call to update the log - err = database.FromContext(c).UpdateLog(l) - if err != nil { - retErr := fmt.Errorf("unable to update logs for service %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated log - l, _ = database.FromContext(c).GetServiceLog(s.GetID()) - - c.JSON(http.StatusOK, l) -} - -// nolint: lll // ignore long line length due to API path -// -// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build}/services/{service}/logs services DeleteServiceLogs -// -// Delete the logs for a service -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: service -// description: ID of the service -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully deleted the service logs -// schema: -// type: string -// '500': -// description: Unable to delete the service logs -// schema: -// "$ref": "#/definitions/Error" - -// DeleteServiceLog represents the API handler to remove -// the logs for a service from the configured backend. -// -// nolint: dupl // ignore similar code with step -func DeleteServiceLog(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := service.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "service": s.GetNumber(), - "user": u.GetName(), - }).Infof("deleting logs for service %s", entry) - - // send API call to remove the log - err := database.FromContext(c).DeleteLog(s.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to delete logs for service %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, fmt.Sprintf("logs deleted for service %s", entry)) -} - -// nolint: lll // ignore long line length due to API path -// -// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step}/logs steps CreateStepLog -// -// Create the logs for a step -// -// --- -// deprecated: true -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: step -// description: Step number -// required: true -// type: integer -// - in: body -// name: body -// description: Payload containing the log to create -// required: true -// schema: -// "$ref": "#/definitions/Log" -// security: -// - ApiKeyAuth: [] -// responses: -// '201': -// description: Successfully created the logs for step -// schema: -// "$ref": "#/definitions/Log" -// '400': -// description: Unable to create the logs for a step -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to create the logs for a step -// schema: -// "$ref": "#/definitions/Error" - -// CreateStepLog represents the API handler to create -// the logs for a step in the configured backend. -// -// nolint: dupl // ignore similar code with service -func CreateStepLog(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := step.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "step": s.GetNumber(), - "user": u.GetName(), - }).Infof("creating logs for step %s", entry) - - // capture body from API request - input := new(library.Log) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for step %s: %w", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update fields in log object - input.SetStepID(s.GetID()) - input.SetBuildID(b.GetID()) - input.SetRepoID(r.GetID()) - - // send API call to create the logs - err = database.FromContext(c).CreateLog(input) - if err != nil { - retErr := fmt.Errorf("unable to create logs for step %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the created log - l, _ := database.FromContext(c).GetStepLog(s.GetID()) - - c.JSON(http.StatusCreated, l) -} - -// nolint: lll // ignore long line length due to API path -// -// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step}/logs steps GetStepLog -// -// Retrieve the logs for a step -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: step -// description: Step number -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the logs for step -// type: json -// schema: -// "$ref": "#/definitions/Log" -// '500': -// description: Unable to retrieve the logs for a step -// schema: -// "$ref": "#/definitions/Error" - -// GetStepLog represents the API handler to capture -// the logs for a step from the configured backend. -func GetStepLog(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := step.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "step": s.GetNumber(), - "user": u.GetName(), - }).Infof("reading logs for step %s", entry) - - // send API call to capture the step logs - l, err := database.FromContext(c).GetStepLog(s.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to get logs for step %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, l) -} - -// nolint: lll // ignore long line length due to API path -// -// swagger:operation PUT /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step}/logs steps UpdateStepLog -// -// Update the logs for a step -// -// --- -// deprecated: true -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: step -// description: Step number -// required: true -// type: integer -// - in: body -// name: body -// description: Payload containing the log to update -// required: true -// schema: -// "$ref": "#/definitions/Log" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully updated the logs for step -// schema: -// "$ref": "#/definitions/Log" -// '400': -// description: Unable to update the logs for a step -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to update the logs for a step -// schema: -// "$ref": "#/definitions/Error" - -// UpdateStepLog represents the API handler to update -// the logs for a step in the configured backend. -func UpdateStepLog(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := step.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "step": s.GetNumber(), - "user": u.GetName(), - }).Infof("updating logs for step %s", entry) - - // send API call to capture the step logs - l, err := database.FromContext(c).GetStepLog(s.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to get logs for step %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // capture body from API request - input := new(library.Log) - - err = c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for step %s: %v", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update log fields if provided - if len(input.GetData()) > 0 { - // update data if set - l.SetData(input.GetData()) - } - - // send API call to update the log - err = database.FromContext(c).UpdateLog(l) - if err != nil { - retErr := fmt.Errorf("unable to update logs for step %s: %v", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated log - l, _ = database.FromContext(c).GetStepLog(s.GetID()) - - c.JSON(http.StatusOK, l) -} - -// nolint: lll // ignore long line length due to API path -// -// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step}/logs steps DeleteStepLog -// -// Delete the logs for a step -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: step -// description: Step number -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully deleted the logs for the step -// schema: -// type: string -// '500': -// description: Unable to delete the logs for the step -// schema: -// "$ref": "#/definitions/Error" - -// DeleteStepLog represents the API handler to remove -// the logs for a step from the configured backend. -// -// nolint: dupl // ignore similar code with service -func DeleteStepLog(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := step.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "step": s.GetNumber(), - "user": u.GetName(), - }).Infof("deleting logs for step %s", entry) - - // send API call to remove the log - err := database.FromContext(c).DeleteLog(s.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to delete logs for step %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, fmt.Sprintf("logs deleted for step %s", entry)) -} diff --git a/api/log/create_service.go b/api/log/create_service.go new file mode 100644 index 000000000..bf8cd4ec1 --- /dev/null +++ b/api/log/create_service.go @@ -0,0 +1,124 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code to step +package log + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/service" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/services/{service}/logs services CreateServiceLog +// +// Create the logs for a service +// +// --- +// deprecated: true +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: service +// description: Service number +// required: true +// type: integer +// - in: body +// name: body +// description: Payload containing the log to create +// required: true +// schema: +// "$ref": "#/definitions/Log" +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: Successfully created the service logs +// '400': +// description: Unable to create the service logs +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the service logs +// schema: +// "$ref": "#/definitions/Error" + +// CreateServiceLog represents the API handler to create +// the logs for a service in the configured backend. +func CreateServiceLog(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := service.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "service": s.GetNumber(), + "user": u.GetName(), + }).Infof("creating logs for service %s", entry) + + // capture body from API request + input := new(library.Log) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for service %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update fields in log object + input.SetServiceID(s.GetID()) + input.SetBuildID(b.GetID()) + input.SetRepoID(r.GetID()) + + // send API call to create the logs + err = database.FromContext(c).CreateLog(input) + if err != nil { + retErr := fmt.Errorf("unable to create logs for service %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusCreated, nil) +} diff --git a/api/log/create_step.go b/api/log/create_step.go new file mode 100644 index 000000000..d2abed7a9 --- /dev/null +++ b/api/log/create_step.go @@ -0,0 +1,124 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code to service +package log + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/step" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step}/logs steps CreateStepLog +// +// Create the logs for a step +// +// --- +// deprecated: true +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: step +// description: Step number +// required: true +// type: integer +// - in: body +// name: body +// description: Payload containing the log to create +// required: true +// schema: +// "$ref": "#/definitions/Log" +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: Successfully created the logs for step +// '400': +// description: Unable to create the logs for a step +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the logs for a step +// schema: +// "$ref": "#/definitions/Error" + +// CreateStepLog represents the API handler to create +// the logs for a step in the configured backend. +func CreateStepLog(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := step.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "step": s.GetNumber(), + "user": u.GetName(), + }).Infof("creating logs for step %s", entry) + + // capture body from API request + input := new(library.Log) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for step %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update fields in log object + input.SetStepID(s.GetID()) + input.SetBuildID(b.GetID()) + input.SetRepoID(r.GetID()) + + // send API call to create the logs + err = database.FromContext(c).CreateLog(input) + if err != nil { + retErr := fmt.Errorf("unable to create logs for step %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusCreated, nil) +} diff --git a/api/log/delete_service.go b/api/log/delete_service.go new file mode 100644 index 000000000..c87d87b1b --- /dev/null +++ b/api/log/delete_service.go @@ -0,0 +1,107 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with step +package log + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/service" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build}/services/{service}/logs services DeleteServiceLog +// +// Delete the logs for a service +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: service +// description: Service number +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted the service logs +// schema: +// type: string +// '500': +// description: Unable to delete the service logs +// schema: +// "$ref": "#/definitions/Error" + +// DeleteServiceLog represents the API handler to remove +// the logs for a service from the configured backend. +func DeleteServiceLog(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := service.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "service": s.GetNumber(), + "user": u.GetName(), + }).Infof("deleting logs for service %s", entry) + + // send API call to capture the service logs + l, err := database.FromContext(c).GetLogForService(s) + if err != nil { + retErr := fmt.Errorf("unable to get logs for service %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // send API call to remove the log + err = database.FromContext(c).DeleteLog(l) + if err != nil { + retErr := fmt.Errorf("unable to delete logs for service %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("logs deleted for service %s", entry)) +} diff --git a/api/log/delete_step.go b/api/log/delete_step.go new file mode 100644 index 000000000..90e7fc7ac --- /dev/null +++ b/api/log/delete_step.go @@ -0,0 +1,107 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with service +package log + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/step" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step}/logs steps DeleteStepLog +// +// Delete the logs for a step +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: step +// description: Step number +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted the logs for the step +// schema: +// type: string +// '500': +// description: Unable to delete the logs for the step +// schema: +// "$ref": "#/definitions/Error" + +// DeleteStepLog represents the API handler to remove +// the logs for a step from the configured backend. +func DeleteStepLog(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := step.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "step": s.GetNumber(), + "user": u.GetName(), + }).Infof("deleting logs for step %s", entry) + + // send API call to capture the step logs + l, err := database.FromContext(c).GetLogForStep(s) + if err != nil { + retErr := fmt.Errorf("unable to get logs for step %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // send API call to remove the log + err = database.FromContext(c).DeleteLog(l) + if err != nil { + retErr := fmt.Errorf("unable to delete logs for step %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("logs deleted for step %s", entry)) +} diff --git a/api/log/doc.go b/api/log/doc.go new file mode 100644 index 000000000..9f619b01c --- /dev/null +++ b/api/log/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package log provides the log handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/log" +package log diff --git a/api/log/get_service.go b/api/log/get_service.go new file mode 100644 index 000000000..2a15ad7e5 --- /dev/null +++ b/api/log/get_service.go @@ -0,0 +1,97 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with step +package log + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/service" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/services/{service}/logs services GetServiceLog +// +// Retrieve the logs for a service +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: service +// description: Service number +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the service logs +// schema: +// "$ref": "#/definitions/Log" +// '500': +// description: Unable to retrieve the service logs +// schema: +// "$ref": "#/definitions/Error" + +// GetServiceLog represents the API handler to capture +// the logs for a service from the configured backend. +func GetServiceLog(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := service.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "service": s.GetNumber(), + "user": u.GetName(), + }).Infof("reading logs for service %s", entry) + + // send API call to capture the service logs + l, err := database.FromContext(c).GetLogForService(s) + if err != nil { + retErr := fmt.Errorf("unable to get logs for service %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, l) +} diff --git a/api/log/get_step.go b/api/log/get_step.go new file mode 100644 index 000000000..39859f1dd --- /dev/null +++ b/api/log/get_step.go @@ -0,0 +1,98 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with service +package log + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/step" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step}/logs steps GetStepLog +// +// Retrieve the logs for a step +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: step +// description: Step number +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the logs for step +// type: json +// schema: +// "$ref": "#/definitions/Log" +// '500': +// description: Unable to retrieve the logs for a step +// schema: +// "$ref": "#/definitions/Error" + +// GetStepLog represents the API handler to capture +// the logs for a step from the configured backend. +func GetStepLog(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := step.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "step": s.GetNumber(), + "user": u.GetName(), + }).Infof("reading logs for step %s", entry) + + // send API call to capture the step logs + l, err := database.FromContext(c).GetLogForStep(s) + if err != nil { + retErr := fmt.Errorf("unable to get logs for step %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, l) +} diff --git a/api/log/list_build.go b/api/log/list_build.go new file mode 100644 index 000000000..fe93947d0 --- /dev/null +++ b/api/log/list_build.go @@ -0,0 +1,134 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/logs builds ListLogsForBuild +// +// List logs for a build in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved logs for the build +// schema: +// type: array +// items: +// "$ref": "#/definitions/Log" +// '500': +// description: Unable to retrieve logs for the build +// schema: +// "$ref": "#/definitions/Error" + +// ListLogsForBuild represents the API handler to capture a +// list of logs for a build from the configured backend. +func ListLogsForBuild(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("listing logs for build %s", entry) + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + //nolint:lll // ignore long line length due to error message + retErr := fmt.Errorf("unable to convert page query parameter for build %s: %w", entry, err) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for build %s: %w", entry, err) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // send API call to capture the list of logs for the build + l, t, err := database.FromContext(c).ListLogsForBuild(b, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to list logs for build %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, l) +} diff --git a/api/log/update_service.go b/api/log/update_service.go new file mode 100644 index 000000000..92d96e1f1 --- /dev/null +++ b/api/log/update_service.go @@ -0,0 +1,137 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with step +package log + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/service" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/repos/{org}/{repo}/builds/{build}/services/{service}/logs services UpdateServiceLog +// +// Update the logs for a service +// +// --- +// deprecated: true +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: service +// description: Service number +// required: true +// type: integer +// - in: body +// name: body +// description: Payload containing the log to update +// required: true +// schema: +// "$ref": "#/definitions/Log" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the service logs +// schema: +// "$ref": "#/definitions/Log" +// '400': +// description: Unable to updated the service logs +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to updates the service logs +// schema: +// "$ref": "#/definitions/Error" + +// UpdateServiceLog represents the API handler to update +// the logs for a service in the configured backend. +func UpdateServiceLog(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := service.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "service": s.GetNumber(), + "user": u.GetName(), + }).Infof("updating logs for service %s", entry) + + // send API call to capture the service logs + l, err := database.FromContext(c).GetLogForService(s) + if err != nil { + retErr := fmt.Errorf("unable to get logs for service %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // capture body from API request + input := new(library.Log) + + err = c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for service %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update log fields if provided + if len(input.GetData()) > 0 { + // update data if set + l.SetData(input.GetData()) + } + + // send API call to update the log + err = database.FromContext(c).UpdateLog(l) + if err != nil { + retErr := fmt.Errorf("unable to update logs for service %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, nil) +} diff --git a/api/log/update_step.go b/api/log/update_step.go new file mode 100644 index 000000000..14a642d57 --- /dev/null +++ b/api/log/update_step.go @@ -0,0 +1,137 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with service +package log + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/step" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step}/logs steps UpdateStepLog +// +// Update the logs for a step +// +// --- +// deprecated: true +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: step +// description: Step number +// required: true +// type: integer +// - in: body +// name: body +// description: Payload containing the log to update +// required: true +// schema: +// "$ref": "#/definitions/Log" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the logs for step +// schema: +// "$ref": "#/definitions/Log" +// '400': +// description: Unable to update the logs for a step +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update the logs for a step +// schema: +// "$ref": "#/definitions/Error" + +// UpdateStepLog represents the API handler to update +// the logs for a step in the configured backend. +func UpdateStepLog(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := step.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "step": s.GetNumber(), + "user": u.GetName(), + }).Infof("updating logs for step %s", entry) + + // send API call to capture the step logs + l, err := database.FromContext(c).GetLogForStep(s) + if err != nil { + retErr := fmt.Errorf("unable to get logs for step %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // capture body from API request + input := new(library.Log) + + err = c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for step %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update log fields if provided + if len(input.GetData()) > 0 { + // update data if set + l.SetData(input.GetData()) + } + + // send API call to update the log + err = database.FromContext(c).UpdateLog(l) + if err != nil { + retErr := fmt.Errorf("unable to update logs for step %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, nil) +} diff --git a/api/metrics.go b/api/metrics.go index 59a5f5f37..08994e65c 100644 --- a/api/metrics.go +++ b/api/metrics.go @@ -8,15 +8,69 @@ import ( "net/http" "time" - "github.com/go-vela/server/database" - "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/queue" + "github.com/go-vela/types/constants" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/sirupsen/logrus" ) +// MetricsQueryParameters holds query parameter information pertaining to requested metrics. +type MetricsQueryParameters struct { + // UserCount represents total platform users + UserCount bool `form:"user_count"` + // RepoCount represents total platform repos + RepoCount bool `form:"repo_count"` + + // BuildCount represents total number of builds + BuildCount bool `form:"build_count"` + // RunningBuildCount represents total number of builds with status==running + RunningBuildCount bool `form:"running_build_count"` + // PendingBuildCount represents total number of builds with status==pending + PendingBuildCount bool `form:"pending_build_count"` + // QueuedBuildCount represents total number of builds currently in the queue + QueuedBuildCount bool `form:"queued_build_count"` + // FailureBuildCount represents total number of builds with status==failure + FailureBuildCount bool `form:"failure_build_count"` + // KilledBuildCount represents total number of builds with status==killed + KilledBuildCount bool `form:"killed_build_count"` + // SuccessBuildCount represents total number of builds with status==success + SuccessBuildCount bool `form:"success_build_count"` + // ErrorBuildCount represents total number of builds with status==error + ErrorBuildCount bool `form:"error_build_count"` + + // StepImageCount represents total number of step images + StepImageCount bool `form:"step_image_count"` + // StepStatusCount represents total number of step statuses + StepStatusCount bool `form:"step_status_count"` + // ServiceImageCount represents total number of service images + ServiceImageCount bool `form:"service_image_count"` + // ServiceStatusCount represents total number of service statuses + ServiceStatusCount bool `form:"service_status_count"` + + // WorkerBuildLimit represents total worker build limit + WorkerBuildLimit bool `form:"worker_build_limit"` + // ActiveWorkerCount represents total number of active workers + ActiveWorkerCount bool `form:"active_worker_count"` + // InactiveWorkerCount represents total number of inactive workers + InactiveWorkerCount bool `form:"inactive_worker_count"` + + // IdleWorkerCount represents total number of workers with a status of idle + // where worker RunningBuildIDs.length = 0 + IdleWorkerCount bool `form:"idle_worker_count"` + // AvailableWorkerCount represents total number of workers with a status of available, + // where worker RunningBuildIDs.length > 0 and < worker BuildLimit + AvailableWorkerCount bool `form:"available_worker_count"` + // BusyWorkerCount represents total number of workers with a status of busy, + // where worker BuildLimit == worker RunningBuildIDs.length + BusyWorkerCount bool `form:"busy_worker_count"` + // ErrorWorkerCount represents total number of workers with a status of error + ErrorWorkerCount bool `form:"error_worker_count"` +} + // predefine Prometheus metrics else they will be regenerated // each function call which will throw error: // "duplicate metrics collector registration attempted". @@ -54,6 +108,111 @@ var ( // produces: // - text/plain // parameters: +// - in: query +// name: user_count +// description: Indicates a request for user count +// type: boolean +// default: false +// - in: query +// name: repo_count +// description: Indicates a request for repo count +// type: boolean +// default: false +// - in: query +// name: build_count +// description: Indicates a request for build count +// type: boolean +// default: false +// - in: query +// name: running_build_count +// description: Indicates a request for running build count +// type: boolean +// default: false +// - in: query +// name: pending_build_count +// description: Indicates a request for pending build count +// type: boolean +// default: false +// - in: query +// name: queued_build_count +// description: Indicates a request for queued build count +// type: boolean +// default: false +// - in: query +// name: failure_build_count +// description: Indicates a request for failure build count +// type: boolean +// default: false +// - in: query +// name: killed_build_count +// description: Indicates a request for killed build count +// type: boolean +// default: false +// - in: query +// name: success_build_count +// description: Indicates a request for success build count +// type: boolean +// default: false +// - in: query +// name: error_build_count +// description: Indicates a request for error build count +// type: boolean +// default: false +// - in: query +// name: step_image_count +// description: Indicates a request for step image count +// type: boolean +// default: false +// - in: query +// name: step_status_count +// description: Indicates a request for step status count +// type: boolean +// default: false +// - in: query +// name: service_image_count +// description: Indicates a request for service image count +// type: boolean +// default: false +// - in: query +// name: service_status_count +// description: Indicates a request for service status count +// type: boolean +// default: false +// - in: query +// name: worker_build_limit +// description: Indicates a request for total worker build limit +// type: boolean +// default: false +// - in: query +// name: active_worker_count +// description: Indicates a request for active worker count +// type: boolean +// default: false +// - in: query +// name: inactive_worker_count +// description: Indicates a request for inactive worker count +// type: boolean +// default: false +// - in: query +// name: idle_worker_count +// description: Indicates a request for idle worker count +// type: boolean +// default: false +// - in: query +// name: available_worker_count +// description: Indicates a request for available worker count +// type: boolean +// default: false +// - in: query +// name: busy_worker_count +// description: Indicates a request for busy worker count +// type: boolean +// default: false +// - in: query +// name: error_worker_count +// description: Indicates a request for error worker count +// type: boolean +// default: false // responses: // '200': // description: Successfully retrieved the Vela metrics @@ -73,142 +232,266 @@ func CustomMetrics(c *gin.Context) { // helper function to get the totals of resource types. // -// nolint: funlen // ignore function length due to comments +//nolint:funlen,gocyclo // ignore function length and cyclomatic complexity func recordGauges(c *gin.Context) { - // send API call to capture the total number of users - u, err := database.FromContext(c).GetUserCount() - if err != nil { - logrus.Errorf("unable to get count of all users: %v", err) - } + // capture middleware values + ctx := c.Request.Context() - // send API call to capture the total number of repos - r, err := database.FromContext(c).GetRepoCount() + // variable to store query parameters + q := MetricsQueryParameters{} + + // take incoming request and bind query parameters + err := c.ShouldBindQuery(&q) if err != nil { - logrus.Errorf("unable to get count of all repos: %v", err) + logrus.Errorf("unable to get bind query parameters: %v", err) + } // continue execution with parameters defaulted to false + + // get each metric separately based on request query parameters + // user_count + if q.UserCount { + // send API call to capture the total number of users + u, err := database.FromContext(c).CountUsers() + if err != nil { + logrus.Errorf("unable to get count of all users: %v", err) + } + // add platform metrics + totals.WithLabelValues("platform", "count", "users").Set(float64(u)) } - // send API call to capture the total number of builds - b, err := database.FromContext(c).GetBuildCount() - if err != nil { - logrus.Errorf("unable to get count of all builds: %v", err) + // repo_count + if q.RepoCount { + // send API call to capture the total number of repos + r, err := database.FromContext(c).CountRepos(ctx) + if err != nil { + logrus.Errorf("unable to get count of all repos: %v", err) + } + // add platform metrics + totals.WithLabelValues("platform", "count", "repos").Set(float64(r)) } - // send API call to capture the total number of running builds - bRun, err := database.FromContext(c).GetBuildCountByStatus("running") - if err != nil { - logrus.Errorf("unable to get count of all running builds: %v", err) + // build_count + if q.BuildCount { + // send API call to capture the total number of builds + b, err := database.FromContext(c).CountBuilds(ctx) + if err != nil { + logrus.Errorf("unable to get count of all builds: %v", err) + } + // add platform metrics + totals.WithLabelValues("platform", "count", "builds").Set(float64(b)) } - // send API call to capture the total number of pending builds - bPen, err := database.FromContext(c).GetBuildCountByStatus("pending") - if err != nil { - logrus.Errorf("unable to get count of all pending builds: %v", err) + // running_build_count + if q.RunningBuildCount { + // send API call to capture the total number of running builds + bRun, err := database.FromContext(c).CountBuildsForStatus(ctx, "running", nil) + if err != nil { + logrus.Errorf("unable to get count of all running builds: %v", err) + } + // add build metrics + totals.WithLabelValues("build", "status", "running").Set(float64(bRun)) } - // send API call to capture the total number of failure builds - bFail, err := database.FromContext(c).GetBuildCountByStatus("failure") - if err != nil { - logrus.Errorf("unable to get count of all failure builds: %v", err) + // pending_build_count + if q.PendingBuildCount { + // send API call to capture the total number of pending builds + bPen, err := database.FromContext(c).CountBuildsForStatus(ctx, "pending", nil) + if err != nil { + logrus.Errorf("unable to get count of all pending builds: %v", err) + } + // add build metrics + totals.WithLabelValues("build", "status", "pending").Set(float64(bPen)) } - // send API call to capture the total number of killed builds - bKill, err := database.FromContext(c).GetBuildCountByStatus("killed") - if err != nil { - logrus.Errorf("unable to get count of all killed builds: %v", err) + // queued_build_count + if q.QueuedBuildCount { + // send API call to capture the total number of queued builds + t, err := queue.FromContext(c).Length(c) + if err != nil { + logrus.Errorf("unable to get count of all queued builds: %v", err) + } + + totals.WithLabelValues("build", "status", "queued").Set(float64(t)) } - // send API call to capture the total number of success builds - bSucc, err := database.FromContext(c).GetBuildCountByStatus("success") - if err != nil { - logrus.Errorf("unable to get count of all success builds: %v", err) + // failure_build_count + if q.FailureBuildCount { + // send API call to capture the total number of failure builds + bFail, err := database.FromContext(c).CountBuildsForStatus(ctx, "failure", nil) + if err != nil { + logrus.Errorf("unable to get count of all failure builds: %v", err) + } + // add build metrics + totals.WithLabelValues("build", "status", "failed").Set(float64(bFail)) } - // send API call to capture the total number of error builds - bErr, err := database.FromContext(c).GetBuildCountByStatus("error") - if err != nil { - logrus.Errorf("unable to get count of all error builds: %v", err) + // killed_build_count + if q.KilledBuildCount { + // send API call to capture the total number of killed builds + bKill, err := database.FromContext(c).CountBuildsForStatus(ctx, "killed", nil) + if err != nil { + logrus.Errorf("unable to get count of all killed builds: %v", err) + } + // add build metrics + totals.WithLabelValues("build", "status", "killed").Set(float64(bKill)) } - // send API call to capture the total number of step images - stepImageMap, err := database.FromContext(c).GetStepImageCount() - if err != nil { - logrus.Errorf("unable to get count of all step images: %v", err) + // success_build_count + if q.SuccessBuildCount { + // send API call to capture the total number of success builds + bSucc, err := database.FromContext(c).CountBuildsForStatus(ctx, "success", nil) + if err != nil { + logrus.Errorf("unable to get count of all success builds: %v", err) + } + // add build metrics + totals.WithLabelValues("build", "status", "success").Set(float64(bSucc)) } - // send API call to capture the total number of step statuses - stepStatusMap, err := database.FromContext(c).GetStepStatusCount() - if err != nil { - logrus.Errorf("unable to get count of all step statuses: %v", err) + // error_build_count + if q.ErrorBuildCount { + // send API call to capture the total number of error builds + bErr, err := database.FromContext(c).CountBuildsForStatus(ctx, "error", nil) + if err != nil { + logrus.Errorf("unable to get count of all error builds: %v", err) + } + // add build metrics + totals.WithLabelValues("build", "status", "error").Set(float64(bErr)) } - // send API call to capture the total number of service images - serviceImageMap, err := database.FromContext(c).GetServiceImageCount() - if err != nil { - logrus.Errorf("unable to get count of all service images: %v", err) + // step_image_count + if q.StepImageCount { + // send API call to capture the total number of step images + stepImageMap, err := database.FromContext(c).ListStepImageCount() + if err != nil { + logrus.Errorf("unable to get count of all step images: %v", err) + } + // add step image metrics + for image, count := range stepImageMap { + stepImages.WithLabelValues(image).Set(count) + } } - // send API call to capture the total number of service statuses - serviceStatusMap, err := database.FromContext(c).GetServiceStatusCount() - if err != nil { - logrus.Errorf("unable to get count of all service statuses: %v", err) + // step_status_count + if q.StepStatusCount { + // send API call to capture the total number of step statuses + stepStatusMap, err := database.FromContext(c).ListStepStatusCount() + if err != nil { + logrus.Errorf("unable to get count of all step statuses: %v", err) + } + // add step status metrics + for status, count := range stepStatusMap { + totals.WithLabelValues("steps", "status", status).Set(count) + } } - // send API call to capture the workers - workers, err := database.FromContext(c).GetWorkerList() - if err != nil { - logrus.Errorf("unable to get workers: %v", err) + // service_image_count + if q.ServiceImageCount { + // send API call to capture the total number of service images + serviceImageMap, err := database.FromContext(c).ListServiceImageCount() + if err != nil { + logrus.Errorf("unable to get count of all service images: %v", err) + } + // add service image metrics + for image, count := range serviceImageMap { + serviceImages.WithLabelValues(image).Set(count) + } } - // Add platform metrics - totals.WithLabelValues("platform", "count", "users").Set(float64(u)) - totals.WithLabelValues("platform", "count", "repos").Set(float64(r)) - totals.WithLabelValues("platform", "count", "builds").Set(float64(b)) - - // Add build metrics - totals.WithLabelValues("build", "status", "running").Set(float64(bRun)) - totals.WithLabelValues("build", "status", "pending").Set(float64(bPen)) - totals.WithLabelValues("build", "status", "failed").Set(float64(bFail)) - totals.WithLabelValues("build", "status", "killed").Set(float64(bKill)) - totals.WithLabelValues("build", "status", "success").Set(float64(bSucc)) - totals.WithLabelValues("build", "status", "error").Set(float64(bErr)) - - // Add worker metrics - var buildLimit int64 - var activeWorkers int64 - var inactiveWorkers int64 - // get the unix time from worker_active_interval ago - before := time.Now().UTC().Add(-c.Value("worker_active_interval").(time.Duration)).Unix() - for _, worker := range workers { - // check if the worker checked in within the last worker_active_interval - if worker.GetLastCheckedIn() >= before { - buildLimit += worker.GetBuildLimit() - activeWorkers++ - } else { - inactiveWorkers++ + // service_status_count + if q.ServiceStatusCount { + // send API call to capture the total number of service statuses + serviceStatusMap, err := database.FromContext(c).ListServiceStatusCount() + if err != nil { + logrus.Errorf("unable to get count of all service statuses: %v", err) + } + // add service status metrics + for status, count := range serviceStatusMap { + totals.WithLabelValues("services", "status", status).Set(count) } } - totals.WithLabelValues("worker", "sum", "build_limit").Set(float64(buildLimit)) - totals.WithLabelValues("worker", "count", "active").Set(float64(activeWorkers)) - totals.WithLabelValues("worker", "count", "inactive").Set(float64(inactiveWorkers)) + // add worker metrics + var ( + buildLimit int64 + activeWorkers int64 + inactiveWorkers int64 + idleWorkers int64 + availableWorkers int64 + busyWorkers int64 + errorWorkers int64 + ) - // Add step status metrics - for status, count := range stepStatusMap { - totals.WithLabelValues("steps", "status", status).Set(count) - } + // get worker metrics based on request query parameters + // worker_build_limit, active_worker_count, inactive_worker_count, idle_worker_count, available_worker_count, busy_worker_count, error_worker_count + if q.WorkerBuildLimit || q.ActiveWorkerCount || q.InactiveWorkerCount || q.IdleWorkerCount || q.AvailableWorkerCount || q.BusyWorkerCount || q.ErrorWorkerCount { + // send API call to capture the workers + workers, err := database.FromContext(c).ListWorkers() + if err != nil { + logrus.Errorf("unable to get workers: %v", err) + } - // Add service status metrics - for status, count := range serviceStatusMap { - totals.WithLabelValues("services", "status", status).Set(count) - } + // get the unix time from worker_active_interval ago + before := time.Now().UTC().Add(-c.Value("worker_active_interval").(time.Duration)).Unix() + + // active, inactive counts + // idle, available, busy, error counts + for _, worker := range workers { + // check if the worker checked in within the last worker_active_interval + if worker.GetLastCheckedIn() >= before { + buildLimit += worker.GetBuildLimit() + activeWorkers++ + } else { + inactiveWorkers++ + } + // check if the worker checked in within the last worker_active_interval + if worker.GetLastCheckedIn() >= before { + + switch worker.GetStatus() { + case constants.WorkerStatusIdle: + idleWorkers++ + case constants.WorkerStatusAvailable: + availableWorkers++ + case constants.WorkerStatusBusy: + busyWorkers++ + case constants.WorkerStatusError: + errorWorkers++ + } + } + } - // Add step image metrics - for image, count := range stepImageMap { - stepImages.WithLabelValues(image).Set(count) - } + // apply metrics based on request query parameters + // worker_build_limit + if q.WorkerBuildLimit { + totals.WithLabelValues("worker", "sum", "build_limit").Set(float64(buildLimit)) + } + + // active_worker_count + if q.ActiveWorkerCount { + totals.WithLabelValues("worker", "count", "active").Set(float64(activeWorkers)) + } + + // inactive_worker_count + if q.InactiveWorkerCount { + totals.WithLabelValues("worker", "count", "inactive").Set(float64(inactiveWorkers)) + } - // Add service image metrics - for image, count := range serviceImageMap { - serviceImages.WithLabelValues(image).Set(count) + // idle_worker_count + if q.IdleWorkerCount { + totals.WithLabelValues("worker", "count", "idle").Set(float64(idleWorkers)) + } + + // available_worker_count + if q.AvailableWorkerCount { + totals.WithLabelValues("worker", "count", "available").Set(float64(availableWorkers)) + } + + // busy_worker_count + if q.BusyWorkerCount { + totals.WithLabelValues("worker", "count", "busy").Set(float64(busyWorkers)) + } + + // error_worker_count + if q.ErrorWorkerCount { + totals.WithLabelValues("worker", "count", "error").Set(float64(errorWorkers)) + } } } diff --git a/api/pagination.go b/api/pagination.go index e7261d563..a63af8214 100644 --- a/api/pagination.go +++ b/api/pagination.go @@ -70,7 +70,6 @@ func (p *Pagination) SetHeaderLink(c *gin.Context) { l = append(l, ls) } - // nolint: gomnd // ignore magic number c.Header("X-Total-Count", strconv.FormatInt(p.Total, 10)) c.Header("Link", strings.Join(l, ", ")) } @@ -121,7 +120,7 @@ func (p *Pagination) TotalPages() int { // resolveScheme is a helper to determine the protocol scheme // c.Request.URL.Scheme does not seem to reliably provide this. // -// nolint: goconst // ignore making constant for https +//nolint:goconst // ignore making constant for https func resolveScheme(r *http.Request) string { switch { case r.Header.Get("X-Forwarded-Proto") == "https": diff --git a/api/pipeline.go b/api/pipeline.go deleted file mode 100644 index fdae72790..000000000 --- a/api/pipeline.go +++ /dev/null @@ -1,567 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "fmt" - "net/http" - "strconv" - "strings" - - "github.com/gin-gonic/gin" - "github.com/go-vela/server/compiler" - "github.com/go-vela/server/compiler/registry/github" - "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/router/middleware/repo" - "github.com/go-vela/server/router/middleware/user" - "github.com/go-vela/server/scm" - "github.com/go-vela/server/util" - "github.com/go-vela/types" - "github.com/go-vela/types/library" - "github.com/go-vela/types/yaml" - "github.com/sirupsen/logrus" -) - -const ( - outputJSON = "json" - outputYAML = "yaml" -) - -// swagger:operation GET /api/v1/pipelines/{org}/{repo} pipelines GetPipeline -// -// Get a pipeline configuration from the source provider -// -// --- -// produces: -// - application/x-yaml -// - application/json -// parameters: -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: query -// name: ref -// description: Ref for retrieving pipeline configuration file -// type: string -// - in: query -// name: output -// description: Output string for specifying output format -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the pipeline -// schema: -// "$ref": "#/definitions/PipelineBuild" -// '400': -// description: Unable to retrieve the pipeline configuration templates -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to retrieve the pipeline configuration templates -// schema: -// "$ref": "#/definitions/Error" - -// GetPipeline represents the API handler to capture a -// pipeline configuration for a repo from the the source provider. -func GetPipeline(ctx *gin.Context) { - // capture middleware values - o := org.Retrieve(ctx) - r := repo.Retrieve(ctx) - u := user.Retrieve(ctx) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading pipeline for repo %s", r.GetFullName()) - - pipeline, _, err := getUnprocessedPipeline(ctx) - if err != nil { - util.HandleError(ctx, http.StatusBadRequest, err) - return - } - - writeOutput(ctx, pipeline) -} - -// swagger:operation GET /api/v1/pipelines/{org}/{repo}/templates pipelines GetTemplates -// -// Get a map of templates utilized by a pipeline configuration from the source provider -// -// --- -// produces: -// - application/x-yaml -// - application/json -// parameters: -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: query -// name: ref -// description: Ref for retrieving pipeline configuration file -// type: string -// - in: query -// name: output -// description: Output string for specifying output format -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the map of pipeline templates -// schema: -// "$ref": "#/definitions/Template" -// '400': -// description: Unable to retrieve the pipeline configuration templates -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to retrieve the pipeline configuration templates -// schema: -// "$ref": "#/definitions/Error" - -// GetTemplates represents the API handler to capture a -// map of templates utilized by a pipeline configuration. -func GetTemplates(ctx *gin.Context) { - // capture middleware values - o := org.Retrieve(ctx) - r := repo.Retrieve(ctx) - u := user.Retrieve(ctx) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading templates from pipeline for repo %s", r.GetFullName()) - - pipeline, _, err := getUnprocessedPipeline(ctx) - if err != nil { - util.HandleError(ctx, http.StatusBadRequest, err) - return - } - - // create map of templates for response body - templates, err := getTemplateLinks(ctx, pipeline.Templates) - if err != nil { - retErr := fmt.Errorf("unable to set template links for %s: %w", repoName(ctx), err) - util.HandleError(ctx, http.StatusBadRequest, retErr) - return - } - - writeOutput(ctx, templates) -} - -// swagger:operation POST /api/v1/pipelines/{org}/{repo}/expand pipelines ExpandPipeline -// -// Get and expand a pipeline configuration from the source provider -// -// --- -// produces: -// - application/x-yaml -// - application/json -// parameters: -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: query -// name: ref -// description: Ref for retrieving pipeline configuration file -// type: string -// - in: query -// name: output -// description: Output string for specifying output format -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved and expanded the pipeline -// type: json -// schema: -// "$ref": "#/definitions/PipelineBuild" -// '400': -// description: Unable to expand the pipeline configuration -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to retrieve the pipeline configuration -// schema: -// "$ref": "#/definitions/Error" - -// ExpandPipeline represents the API handler to capture and -// expand a pipeline configuration. -func ExpandPipeline(ctx *gin.Context) { - // capture middleware values - o := org.Retrieve(ctx) - r := repo.Retrieve(ctx) - u := user.Retrieve(ctx) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("expanding templates from pipeline for repo %s", r.GetFullName()) - - pipeline, comp, err := getUnprocessedPipeline(ctx) - if err != nil { - util.HandleError(ctx, http.StatusBadRequest, err) - return - } - - if err := expandPipeline(ctx, pipeline, comp, false); err != nil { - util.HandleError(ctx, http.StatusBadRequest, err) - return - } - - writeOutput(ctx, pipeline) -} - -// swagger:operation POST /api/v1/pipelines/{org}/{repo}/validate pipelines ValidatePipeline -// -// Get, expand and validate a pipeline configuration from the source provider -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: query -// name: ref -// description: Ref for retrieving pipeline configuration file -// type: string -// - in: query -// name: output -// description: Output string for specifying output format -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved, expanded and validated the pipeline -// schema: -// type: string -// '400': -// description: Unable to validate the pipeline configuration -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to retrieve the pipeline configuration -// schema: -// "$ref": "#/definitions/Error" - -// ValidatePipeline represents the API handler to capture, expand and -// validate a pipeline configuration. -func ValidatePipeline(ctx *gin.Context) { - // capture middleware values - o := org.Retrieve(ctx) - r := repo.Retrieve(ctx) - u := user.Retrieve(ctx) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("validating pipeline for repo %s", r.GetFullName()) - - pipeline, comp, err := getUnprocessedPipeline(ctx) - if err != nil { - util.HandleError(ctx, http.StatusBadRequest, err) - return - } - - // check optional template query parameter - if ok, _ := strconv.ParseBool(ctx.DefaultQuery("template", "true")); ok { - if err := expandPipeline(ctx, pipeline, comp, false); err != nil { - util.HandleError(ctx, http.StatusBadRequest, err) - return - } - } - - // validate the yaml configuration - if err = comp.Validate(pipeline); err != nil { - retErr := fmt.Errorf("unable to validate pipeline configuration for %s: %w", repoName(ctx), err) - util.HandleError(ctx, http.StatusBadRequest, retErr) - return - } - - writeOutput(ctx, pipeline) -} - -// swagger:operation POST /api/v1/pipelines/{org}/{repo}/compile pipelines CompilePipeline -// -// Get, expand and compile a pipeline configuration from the source provider -// -// --- -// produces: -// - application/x-yaml -// - application/json -// parameters: -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: query -// name: ref -// description: Ref for retrieving pipeline configuration file -// type: string -// - in: query -// name: output -// description: Output string for specifying output format -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved and compiled the pipeline -// schema: -// "$ref": "#/definitions/PipelineBuild" -// '400': -// description: Unable to validate the pipeline configuration -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to retrieve the pipeline configuration -// schema: -// "$ref": "#/definitions/Error" - -// CompilePipeline represents the API handler to capture, -// expand and compile a pipeline configuration. -// -func CompilePipeline(ctx *gin.Context) { - // capture middleware values - o := org.Retrieve(ctx) - r := repo.Retrieve(ctx) - u := user.Retrieve(ctx) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("compiling pipeline for repo %s", r.GetFullName()) - - pipeline, comp, err := getUnprocessedPipeline(ctx) - if err != nil { - util.HandleError(ctx, http.StatusBadRequest, err) - return - } - - if err := expandPipeline(ctx, pipeline, comp, true); err != nil { - util.HandleError(ctx, http.StatusBadRequest, err) - return - } - - // validate the yaml configuration - if err = comp.Validate(pipeline); err != nil { - retErr := fmt.Errorf("unable to validate pipeline configuration for %s: %w", repoName(ctx), err) - util.HandleError(ctx, http.StatusBadRequest, retErr) - return - } - - writeOutput(ctx, pipeline) -} - -// getUnprocessedPipeline retrieves the unprocessed pipeline from a given context. -func getUnprocessedPipeline(ctx *gin.Context) (*yaml.Build, compiler.Engine, error) { - // capture middleware values - meta := ctx.MustGet("metadata").(*types.Metadata) - repo := repo.Retrieve(ctx) - - // capture query parameters - ref := ctx.DefaultQuery("ref", repo.GetBranch()) - - // send API call to capture the repo owner - user, err := database.FromContext(ctx).GetUser(repo.GetUserID()) - if err != nil { - return nil, nil, fmt.Errorf("unable to get owner for %s: %w", repo.GetFullName(), err) - } - - // send API call to capture the pipeline configuration file - config, err := scm.FromContext(ctx).ConfigBackoff(user, repo, ref) - if err != nil { - return nil, nil, fmt.Errorf("unable to get pipeline configuration for %s: %w", repoName(ctx), err) - } - - // create the compiler with extra information embedded into it - comp := compiler.FromContext(ctx). - WithMetadata(meta). - WithRepo(repo). - WithUser(user) - - pipeline, err := comp.Parse(config) - if err != nil { - // nolint: lll // ignore long line length due to error message - return nil, nil, fmt.Errorf("unable to parse pipeline configuration for %s: %w", repoName(ctx), err) - } - - return pipeline, comp, nil -} - -// getTemplateLinks helper function that retrieves source provider links -// for a list of templates and returns a map of library templates. -// -// nolint: lll // ignore long line length due to variable names -func getTemplateLinks(ctx *gin.Context, templates yaml.TemplateSlice) (map[string]*library.Template, error) { - r := repo.Retrieve(ctx) - u, err := database.FromContext(ctx).GetUser(r.GetUserID()) - if err != nil { - return nil, err - } - - m := make(map[string]*library.Template) - for _, t := range templates { - // convert to library type - tmpl := t.ToLibrary() - - // create a new compiler github client for parsing, - // no address or token needed for Parse - cl, err := github.New("", "") - if err != nil { - return nil, fmt.Errorf("unable to create compiler github client: %w", err) - } - - // parse template source - src, err := cl.Parse(tmpl.GetSource()) - if err != nil { - return nil, fmt.Errorf("unable to parse source for %s: %w", tmpl.GetSource(), err) - } - - // retrieve link to template file from github - link, err := scm.FromContext(ctx).GetHTMLURL(u, src.Org, src.Repo, src.Name, src.Ref) - if err != nil { - return nil, fmt.Errorf("unable to get html url for %s/%s/%s/@%s: %w", src.Org, src.Repo, src.Name, src.Ref, err) - } - - // set link to template file - tmpl.SetLink(link) - - m[tmpl.GetName()] = tmpl - } - - return m, nil -} - -// repoName takes the given context and returns a string friendly -// representation with the format of 'repository@reference'. -func repoName(ctx *gin.Context) string { - repo := repo.Retrieve(ctx) - ref := ctx.DefaultQuery("ref", repo.GetBranch()) - - return fmt.Sprintf("%s@%s", repo.GetFullName(), ref) -} - -// writeOutput returns writes output to the request based on the preferred -// output as defined in the request's 'output' query defaulting to YAML. -func writeOutput(ctx *gin.Context, pipeline interface{}) { - output := ctx.DefaultQuery("output", outputYAML) - - // format response body based off output query parameter - switch strings.ToLower(output) { - case outputJSON: - ctx.JSON(http.StatusOK, pipeline) - case outputYAML: - fallthrough - default: - ctx.YAML(http.StatusOK, pipeline) - } -} - -// expandPipeline uses a given pipeline and compiler to expand stages and steps -// in the pipeline along with optionally substituting the environmental variables. -// -// nolint: lll // ignore long line length due to variable names -func expandPipeline(ctx *gin.Context, pipeline *yaml.Build, comp compiler.Engine, substituteEnv bool) error { - // create map of templates for easy lookup - templates := pipeline.Templates.Map() - - var err error - - if len(pipeline.Stages) > 0 { - // inject the templates into the stages - pipeline.Stages, pipeline.Secrets, pipeline.Services, pipeline.Environment, err = comp.ExpandStages(pipeline, templates) - if err != nil { - return fmt.Errorf("unable to expand stages in pipeline configuration for %s: %w", repoName(ctx), err) - } - - if substituteEnv { - // inject the substituted environment variables into the stages - pipeline.Stages, err = comp.SubstituteStages(pipeline.Stages) - if err != nil { - // nolint: lll // ignore long line length due to error message - return fmt.Errorf("unable to substitute stages in pipeline configuration for %s: %w", repoName(ctx), err) - } - } - } else { - // inject the templates into the steps - pipeline.Steps, pipeline.Secrets, pipeline.Services, pipeline.Environment, err = comp.ExpandSteps(pipeline, templates) - if err != nil { - return fmt.Errorf("unable to expand steps in pipeline configuration for %s: %w", repoName(ctx), err) - } - - if substituteEnv { - // inject the substituted environment variables into the steps - pipeline.Steps, err = comp.SubstituteSteps(pipeline.Steps) - if err != nil { - // nolint: lll // ignore long line length due to error message - return fmt.Errorf("unable to substitute steps in pipeline configuration for %s: %w", repoName(ctx), err) - } - } - } - - return nil -} diff --git a/api/pipeline/compile.go b/api/pipeline/compile.go new file mode 100644 index 000000000..8ac5fed93 --- /dev/null +++ b/api/pipeline/compile.go @@ -0,0 +1,110 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with expand +package pipeline + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/compiler" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/pipeline" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/pipelines/{org}/{repo}/{pipeline}/compile pipelines CompilePipeline +// +// Get, expand and compile a pipeline from the configured backend +// +// --- +// produces: +// - application/x-yaml +// - application/json +// parameters: +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: pipeline +// description: Commit SHA for pipeline to retrieve +// required: true +// type: string +// - in: query +// name: output +// description: Output string for specifying output format +// type: string +// default: yaml +// enum: +// - json +// - yaml +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved and compiled the pipeline +// schema: +// "$ref": "#/definitions/PipelineBuild" +// '400': +// description: Unable to validate the pipeline configuration +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to retrieve the pipeline configuration +// schema: +// "$ref": "#/definitions/Error" + +// CompilePipeline represents the API handler to capture, +// expand and compile a pipeline configuration. +func CompilePipeline(c *gin.Context) { + // capture middleware values + m := c.MustGet("metadata").(*types.Metadata) + o := org.Retrieve(c) + p := pipeline.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), p.GetCommit()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "pipeline": p.GetCommit(), + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("compiling pipeline %s", entry) + + // ensure we use the expected pipeline type when compiling + r.SetPipelineType(p.GetType()) + + // create the compiler object + compiler := compiler.FromContext(c).Duplicate().WithCommit(p.GetCommit()).WithMetadata(m).WithRepo(r).WithUser(u) + + // compile the pipeline + pipeline, _, err := compiler.CompileLite(p.GetData(), true, true) + if err != nil { + retErr := fmt.Errorf("unable to compile pipeline %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + writeOutput(c, pipeline) +} diff --git a/api/pipeline/create.go b/api/pipeline/create.go new file mode 100644 index 000000000..87bfb6634 --- /dev/null +++ b/api/pipeline/create.go @@ -0,0 +1,112 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/pipelines/{org}/{repo} pipelines CreatePipeline +// +// Create a pipeline in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: body +// name: body +// description: Payload containing the pipeline to create +// required: true +// schema: +// "$ref": "#/definitions/Pipeline" +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: Successfully created the pipeline +// type: json +// schema: +// "$ref": "#/definitions/Pipeline" +// '400': +// description: Unable to create the pipeline +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to create the pipeline +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the pipeline +// schema: +// "$ref": "#/definitions/Error" + +// CreatePipeline represents the API handler to +// create a pipeline in the configured backend. +func CreatePipeline(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logger := logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }) + + logger.Infof("creating new pipeline for repo %s", r.GetFullName()) + + // capture body from API request + input := new(library.Pipeline) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for new build for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update fields in pipeline object + input.SetRepoID(r.GetID()) + + // send API call to create the pipeline + p, err := database.FromContext(c).CreatePipeline(ctx, input) + if err != nil { + retErr := fmt.Errorf("unable to create pipeline %s/%s: %w", r.GetFullName(), input.GetCommit(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusCreated, p) +} diff --git a/api/pipeline/delete.go b/api/pipeline/delete.go new file mode 100644 index 000000000..1012b3094 --- /dev/null +++ b/api/pipeline/delete.go @@ -0,0 +1,93 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/pipeline" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/pipelines/{org}/{repo}/{pipeline} pipelines DeletePipeline +// +// Delete a pipeline from the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: pipeline +// description: Commit SHA for pipeline to delete +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted the pipeline +// schema: +// type: string +// '400': +// description: Unable to delete the pipeline +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to delete the pipeline +// schema: +// "$ref": "#/definitions/Error" + +// DeletePipeline represents the API handler to remove +// a pipeline for a repo from the configured backend. +func DeletePipeline(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + p := pipeline.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), p.GetCommit()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "pipeline": p.GetCommit(), + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("deleting pipeline %s", entry) + + // send API call to remove the build + err := database.FromContext(c).DeletePipeline(ctx, p) + if err != nil { + retErr := fmt.Errorf("unable to delete pipeline %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("pipeline %s deleted", entry)) +} diff --git a/api/pipeline/doc.go b/api/pipeline/doc.go new file mode 100644 index 000000000..0b2c02901 --- /dev/null +++ b/api/pipeline/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package pipeline provides the pipeline handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/pipeline" +package pipeline diff --git a/api/pipeline/expand.go b/api/pipeline/expand.go new file mode 100644 index 000000000..17f782fee --- /dev/null +++ b/api/pipeline/expand.go @@ -0,0 +1,111 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with compile +package pipeline + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/compiler" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/pipeline" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/pipelines/{org}/{repo}/{pipeline}/expand pipelines ExpandPipeline +// +// Get and expand a pipeline from the configured backend +// +// --- +// produces: +// - application/x-yaml +// - application/json +// parameters: +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: pipeline +// description: Commit SHA for pipeline to retrieve +// required: true +// type: string +// - in: query +// name: output +// description: Output string for specifying output format +// type: string +// default: yaml +// enum: +// - json +// - yaml +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved and expanded the pipeline +// type: json +// schema: +// "$ref": "#/definitions/PipelineBuild" +// '400': +// description: Unable to expand the pipeline configuration +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to retrieve the pipeline configuration +// schema: +// "$ref": "#/definitions/Error" + +// ExpandPipeline represents the API handler to capture and +// expand a pipeline configuration. +func ExpandPipeline(c *gin.Context) { + // capture middleware values + m := c.MustGet("metadata").(*types.Metadata) + o := org.Retrieve(c) + p := pipeline.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), p.GetCommit()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "pipeline": p.GetCommit(), + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("expanding templates for pipeline %s", entry) + + // ensure we use the expected pipeline type when compiling + r.SetPipelineType(p.GetType()) + + // create the compiler object + compiler := compiler.FromContext(c).Duplicate().WithCommit(p.GetCommit()).WithMetadata(m).WithRepo(r).WithUser(u) + + // expand the templates in the pipeline + pipeline, _, err := compiler.CompileLite(p.GetData(), true, false) + if err != nil { + retErr := fmt.Errorf("unable to expand pipeline %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + writeOutput(c, pipeline) +} diff --git a/api/pipeline/get.go b/api/pipeline/get.go new file mode 100644 index 000000000..45beb61dc --- /dev/null +++ b/api/pipeline/get.go @@ -0,0 +1,70 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/pipeline" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/pipelines/{org}/{repo}/{pipeline} pipelines GetPipeline +// +// Get a pipeline from the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: pipeline +// description: Commit SHA for pipeline to retrieve +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the pipeline +// type: json +// schema: +// "$ref": "#/definitions/Pipeline" + +// GetPipeline represents the API handler to capture +// a pipeline for a repo from the configured backend. +func GetPipeline(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + p := pipeline.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "pipeline": p.GetCommit(), + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("reading pipeline %s/%s", r.GetFullName(), p.GetCommit()) + + c.JSON(http.StatusOK, p) +} diff --git a/api/pipeline/list.go b/api/pipeline/list.go new file mode 100644 index 000000000..a43f33788 --- /dev/null +++ b/api/pipeline/list.go @@ -0,0 +1,140 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/pipelines/{org}/{repo} pipelines ListPipelines +// +// List pipelines from the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the pipelines +// schema: +// type: array +// items: +// "$ref": "#/definitions/Pipeline" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve the list of pipelines +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the list of pipelines +// schema: +// "$ref": "#/definitions/Error" + +// ListPipelines represents the API handler to capture a list +// of pipelines for a repo from the configured backend. +func ListPipelines(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("listing pipelines for repo %s", r.GetFullName()) + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + //nolint:lll // ignore long line length due to error message + retErr := fmt.Errorf("unable to convert page query parameter for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + //nolint:lll // ignore long line length due to error message + retErr := fmt.Errorf("unable to convert per_page query parameter for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + // + //nolint:gomnd // ignore magic number + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + p, t, err := database.FromContext(c).ListPipelinesForRepo(ctx, r, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to list pipelines for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, p) +} diff --git a/api/pipeline/output.go b/api/pipeline/output.go new file mode 100644 index 000000000..46fcace56 --- /dev/null +++ b/api/pipeline/output.go @@ -0,0 +1,35 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/util" +) + +const ( + outputJSON = "json" + outputYAML = "yaml" +) + +// writeOutput is a helper function to return the provided value to the +// request based off the output query parameter provided. If no output +// query parameter is provided, then YAML is used by default. +func writeOutput(c *gin.Context, value interface{}) { + output := util.QueryParameter(c, "output", outputYAML) + + // format response body based off output query parameter + switch strings.ToLower(output) { + case outputJSON: + c.JSON(http.StatusOK, value) + case outputYAML: + fallthrough + default: + c.YAML(http.StatusOK, value) + } +} diff --git a/api/pipeline/template.go b/api/pipeline/template.go new file mode 100644 index 000000000..54233b691 --- /dev/null +++ b/api/pipeline/template.go @@ -0,0 +1,160 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/compiler" + "github.com/go-vela/server/compiler/registry/github" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/pipeline" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types" + "github.com/go-vela/types/library" + "github.com/go-vela/types/yaml" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/pipelines/{org}/{repo}/{pipeline}/templates pipelines GetTemplates +// +// Get a map of templates utilized by a pipeline from the configured backend +// +// --- +// produces: +// - application/x-yaml +// - application/json +// parameters: +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: pipeline +// description: Commit SHA for pipeline to retrieve +// required: true +// type: string +// - in: query +// name: output +// description: Output string for specifying output format +// type: string +// default: yaml +// enum: +// - json +// - yaml +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the map of pipeline templates +// schema: +// "$ref": "#/definitions/Template" +// '400': +// description: Unable to retrieve the pipeline configuration templates +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to retrieve the pipeline configuration templates +// schema: +// "$ref": "#/definitions/Error" + +// GetTemplates represents the API handler to capture a +// map of templates utilized by a pipeline configuration. +func GetTemplates(c *gin.Context) { + // capture middleware values + m := c.MustGet("metadata").(*types.Metadata) + o := org.Retrieve(c) + p := pipeline.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), p.GetCommit()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "pipeline": p.GetCommit(), + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("reading templates from pipeline %s", entry) + + // create the compiler object + compiler := compiler.FromContext(c).Duplicate().WithCommit(p.GetCommit()).WithMetadata(m).WithRepo(r).WithUser(u) + + // parse the pipeline configuration + pipeline, _, err := compiler.Parse(p.GetData(), p.GetType(), new(yaml.Template)) + if err != nil { + util.HandleError(c, http.StatusBadRequest, fmt.Errorf("unable to parse pipeline %s: %w", entry, err)) + + return + } + + // send API call to capture the repo owner + user, err := database.FromContext(c).GetUser(r.GetUserID()) + if err != nil { + util.HandleError(c, http.StatusBadRequest, fmt.Errorf("unable to get owner for %s: %w", r.GetFullName(), err)) + + return + } + + baseErr := fmt.Sprintf("unable to set template links for %s", entry) + + templates := make(map[string]*library.Template) + for name, template := range pipeline.Templates.Map() { + templates[name] = template.ToLibrary() + + // create a compiler registry client for parsing (no address or token needed for Parse) + registry, err := github.New("", "") + if err != nil { + util.HandleError(c, http.StatusBadRequest, fmt.Errorf("%s: unable to create compiler github client: %w", baseErr, err)) + + return + } + + // capture source path to template + source := template.Source + + // if type is file, compose a source string so the template can be found + if strings.EqualFold(template.Type, "file") { + source = fmt.Sprintf("%s%s/%s/%s@%s", registry.URL, o, r.GetName(), source, p.GetCommit()) + } + + // parse the source for the template using the compiler registry client + src, err := registry.Parse(source) + if err != nil { + util.HandleError(c, http.StatusBadRequest, fmt.Errorf("%s: unable to parse source for %s: %w", baseErr, template.Source, err)) + + return + } + + // retrieve link to template file from github + link, err := scm.FromContext(c).GetHTMLURL(user, src.Org, src.Repo, src.Name, src.Ref) + if err != nil { + util.HandleError(c, http.StatusBadRequest, fmt.Errorf("%s: unable to get html url for %s/%s/%s/@%s: %w", baseErr, src.Org, src.Repo, src.Name, src.Ref, err)) + + return + } + + // set link to file for template + templates[name].SetLink(link) + } + + writeOutput(c, templates) +} diff --git a/api/pipeline/update.go b/api/pipeline/update.go new file mode 100644 index 000000000..8c349f5bb --- /dev/null +++ b/api/pipeline/update.go @@ -0,0 +1,184 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/pipeline" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/pipelines/{org}/{repo}/{pipeline} pipelines UpdatePipeline +// +// Update a pipeline in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: pipeline +// description: Commit SHA for pipeline to update +// required: true +// type: string +// - in: body +// name: body +// description: Payload containing the pipeline to update +// required: true +// schema: +// "$ref": "#/definitions/Pipeline" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the pipeline +// schema: +// "$ref": "#/definitions/Pipeline" +// '404': +// description: Unable to update the pipeline +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update the pipeline +// schema: +// "$ref": "#/definitions/Error" + +// UpdatePipeline represents the API handler to update +// a pipeline for a repo in the configured backend. +func UpdatePipeline(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + p := pipeline.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), p.GetCommit()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "pipeline": p.GetCommit(), + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("updating pipeline %s", entry) + + // capture body from API request + input := new(library.Pipeline) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for pipeline %s: %w", entry, err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // check if the Flavor field in the pipeline was provided + if len(input.GetFlavor()) > 0 { + // update the Flavor field + p.SetFlavor(input.GetFlavor()) + } + + // check if the Platform field in the pipeline was provided + if len(input.GetPlatform()) > 0 { + // update the Platform field + p.SetPlatform(input.GetPlatform()) + } + + // check if the Ref field in the pipeline was provided + if len(input.GetRef()) > 0 { + // update the Ref field + p.SetRef(input.GetRef()) + } + + // check if the Type field in the pipeline was provided + if len(input.GetType()) > 0 { + // update the Type field + p.SetType(input.GetType()) + } + + // check if the Version field in the pipeline was provided + if len(input.GetVersion()) > 0 { + // update the Version field + p.SetVersion(input.GetVersion()) + } + + // check if the ExternalSecrets field in the pipeline was provided + if input.ExternalSecrets != nil { + // update the ExternalSecrets field + p.SetExternalSecrets(input.GetExternalSecrets()) + } + + // check if the InternalSecrets field in the pipeline was provided + if input.InternalSecrets != nil { + // update the InternalSecrets field + p.SetInternalSecrets(input.GetInternalSecrets()) + } + + // check if the Services field in the pipeline was provided + if input.Services != nil { + // update the Services field + p.SetServices(input.GetServices()) + } + + // check if the Stages field in the pipeline was provided + if input.Stages != nil { + // update the Stages field + p.SetStages(input.GetStages()) + } + + // check if the Steps field in the pipeline was provided + if input.Steps != nil { + // update the Steps field + p.SetSteps(input.GetSteps()) + } + + // check if the Templates field in the pipeline was provided + if input.Templates != nil { + // update the Templates field + p.SetTemplates(input.GetTemplates()) + } + + // check if the Data field in the pipeline was provided + if len(input.GetData()) > 0 { + // update the data field + p.SetData(input.GetData()) + } + + // send API call to update the pipeline + p, err = database.FromContext(c).UpdatePipeline(ctx, p) + if err != nil { + retErr := fmt.Errorf("unable to update pipeline %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, p) +} diff --git a/api/pipeline/validate.go b/api/pipeline/validate.go new file mode 100644 index 000000000..45aae1ff8 --- /dev/null +++ b/api/pipeline/validate.go @@ -0,0 +1,118 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/compiler" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/pipeline" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/pipelines/{org}/{repo}/{pipeline}/validate pipelines ValidatePipeline +// +// Get, expand and validate a pipeline from the configured backend +// +// --- +// produces: +// - application/x-yaml +// - application/json +// parameters: +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: pipeline +// description: Commit SHA for pipeline to retrieve +// required: true +// type: string +// - in: query +// name: output +// description: Output string for specifying output format +// type: string +// default: yaml +// enum: +// - json +// - yaml +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved, expanded and validated the pipeline +// schema: +// type: string +// '400': +// description: Unable to validate the pipeline configuration +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to retrieve the pipeline configuration +// schema: +// "$ref": "#/definitions/Error" + +// ValidatePipeline represents the API handler to capture, +// expand and validate a pipeline configuration. +func ValidatePipeline(c *gin.Context) { + // capture middleware values + m := c.MustGet("metadata").(*types.Metadata) + o := org.Retrieve(c) + p := pipeline.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), p.GetCommit()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "pipeline": p.GetCommit(), + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("validating pipeline %s", entry) + + // ensure we use the expected pipeline type when compiling + r.SetPipelineType(p.GetType()) + + // create the compiler object + compiler := compiler.FromContext(c).Duplicate().WithCommit(p.GetCommit()).WithMetadata(m).WithRepo(r).WithUser(u) + + // capture optional template query parameter + template, err := strconv.ParseBool(c.DefaultQuery("template", "true")) + if err != nil { + util.HandleError(c, http.StatusBadRequest, fmt.Errorf("unable to parse template query parameter for %s: %w", entry, err)) + + return + } + + // validate the pipeline + pipeline, _, err := compiler.CompileLite(p.GetData(), template, false) + if err != nil { + retErr := fmt.Errorf("unable to validate pipeline %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + writeOutput(c, pipeline) +} diff --git a/api/repo.go b/api/repo.go deleted file mode 100644 index 6fd5476ad..000000000 --- a/api/repo.go +++ /dev/null @@ -1,1075 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "encoding/base64" - "fmt" - "net/http" - "strconv" - "strings" - - "github.com/go-vela/server/router/middleware/org" - - "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/repo" - "github.com/go-vela/server/router/middleware/user" - "github.com/go-vela/server/scm" - "github.com/go-vela/server/util" - - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/sirupsen/logrus" -) - -// swagger:operation POST /api/v1/repos repos CreateRepo -// -// Create a repo in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: body -// name: body -// description: Payload containing the repo to create -// required: true -// schema: -// "$ref": "#/definitions/Repo" -// security: -// - ApiKeyAuth: [] -// responses: -// '201': -// description: Successfully created the repo -// schema: -// "$ref": "#/definitions/Repo" -// '400': -// description: Unable to create the repo -// schema: -// "$ref": "#/definitions/Error" -// '403': -// description: Unable to create the repo -// schema: -// "$ref": "#/definitions/Error" -// '409': -// description: Unable to create the repo -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to create the repo -// schema: -// "$ref": "#/definitions/Error" -// '503': -// description: Unable to create the repo -// schema: -// "$ref": "#/definitions/Error" - -// CreateRepo represents the API handler to -// create a repo in the configured backend. -// -// nolint: funlen,gocyclo // ignore function length and cyclomatic complexity -func CreateRepo(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - allowlist := c.Value("allowlist").([]string) - defaultBuildLimit := c.Value("defaultBuildLimit").(int64) - defaultTimeout := c.Value("defaultTimeout").(int64) - maxBuildLimit := c.Value("maxBuildLimit").(int64) - - // capture body from API request - input := new(library.Repo) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for new repo: %w", err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": input.GetOrg(), - "repo": input.GetName(), - "user": u.GetName(), - }).Infof("creating new repo %s", input.GetFullName()) - - // get repo information from the source - r, err := scm.FromContext(c).GetRepo(u, input) - if err != nil { - retErr := fmt.Errorf("unable to retrieve repo info for %s from source: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update fields in repo object - r.SetUserID(u.GetID()) - - // set the active field based off the input provided - if input.Active == nil { - // default active field to true - r.SetActive(true) - } else { - r.SetActive(input.GetActive()) - } - - // set the build limit field based off the input provided - if input.GetBuildLimit() == 0 { - // default build limit to value configured by server - r.SetBuildLimit(defaultBuildLimit) - } else if input.GetBuildLimit() > maxBuildLimit { - // set build limit to value configured by server to prevent limit from exceeding max - r.SetBuildLimit(maxBuildLimit) - } else { - r.SetBuildLimit(input.GetBuildLimit()) - } - - // set the timeout field based off the input provided - if input.GetTimeout() == 0 && defaultTimeout == 0 { - // default build timeout to 30m - r.SetTimeout(constants.BuildTimeoutDefault) - } else if input.GetTimeout() == 0 { - r.SetTimeout(defaultTimeout) - } else { - r.SetTimeout(input.GetTimeout()) - } - - // set the visibility field based off the input provided - if len(input.GetVisibility()) == 0 { - // default visibility field to public - r.SetVisibility(constants.VisibilityPublic) - } else { - r.SetVisibility(input.GetVisibility()) - } - - // set default events if no events are passed in - if !input.GetAllowPull() && !input.GetAllowPush() && - !input.GetAllowDeploy() && !input.GetAllowTag() && - !input.GetAllowComment() { - // default events to push and pull_request - r.SetAllowPull(true) - r.SetAllowPush(true) - } else { - r.SetAllowComment(input.GetAllowComment()) - r.SetAllowDeploy(input.GetAllowDeploy()) - r.SetAllowPull(input.GetAllowPull()) - r.SetAllowPush(input.GetAllowPush()) - r.SetAllowTag(input.GetAllowTag()) - } - - if len(input.GetPipelineType()) == 0 { - r.SetPipelineType(constants.PipelineTypeYAML) - } else { - // ensure the pipeline type matches one of the expected values - if input.GetPipelineType() != constants.PipelineTypeYAML && - input.GetPipelineType() != constants.PipelineTypeGo && - input.GetPipelineType() != constants.PipelineTypeStarlark { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to create new repo %s: invalid pipeline_type provided %s", r.GetFullName(), input.GetPipelineType()) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - r.SetPipelineType(input.GetPipelineType()) - } - - // create unique id for the repo - uid, err := uuid.NewRandom() - if err != nil { - retErr := fmt.Errorf("unable to create UID for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - r.SetHash( - base64.StdEncoding.EncodeToString( - []byte(strings.TrimSpace(uid.String())), - ), - ) - - // ensure repo is allowed to be activated - if !checkAllowlist(r, allowlist) { - retErr := fmt.Errorf("unable to activate repo: %s is not on allowlist", r.GetFullName()) - - util.HandleError(c, http.StatusForbidden, retErr) - - return - } - - // send API call to capture the repo from the database - dbRepo, err := database.FromContext(c).GetRepo(r.GetOrg(), r.GetName()) - if err == nil && dbRepo.GetActive() { - retErr := fmt.Errorf("unable to activate repo: %s is already active", r.GetFullName()) - - util.HandleError(c, http.StatusConflict, retErr) - - return - } - - // check if the repo already has a hash created - if len(dbRepo.GetHash()) > 0 { - // overwrite the new repo hash with the existing repo hash - r.SetHash(dbRepo.GetHash()) - } - - // send API call to create the webhook - if c.Value("webhookvalidation").(bool) { - _, err = scm.FromContext(c).Enable(u, r.GetOrg(), r.GetName(), r.GetHash()) - if err != nil { - retErr := fmt.Errorf("unable to create webhook for %s: %w", r.GetFullName(), err) - - switch err.Error() { - case "repo already enabled": - util.HandleError(c, http.StatusConflict, retErr) - return - case "repo not found": - util.HandleError(c, http.StatusNotFound, retErr) - return - } - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - } - - // if the repo exists but is inactive - if len(dbRepo.GetOrg()) > 0 && !dbRepo.GetActive() { - // update the repo owner - dbRepo.SetUserID(u.GetID()) - // update the default branch - dbRepo.SetBranch(r.GetBranch()) - // activate the repo - dbRepo.SetActive(true) - - // send API call to update the repo - err = database.FromContext(c).UpdateRepo(dbRepo) - if err != nil { - retErr := fmt.Errorf("unable to set repo %s to active: %w", dbRepo.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated repo - r, _ = database.FromContext(c).GetRepo(dbRepo.GetOrg(), dbRepo.GetName()) - } else { - // send API call to create the repo - err = database.FromContext(c).CreateRepo(r) - if err != nil { - retErr := fmt.Errorf("unable to create new repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the created repo - r, _ = database.FromContext(c).GetRepo(r.GetOrg(), r.GetName()) - } - - c.JSON(http.StatusCreated, r) -} - -// swagger:operation GET /api/v1/repos repos GetRepos -// -// Get all repos in the configured backend -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// parameters: -// - in: query -// name: page -// description: The page of results to retrieve -// type: integer -// default: 1 -// - in: query -// name: per_page -// description: How many results per page to return -// type: integer -// maximum: 100 -// default: 10 -// responses: -// '200': -// description: Successfully retrieved the repo -// schema: -// type: array -// items: -// "$ref": "#/definitions/Repo" -// headers: -// X-Total-Count: -// description: Total number of results -// type: integer -// Link: -// description: see https://tools.ietf.org/html/rfc5988 -// type: string -// '400': -// description: Unable to retrieve the repo -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the repo -// schema: -// "$ref": "#/definitions/Error" - -// GetRepos represents the API handler to capture a list -// of repos for a user from the configured backend. -func GetRepos(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Infof("reading repos for user %s", u.GetName()) - - // capture page query parameter if present - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) - if err != nil { - retErr := fmt.Errorf("unable to convert page query parameter for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // capture per_page query parameter if present - perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to convert per_page query parameter for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // ensure per_page isn't above or below allowed values - // - // nolint: gomnd // ignore magic number - perPage = util.MaxInt(1, util.MinInt(100, perPage)) - - // send API call to capture the total number of repos for the user - t, err := database.FromContext(c).GetUserRepoCount(u) - if err != nil { - retErr := fmt.Errorf("unable to get repo count for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the list of repos for the user - r, err := database.FromContext(c).GetUserRepoList(u, page, perPage) - if err != nil { - retErr := fmt.Errorf("unable to get repos for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // create pagination object - pagination := Pagination{ - Page: page, - PerPage: perPage, - Total: t, - } - // set pagination headers - pagination.SetHeaderLink(c) - - c.JSON(http.StatusOK, r) -} - -// swagger:operation GET /api/v1/{org} repos GetOrgRepos -// -// Get all repos for the provided org in the configured backend -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: query -// name: active -// description: Filter active repos -// type: boolean -// default: true -// - in: query -// name: page -// description: The page of results to retrieve -// type: integer -// default: 1 -// - in: query -// name: per_page -// description: How many results per page to return -// type: integer -// maximum: 100 -// default: 10 -// responses: -// '200': -// description: Successfully retrieved the repo -// schema: -// type: array -// items: -// "$ref": "#/definitions/Repo" -// headers: -// X-Total-Count: -// description: Total number of results -// type: integer -// Link: -// description: see https://tools.ietf.org/html/rfc5988 -// type: string -// '400': -// description: Unable to retrieve the org -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the org -// schema: -// "$ref": "#/definitions/Error" - -// GetOrgRepos represents the API handler to capture a list -// of repos for an org from the configured backend. -func GetOrgRepos(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - org := c.Param("org") - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": org, - "user": u.GetName(), - }).Infof("reading repos for org %s", org) - - // capture page query parameter if present - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) - if err != nil { - retErr := fmt.Errorf("unable to convert page query parameter for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // capture per_page query parameter if present - perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to convert per_page query parameter for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // ensure per_page isn't above or below allowed values - // - // nolint: gomnd // ignore magic number - perPage = util.MaxInt(1, util.MinInt(100, perPage)) - - // See if the user is an org admin to bypass individual permission checks - perm, err := scm.FromContext(c).OrgAccess(u, org) - if err != nil { - logrus.Errorf("unable to get user %s access level for org %s", u.GetName(), org) - } - - filters := map[string]string{} - // Only show public repos to non-admins - if perm != "admin" { - filters["visibility"] = "public" - } - - filters["active"] = c.DefaultQuery("active", "true") - - // send API call to capture the total number of repos for the org - t, err := database.FromContext(c).GetOrgRepoCount(org, filters) - if err != nil { - retErr := fmt.Errorf("unable to get repo count for org %s: %w", org, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the list of repos for the org - r, err := database.FromContext(c).GetOrgRepoList(org, filters, page, perPage) - if err != nil { - retErr := fmt.Errorf("unable to get repos for org %s: %w", org, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // create pagination object - pagination := Pagination{ - Page: page, - PerPage: perPage, - Total: t, - } - // set pagination headers - pagination.SetHeaderLink(c) - - c.JSON(http.StatusOK, r) -} - -// swagger:operation GET /api/v1/repos/{org}/{repo} repos GetRepo -// -// Get a repo in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the repo -// schema: -// "$ref": "#/definitions/Repo" - -// GetRepo represents the API handler to -// capture a repo from the configured backend. -func GetRepo(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading repo %s", r.GetFullName()) - - c.JSON(http.StatusOK, r) -} - -// swagger:operation PUT /api/v1/repos/{org}/{repo} repos UpdateRepo -// -// Update a repo in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: body -// name: body -// description: Payload containing the repo to update -// required: true -// schema: -// "$ref": "#/definitions/Repo" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully updated the repo -// schema: -// "$ref": "#/definitions/Repo" -// '400': -// description: Unable to update the repo -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to update the repo -// schema: -// "$ref": "#/definitions/Error" -// '503': -// description: Unable to update the repo -// schema: -// "$ref": "#/definitions/Error" - -// UpdateRepo represents the API handler to update -// a repo in the configured backend. -// -// nolint: funlen // ignore function length due to comments and conditionals -func UpdateRepo(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - maxBuildLimit := c.Value("maxBuildLimit").(int64) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("updating repo %s", r.GetFullName()) - - // capture body from API request - input := new(library.Repo) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update repo fields if provided - if len(input.GetBranch()) > 0 { - // update branch if set - r.SetBranch(input.GetBranch()) - } - - // update build limit if set - if input.GetBuildLimit() > 0 { - // allow build limit between 1 - value configured by server - r.SetBuildLimit( - int64( - util.MaxInt( - constants.BuildLimitMin, - util.MinInt( - int(input.GetBuildLimit()), - int(maxBuildLimit), - ), // clamp max - ), // clamp min - ), - ) - } - - if input.GetTimeout() > 0 { - // update build timeout if set - r.SetTimeout( - int64( - util.MaxInt( - constants.BuildTimeoutMin, - util.MinInt( - int(input.GetTimeout()), - constants.BuildTimeoutMax, - ), // clamp max - ), // clamp min - ), - ) - } - - if input.GetCounter() > 0 { - if input.GetCounter() <= r.GetCounter() { - retErr := fmt.Errorf("unable to set counter for repo %s: must be greater than current %d", - r.GetFullName(), r.GetCounter()) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - r.SetCounter(input.GetCounter()) - } - - if len(input.GetVisibility()) > 0 { - // update visibility if set - r.SetVisibility(input.GetVisibility()) - } - - if input.Private != nil { - // update private if set - r.SetPrivate(input.GetPrivate()) - } - - if input.Active != nil { - // update active if set - r.SetActive(input.GetActive()) - } - - if input.AllowPull != nil { - // update allow_pull if set - r.SetAllowPull(input.GetAllowPull()) - } - - if input.AllowPush != nil { - // update allow_push if set - r.SetAllowPush(input.GetAllowPush()) - } - - if input.AllowDeploy != nil { - // update allow_deploy if set - r.SetAllowDeploy(input.GetAllowDeploy()) - } - - if input.AllowTag != nil { - // update allow_tag if set - r.SetAllowTag(input.GetAllowTag()) - } - - if input.AllowComment != nil { - // update allow_comment if set - r.SetAllowComment(input.GetAllowComment()) - } - - // set default events if no events are enabled - if !r.GetAllowPull() && !r.GetAllowPush() && - !r.GetAllowDeploy() && !r.GetAllowTag() && - !r.GetAllowComment() { - r.SetAllowPull(true) - r.SetAllowPush(true) - } - - if len(input.GetPipelineType()) != 0 { - // ensure the pipeline type matches one of the expected values - if input.GetPipelineType() != constants.PipelineTypeYAML && - input.GetPipelineType() != constants.PipelineTypeGo && - input.GetPipelineType() != constants.PipelineTypeStarlark { - retErr := fmt.Errorf("pipeline_type of %s is invalid", input.GetPipelineType()) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - r.SetPipelineType(input.GetPipelineType()) - } - - // set hash for repo if no hash is already set - if len(r.GetHash()) == 0 { - // create unique id for the repo - uid, err := uuid.NewRandom() - if err != nil { - retErr := fmt.Errorf("unable to create UID for repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - r.SetHash( - base64.StdEncoding.EncodeToString( - []byte(strings.TrimSpace(uid.String())), - ), - ) - } - - // send API call to update the repo - err = database.FromContext(c).UpdateRepo(r) - if err != nil { - retErr := fmt.Errorf("unable to update repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated repo - r, _ = database.FromContext(c).GetRepo(r.GetOrg(), r.GetName()) - - c.JSON(http.StatusOK, r) -} - -// swagger:operation DELETE /api/v1/repos/{org}/{repo} repos DeleteRepo -// -// Delete a repo in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully deleted the repo -// schema: -// type: string -// '500': -// description: Unable to deleted the repo -// schema: -// "$ref": "#/definitions/Error" -// '510': -// description: Unable to deleted the repo -// schema: -// "$ref": "#/definitions/Error" - -// DeleteRepo represents the API handler to remove -// a repo from the configured backend. -func DeleteRepo(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("deleting repo %s", r.GetFullName()) - - // send API call to remove the webhook - err := scm.FromContext(c).Disable(u, r.GetOrg(), r.GetName()) - if err != nil { - retErr := fmt.Errorf("unable to delete webhook for %s: %w", r.GetFullName(), err) - - if err.Error() == "Repo not found" { - util.HandleError(c, http.StatusNotExtended, retErr) - - return - } - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // Mark the the repo as inactive - r.SetActive(false) - - err = database.FromContext(c).UpdateRepo(r) - if err != nil { - retErr := fmt.Errorf("unable to set repo %s to inactive: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // Comment out actual delete until delete mechanism is fleshed out - // err = database.FromContext(c).DeleteRepo(r.ID) - // if err != nil { - // retErr := fmt.Errorf("Error while deleting repo %s: %v", r.FullName, err) - // util.HandleError(c, http.StatusInternalServerError, retErr) - // return - // } - - c.JSON(http.StatusOK, fmt.Sprintf("repo %s deleted", r.GetFullName())) -} - -// swagger:operation PATCH /api/v1/repos/{org}/{repo}/repair repos RepairRepo -// -// Remove and recreate the webhook for a repo -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully repaired the repo -// schema: -// type: string -// '500': -// description: Unable to repair the repo -// schema: -// "$ref": "#/definitions/Error" - -// RepairRepo represents the API handler to remove -// and then create a webhook for a repo. -func RepairRepo(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("repairing repo %s", r.GetFullName()) - - // send API call to remove the webhook - err := scm.FromContext(c).Disable(u, r.GetOrg(), r.GetName()) - if err != nil { - retErr := fmt.Errorf("unable to delete webhook for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to create the webhook - _, err = scm.FromContext(c).Enable(u, r.GetOrg(), r.GetName(), r.GetHash()) - if err != nil { - retErr := fmt.Errorf("unable to create webhook for %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // if the repo was previously inactive, mark it as active - if !r.GetActive() { - r.SetActive(true) - - // send API call to update the repo - err = database.FromContext(c).UpdateRepo(r) - if err != nil { - retErr := fmt.Errorf("unable to set repo %s to active: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - } - - c.JSON(http.StatusOK, fmt.Sprintf("repo %s repaired", r.GetFullName())) -} - -// swagger:operation PATCH /api/v1/repos/{org}/{repo}/chown repos ChownRepo -// -// Change the owner of the webhook for a repo -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully changed the owner for the repo -// schema: -// type: string -// '500': -// description: Unable to change the owner for the repo -// schema: -// "$ref": "#/definitions/Error" - -// ChownRepo represents the API handler to change -// the owner of a repo in the configured backend. -func ChownRepo(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("changing owner of repo %s to %s", r.GetFullName(), u.GetName()) - - // update repo owner - r.SetUserID(u.GetID()) - - // send API call to updated the repo - err := database.FromContext(c).UpdateRepo(r) - if err != nil { - retErr := fmt.Errorf("unable to change owner of repo %s: %w", r.GetFullName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, fmt.Sprintf("repo %s changed owner", r.GetFullName())) -} - -// checkAllowlist is a helper function to ensure only repos in the -// allowlist are allowed to enable repos. If the allowlist is -// empty then any repo can be enabled. -func checkAllowlist(r *library.Repo, allowlist []string) bool { - // if the allowlist is not set or empty allow any repo to be enabled - if len(allowlist) == 0 { - return true - } - - for _, repo := range allowlist { - // allow all repos in org - if strings.Contains(repo, "/*") { - if strings.HasPrefix(repo, r.GetOrg()) { - return true - } - } - - // allow specific repo within org - if repo == r.GetFullName() { - return true - } - } - - return false -} diff --git a/api/repo/chown.go b/api/repo/chown.go new file mode 100644 index 000000000..93a4ec5fa --- /dev/null +++ b/api/repo/chown.go @@ -0,0 +1,82 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation PATCH /api/v1/repos/{org}/{repo}/chown repos ChownRepo +// +// Change the owner of the webhook for a repo +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully changed the owner for the repo +// schema: +// type: string +// '500': +// description: Unable to change the owner for the repo +// schema: +// "$ref": "#/definitions/Error" + +// ChownRepo represents the API handler to change +// the owner of a repo in the configured backend. +func ChownRepo(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("changing owner of repo %s to %s", r.GetFullName(), u.GetName()) + + // update repo owner + r.SetUserID(u.GetID()) + + // send API call to update the repo + _, err := database.FromContext(c).UpdateRepo(ctx, r) + if err != nil { + retErr := fmt.Errorf("unable to change owner of repo %s to %s: %w", r.GetFullName(), u.GetName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("repo %s changed owner to %s", r.GetFullName(), u.GetName())) +} diff --git a/api/repo/create.go b/api/repo/create.go new file mode 100644 index 000000000..d76aae325 --- /dev/null +++ b/api/repo/create.go @@ -0,0 +1,323 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "encoding/base64" + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/repos repos CreateRepo +// +// Create a repo in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: Payload containing the repo to create +// required: true +// schema: +// "$ref": "#/definitions/Repo" +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: Successfully created the repo +// schema: +// "$ref": "#/definitions/Repo" +// '400': +// description: Unable to create the repo +// schema: +// "$ref": "#/definitions/Error" +// '403': +// description: Unable to create the repo +// schema: +// "$ref": "#/definitions/Error" +// '409': +// description: Unable to create the repo +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the repo +// schema: +// "$ref": "#/definitions/Error" +// '503': +// description: Unable to create the repo +// schema: +// "$ref": "#/definitions/Error" + +// CreateRepo represents the API handler to +// create a repo in the configured backend. +// +//nolint:funlen,gocyclo // ignore function length and cyclomatic complexity +func CreateRepo(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + allowlist := c.Value("allowlist").([]string) + defaultBuildLimit := c.Value("defaultBuildLimit").(int64) + defaultTimeout := c.Value("defaultTimeout").(int64) + maxBuildLimit := c.Value("maxBuildLimit").(int64) + defaultRepoEvents := c.Value("defaultRepoEvents").([]string) + ctx := c.Request.Context() + + // capture body from API request + input := new(library.Repo) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for new repo: %w", err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": input.GetOrg(), + "repo": input.GetName(), + "user": u.GetName(), + }).Infof("creating new repo %s", input.GetFullName()) + + // get repo information from the source + r, err := scm.FromContext(c).GetRepo(u, input) + if err != nil { + retErr := fmt.Errorf("unable to retrieve repo info for %s from source: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update fields in repo object + r.SetUserID(u.GetID()) + + // set the active field based off the input provided + if input.Active == nil { + // default active field to true + r.SetActive(true) + } else { + r.SetActive(input.GetActive()) + } + + // set the build limit field based off the input provided + if input.GetBuildLimit() == 0 { + // default build limit to value configured by server + r.SetBuildLimit(defaultBuildLimit) + } else if input.GetBuildLimit() > maxBuildLimit { + // set build limit to value configured by server to prevent limit from exceeding max + r.SetBuildLimit(maxBuildLimit) + } else { + r.SetBuildLimit(input.GetBuildLimit()) + } + + // set the timeout field based off the input provided + if input.GetTimeout() == 0 && defaultTimeout == 0 { + // default build timeout to 30m + r.SetTimeout(constants.BuildTimeoutDefault) + } else if input.GetTimeout() == 0 { + r.SetTimeout(defaultTimeout) + } else { + r.SetTimeout(input.GetTimeout()) + } + + // set the visibility field based off the input provided + if len(input.GetVisibility()) > 0 { + // default visibility field to the input visibility + r.SetVisibility(input.GetVisibility()) + } + + // fields restricted to platform admins + if u.GetAdmin() { + // trusted default is false + if input.GetTrusted() != r.GetTrusted() { + r.SetTrusted(input.GetTrusted()) + } + } + + // set default events if no events are passed in + if !input.GetAllowPull() && !input.GetAllowPush() && + !input.GetAllowDeploy() && !input.GetAllowTag() && + !input.GetAllowComment() { + for _, event := range defaultRepoEvents { + switch event { + case constants.EventPull: + r.SetAllowPull(true) + case constants.EventPush: + r.SetAllowPush(true) + case constants.EventDeploy: + r.SetAllowDeploy(true) + case constants.EventTag: + r.SetAllowTag(true) + case constants.EventComment: + r.SetAllowComment(true) + } + } + } else { + r.SetAllowComment(input.GetAllowComment()) + r.SetAllowDeploy(input.GetAllowDeploy()) + r.SetAllowPull(input.GetAllowPull()) + r.SetAllowPush(input.GetAllowPush()) + r.SetAllowTag(input.GetAllowTag()) + } + + if len(input.GetPipelineType()) == 0 { + r.SetPipelineType(constants.PipelineTypeYAML) + } else { + // ensure the pipeline type matches one of the expected values + if input.GetPipelineType() != constants.PipelineTypeYAML && + input.GetPipelineType() != constants.PipelineTypeGo && + input.GetPipelineType() != constants.PipelineTypeStarlark { + retErr := fmt.Errorf("unable to create new repo %s: invalid pipeline_type provided %s", r.GetFullName(), input.GetPipelineType()) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + r.SetPipelineType(input.GetPipelineType()) + } + + // create unique id for the repo + uid, err := uuid.NewRandom() + if err != nil { + retErr := fmt.Errorf("unable to create UID for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + + return + } + + r.SetHash( + base64.StdEncoding.EncodeToString( + []byte(strings.TrimSpace(uid.String())), + ), + ) + + // ensure repo is allowed to be activated + if !util.CheckAllowlist(r, allowlist) { + retErr := fmt.Errorf("unable to activate repo: %s is not on allowlist", r.GetFullName()) + + util.HandleError(c, http.StatusForbidden, retErr) + + return + } + + // send API call to capture the repo from the database + dbRepo, err := database.FromContext(c).GetRepoForOrg(ctx, r.GetOrg(), r.GetName()) + if err == nil && dbRepo.GetActive() { + retErr := fmt.Errorf("unable to activate repo: %s is already active", r.GetFullName()) + + util.HandleError(c, http.StatusConflict, retErr) + + return + } + + // check if the repo already has a hash created + if len(dbRepo.GetHash()) > 0 { + // overwrite the new repo hash with the existing repo hash + r.SetHash(dbRepo.GetHash()) + } + + h := new(library.Hook) + + // err being nil means we have a record of this repo (dbRepo) + if err == nil { + h, _ = database.FromContext(c).LastHookForRepo(dbRepo) + + // make sure our record of the repo allowed events matches what we send to SCM + // what the dbRepo has should override default events on enable + r.SetAllowComment(dbRepo.GetAllowComment()) + r.SetAllowDeploy(dbRepo.GetAllowDeploy()) + r.SetAllowPull(dbRepo.GetAllowPull()) + r.SetAllowPush(dbRepo.GetAllowPush()) + r.SetAllowTag(dbRepo.GetAllowTag()) + } + + // check if we should create the webhook + if c.Value("webhookvalidation").(bool) { + // send API call to create the webhook + h, _, err = scm.FromContext(c).Enable(u, r, h) + if err != nil { + retErr := fmt.Errorf("unable to create webhook for %s: %w", r.GetFullName(), err) + + switch err.Error() { + case "repo already enabled": + util.HandleError(c, http.StatusConflict, retErr) + return + case "repo not found": + util.HandleError(c, http.StatusNotFound, retErr) + return + } + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + // if the repo exists but is inactive + if len(dbRepo.GetOrg()) > 0 && !dbRepo.GetActive() { + // update the repo owner + dbRepo.SetUserID(u.GetID()) + // update the default branch + dbRepo.SetBranch(r.GetBranch()) + // activate the repo + dbRepo.SetActive(true) + + // send API call to update the repo + r, err = database.FromContext(c).UpdateRepo(ctx, dbRepo) + if err != nil { + retErr := fmt.Errorf("unable to set repo %s to active: %w", dbRepo.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } else { + // send API call to create the repo + r, err = database.FromContext(c).CreateRepo(ctx, r) + if err != nil { + retErr := fmt.Errorf("unable to create new repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + // create init hook in the DB after repo has been added in order to capture its ID + if c.Value("webhookvalidation").(bool) { + // update initialization hook + h.SetRepoID(r.GetID()) + // create first hook for repo in the database + _, err = database.FromContext(c).CreateHook(h) + if err != nil { + retErr := fmt.Errorf("unable to create initialization webhook for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + c.JSON(http.StatusCreated, r) +} diff --git a/api/repo/delete.go b/api/repo/delete.go new file mode 100644 index 000000000..4ddaf11f7 --- /dev/null +++ b/api/repo/delete.go @@ -0,0 +1,110 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/repos/{org}/{repo} repos DeleteRepo +// +// Delete a repo in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted the repo +// schema: +// type: string +// '500': +// description: Unable to deleted the repo +// schema: +// "$ref": "#/definitions/Error" +// '510': +// description: Unable to deleted the repo +// schema: +// "$ref": "#/definitions/Error" + +// DeleteRepo represents the API handler to remove +// a repo from the configured backend. +func DeleteRepo(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("deleting repo %s", r.GetFullName()) + + // send API call to remove the webhook + err := scm.FromContext(c).Disable(u, r.GetOrg(), r.GetName()) + if err != nil { + retErr := fmt.Errorf("unable to delete webhook for %s: %w", r.GetFullName(), err) + + if err.Error() == "Repo not found" { + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // Mark the repo as inactive + r.SetActive(false) + + _, err = database.FromContext(c).UpdateRepo(ctx, r) + if err != nil { + retErr := fmt.Errorf("unable to set repo %s to inactive: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // Comment out actual delete until delete mechanism is fleshed out + // err = database.FromContext(c).DeleteRepo(r.ID) + // if err != nil { + // retErr := fmt.Errorf("Error while deleting repo %s: %w", r.FullName, err) + // util.HandleError(c, http.StatusInternalServerError, retErr) + // return + // } + + c.JSON(http.StatusOK, fmt.Sprintf("repo %s set to inactive", r.GetFullName())) +} diff --git a/api/repo/doc.go b/api/repo/doc.go new file mode 100644 index 000000000..82eb5304e --- /dev/null +++ b/api/repo/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package repo provides the repo handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/repo" +package repo diff --git a/api/repo/get.go b/api/repo/get.go new file mode 100644 index 000000000..65cbede1b --- /dev/null +++ b/api/repo/get.go @@ -0,0 +1,61 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo} repos GetRepo +// +// Get a repo in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the repo +// schema: +// "$ref": "#/definitions/Repo" + +// GetRepo represents the API handler to +// capture a repo from the configured backend. +func GetRepo(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("reading repo %s", r.GetFullName()) + + c.JSON(http.StatusOK, r) +} diff --git a/api/repo/list.go b/api/repo/list.go new file mode 100644 index 000000000..77c127b3c --- /dev/null +++ b/api/repo/list.go @@ -0,0 +1,131 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos repos ListRepos +// +// Get all repos in the configured backend +// +// --- +// produces: +// - application/json +// security: +// - ApiKeyAuth: [] +// parameters: +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// responses: +// '200': +// description: Successfully retrieved the repo +// schema: +// type: array +// items: +// "$ref": "#/definitions/Repo" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve the repo +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the repo +// schema: +// "$ref": "#/definitions/Error" + +// ListRepos represents the API handler to capture a list +// of repos for a user from the configured backend. +func ListRepos(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Infof("listing repos for user %s", u.GetName()) + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + retErr := fmt.Errorf("unable to convert page query parameter for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // capture the sort_by query parameter if present + sortBy := util.QueryParameter(c, "sort_by", "name") + + // capture the query parameters if present: + // + // * active + filters := map[string]interface{}{ + "active": util.QueryParameter(c, "active", "true"), + } + + // send API call to capture the list of repos for the user + r, t, err := database.FromContext(c).ListReposForUser(ctx, u, sortBy, filters, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to get repos for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, r) +} diff --git a/api/repo/list_org.go b/api/repo/list_org.go new file mode 100644 index 000000000..727b91b5d --- /dev/null +++ b/api/repo/list_org.go @@ -0,0 +1,164 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org} repos ListReposForOrg +// +// Get all repos for the provided org in the configured backend +// +// --- +// produces: +// - application/json +// security: +// - ApiKeyAuth: [] +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: query +// name: active +// description: Filter active repos +// type: boolean +// default: true +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// - in: query +// name: sort_by +// description: How to sort the results +// type: string +// enum: +// - name +// - latest +// default: name +// responses: +// '200': +// description: Successfully retrieved the repo +// schema: +// type: array +// items: +// "$ref": "#/definitions/Repo" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve the org +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the org +// schema: +// "$ref": "#/definitions/Error" + +// ListReposForOrg represents the API handler to capture a list +// of repos for an org from the configured backend. +func ListReposForOrg(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "user": u.GetName(), + }).Infof("listing repos for org %s", o) + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + retErr := fmt.Errorf("unable to convert page query parameter for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // capture the sort_by query parameter if present + sortBy := util.QueryParameter(c, "sort_by", "name") + + // capture the query parameters if present: + // + // * active + filters := map[string]interface{}{ + "active": util.QueryParameter(c, "active", "true"), + } + + // See if the user is an org admin to bypass individual permission checks + perm, err := scm.FromContext(c).OrgAccess(u, o) + if err != nil { + logrus.Errorf("unable to get user %s access level for org %s", u.GetName(), o) + } + // Only show public repos to non-admins + if perm != "admin" { + filters["visibility"] = constants.VisibilityPublic + } + + // send API call to capture the list of repos for the org + r, t, err := database.FromContext(c).ListReposForOrg(ctx, o, sortBy, filters, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to get repos for org %s: %w", o, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, r) +} diff --git a/api/repo/repair.go b/api/repo/repair.go new file mode 100644 index 000000000..337f52d91 --- /dev/null +++ b/api/repo/repair.go @@ -0,0 +1,137 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation PATCH /api/v1/repos/{org}/{repo}/repair repos RepairRepo +// +// Remove and recreate the webhook for a repo +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully repaired the repo +// schema: +// type: string +// '500': +// description: Unable to repair the repo +// schema: +// "$ref": "#/definitions/Error" + +// RepairRepo represents the API handler to remove +// and then create a webhook for a repo. +func RepairRepo(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("repairing repo %s", r.GetFullName()) + + // check if we should create the webhook + if c.Value("webhookvalidation").(bool) { + // send API call to remove the webhook + err := scm.FromContext(c).Disable(u, r.GetOrg(), r.GetName()) + if err != nil { + retErr := fmt.Errorf("unable to delete webhook for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + hook, err := database.FromContext(c).LastHookForRepo(r) + if err != nil { + retErr := fmt.Errorf("unable to get last hook for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // send API call to create the webhook + hook, _, err = scm.FromContext(c).Enable(u, r, hook) + if err != nil { + retErr := fmt.Errorf("unable to create webhook for %s: %w", r.GetFullName(), err) + + switch err.Error() { + case "repo already enabled": + util.HandleError(c, http.StatusConflict, retErr) + return + case "repo not found": + util.HandleError(c, http.StatusNotFound, retErr) + return + } + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + hook.SetRepoID(r.GetID()) + + _, err = database.FromContext(c).CreateHook(hook) + if err != nil { + retErr := fmt.Errorf("unable to create initialization webhook for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + // if the repo was previously inactive, mark it as active + if !r.GetActive() { + r.SetActive(true) + + // send API call to update the repo + _, err := database.FromContext(c).UpdateRepo(ctx, r) + if err != nil { + retErr := fmt.Errorf("unable to set repo %s to active: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + c.JSON(http.StatusOK, fmt.Sprintf("repo %s repaired", r.GetFullName())) +} diff --git a/api/repo/update.go b/api/repo/update.go new file mode 100644 index 000000000..a7684c393 --- /dev/null +++ b/api/repo/update.go @@ -0,0 +1,309 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "encoding/base64" + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/repos/{org}/{repo} repos UpdateRepo +// +// Update a repo in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: body +// name: body +// description: Payload containing the repo to update +// required: true +// schema: +// "$ref": "#/definitions/Repo" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the repo +// schema: +// "$ref": "#/definitions/Repo" +// '400': +// description: Unable to update the repo +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update the repo +// schema: +// "$ref": "#/definitions/Error" +// '503': +// description: Unable to update the repo +// schema: +// "$ref": "#/definitions/Error" + +// UpdateRepo represents the API handler to update +// a repo in the configured backend. +// +//nolint:funlen,gocyclo // ignore function length +func UpdateRepo(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + maxBuildLimit := c.Value("maxBuildLimit").(int64) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("updating repo %s", r.GetFullName()) + + // capture body from API request + input := new(library.Repo) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + eventsChanged := false + + // update repo fields if provided + if len(input.GetBranch()) > 0 { + // update branch if set + r.SetBranch(input.GetBranch()) + } + + // update build limit if set + if input.GetBuildLimit() > 0 { + // allow build limit between 1 - value configured by server + r.SetBuildLimit( + int64( + util.MaxInt( + constants.BuildLimitMin, + util.MinInt( + int(input.GetBuildLimit()), + int(maxBuildLimit), + ), // clamp max + ), // clamp min + ), + ) + } + + if input.GetTimeout() > 0 { + // update build timeout if set + r.SetTimeout( + int64( + util.MaxInt( + constants.BuildTimeoutMin, + util.MinInt( + int(input.GetTimeout()), + constants.BuildTimeoutMax, + ), // clamp max + ), // clamp min + ), + ) + } + + if input.GetCounter() > 0 { + if input.GetCounter() <= r.GetCounter() { + retErr := fmt.Errorf("unable to set counter for repo %s: must be greater than current %d", + r.GetFullName(), r.GetCounter()) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + r.SetCounter(input.GetCounter()) + } + + if len(input.GetVisibility()) > 0 { + // update visibility if set + r.SetVisibility(input.GetVisibility()) + } + + if input.Private != nil { + // update private if set + r.SetPrivate(input.GetPrivate()) + } + + if input.Active != nil { + // update active if set + r.SetActive(input.GetActive()) + } + + if input.AllowPull != nil { + // update allow_pull if set + r.SetAllowPull(input.GetAllowPull()) + + eventsChanged = true + } + + if input.AllowPush != nil { + // update allow_push if set + r.SetAllowPush(input.GetAllowPush()) + + eventsChanged = true + } + + if input.AllowDeploy != nil { + // update allow_deploy if set + r.SetAllowDeploy(input.GetAllowDeploy()) + + eventsChanged = true + } + + if input.AllowTag != nil { + // update allow_tag if set + r.SetAllowTag(input.GetAllowTag()) + + eventsChanged = true + } + + if input.AllowComment != nil { + // update allow_comment if set + r.SetAllowComment(input.GetAllowComment()) + + eventsChanged = true + } + + // set default events if no events are enabled + if !r.GetAllowPull() && !r.GetAllowPush() && + !r.GetAllowDeploy() && !r.GetAllowTag() && + !r.GetAllowComment() { + r.SetAllowPull(true) + r.SetAllowPush(true) + } + + if len(input.GetPipelineType()) != 0 { + // ensure the pipeline type matches one of the expected values + if input.GetPipelineType() != constants.PipelineTypeYAML && + input.GetPipelineType() != constants.PipelineTypeGo && + input.GetPipelineType() != constants.PipelineTypeStarlark { + retErr := fmt.Errorf("pipeline_type of %s is invalid", input.GetPipelineType()) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + r.SetPipelineType(input.GetPipelineType()) + } + + // set hash for repo if no hash is already set + if len(r.GetHash()) == 0 { + // create unique id for the repo + uid, err := uuid.NewRandom() + if err != nil { + retErr := fmt.Errorf("unable to create UID for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + + return + } + + r.SetHash( + base64.StdEncoding.EncodeToString( + []byte(strings.TrimSpace(uid.String())), + ), + ) + } + + // fields restricted to platform admins + if u.GetAdmin() { + // trusted + if input.GetTrusted() != r.GetTrusted() { + r.SetTrusted(input.GetTrusted()) + } + } + + // if webhook validation is not set or events didn't change, skip webhook update + if c.Value("webhookvalidation").(bool) && eventsChanged { + // grab last hook from repo to fetch the webhook ID + lastHook, err := database.FromContext(c).LastHookForRepo(r) + if err != nil { + retErr := fmt.Errorf("unable to retrieve last hook for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + // if user is platform admin, fetch the repo owner token to make changes to webhook + if u.GetAdmin() { + // capture admin name for logging + admn := u.GetName() + + u, err = database.FromContext(c).GetUser(r.GetUserID()) + if err != nil { + retErr := fmt.Errorf("unable to get repo owner of %s for platform admin webhook update: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // log admin override update repo hook + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("platform admin %s updating repo webhook events for repo %s", admn, r.GetFullName()) + } + // update webhook with new events + err = scm.FromContext(c).Update(u, r, lastHook.GetWebhookID()) + if err != nil { + retErr := fmt.Errorf("unable to update repo webhook for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + // send API call to update the repo + r, err = database.FromContext(c).UpdateRepo(ctx, r) + if err != nil { + retErr := fmt.Errorf("unable to update repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, r) +} diff --git a/api/schedule/create.go b/api/schedule/create.go new file mode 100644 index 000000000..667eb3ca9 --- /dev/null +++ b/api/schedule/create.go @@ -0,0 +1,241 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "fmt" + "net/http" + "time" + + "github.com/adhocore/gronx" + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/schedules/{org}/{repo} schedules CreateSchedule +// +// Create a schedule in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: body +// name: body +// description: Payload containing the schedule to create +// required: true +// schema: +// "$ref": "#/definitions/Schedule" +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: Successfully created the schedule +// schema: +// "$ref": "#/definitions/Schedule" +// '400': +// description: Unable to create the schedule +// schema: +// "$ref": "#/definitions/Error" +// '403': +// description: Unable to create the schedule +// schema: +// "$ref": "#/definitions/Error" +// '409': +// description: Unable to create the schedule +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the schedule +// schema: +// "$ref": "#/definitions/Error" +// '503': +// description: Unable to create the schedule +// schema: +// "$ref": "#/definitions/Error" + +// CreateSchedule represents the API handler to +// create a schedule in the configured backend. +func CreateSchedule(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + r := repo.Retrieve(c) + ctx := c.Request.Context() + allowlist := c.Value("allowlistschedule").([]string) + minimumFrequency := c.Value("scheduleminimumfrequency").(time.Duration) + + // capture body from API request + input := new(library.Schedule) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for new schedule: %w", err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure the entry is valid + err = validateEntry(minimumFrequency, input.GetEntry()) + if err != nil { + retErr := fmt.Errorf("schedule of %s with entry %s is invalid: %w", input.GetName(), input.GetEntry(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure schedule name is defined + if input.GetName() == "" { + util.HandleError(c, http.StatusBadRequest, fmt.Errorf("schedule name must be set")) + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("creating new schedule %s", input.GetName()) + + // ensure repo is allowed to create new schedules + if !util.CheckAllowlist(r, allowlist) { + retErr := fmt.Errorf("unable to create schedule %s: %s is not on allowlist", input.GetName(), r.GetFullName()) + + util.HandleError(c, http.StatusForbidden, retErr) + + return + } + + s := new(library.Schedule) + + // update fields in schedule object + s.SetCreatedBy(u.GetName()) + s.SetRepoID(r.GetID()) + s.SetName(input.GetName()) + s.SetEntry(input.GetEntry()) + s.SetCreatedAt(time.Now().UTC().Unix()) + s.SetUpdatedAt(time.Now().UTC().Unix()) + s.SetUpdatedBy(u.GetName()) + + if input.GetBranch() == "" { + s.SetBranch(r.GetBranch()) + } else { + s.SetBranch(input.GetBranch()) + } + + // set the active field based off the input provided + if input.Active == nil { + // default active field to true + s.SetActive(true) + } else { + s.SetActive(input.GetActive()) + } + + // send API call to capture the schedule from the database + dbSchedule, err := database.FromContext(c).GetScheduleForRepo(ctx, r, input.GetName()) + if err == nil && dbSchedule.GetActive() { + retErr := fmt.Errorf("unable to create schedule: %s is already active", input.GetName()) + + util.HandleError(c, http.StatusConflict, retErr) + + return + } + + if !r.GetActive() { + retErr := fmt.Errorf("unable to create schedule: %s repo %s is disabled", input.GetName(), r.GetFullName()) + + util.HandleError(c, http.StatusConflict, retErr) + + return + } + + // if the schedule exists but is inactive + if dbSchedule.GetID() != 0 && !dbSchedule.GetActive() && input.GetActive() { + // update the user who created the schedule + dbSchedule.SetUpdatedBy(u.GetName()) + // activate the schedule + dbSchedule.SetActive(true) + + // send API call to update the schedule + s, err = database.FromContext(c).UpdateSchedule(ctx, dbSchedule, true) + if err != nil { + retErr := fmt.Errorf("unable to set schedule %s to active: %w", dbSchedule.GetName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } else { + // send API call to create the schedule + s, err = database.FromContext(c).CreateSchedule(ctx, s) + if err != nil { + retErr := fmt.Errorf("unable to create new schedule %s: %w", r.GetName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + c.JSON(http.StatusCreated, s) +} + +// validateEntry validates the entry for a minimum frequency. +func validateEntry(minimum time.Duration, entry string) error { + gron := gronx.New() + + // check if expr is even valid + valid := gron.IsValid(entry) + if !valid { + return fmt.Errorf("invalid entry of %s", entry) + } + + // iterate 5 times through ticks in an effort to catch scalene entries + tickForward := 5 + + // start with now + t := time.Now().UTC() + + for i := 0; i < tickForward; i++ { + // check the previous occurrence of the entry + prevTime, err := gronx.PrevTickBefore(entry, t, true) + if err != nil { + return err + } + + // check the next occurrence of the entry + nextTime, err := gronx.NextTickAfter(entry, t, false) + if err != nil { + return err + } + + // ensure the time between previous and next schedule exceeds the minimum duration + if nextTime.Sub(prevTime) < minimum { + return fmt.Errorf("entry needs to occur less frequently than every %s", minimum) + } + + t = nextTime + } + + return nil +} diff --git a/api/schedule/create_test.go b/api/schedule/create_test.go new file mode 100644 index 000000000..a2956f6ca --- /dev/null +++ b/api/schedule/create_test.go @@ -0,0 +1,78 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "testing" + "time" +) + +func Test_validateEntry(t *testing.T) { + type args struct { + minimum time.Duration + entry string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "exceeds minimum frequency", + args: args{ + minimum: 30 * time.Minute, + entry: "* * * * *", + }, + wantErr: true, + }, + { + name: "exceeds minimum frequency with tag", + args: args{ + minimum: 30 * time.Minute, + entry: "@15minutes", + }, + wantErr: true, + }, + { + name: "exceeds minimum frequency with scalene entry pattern", + args: args{ + minimum: 30 * time.Minute, + entry: "1,2,45 * * * *", + }, + wantErr: true, + }, + { + name: "meets minimum frequency", + args: args{ + minimum: 30 * time.Second, + entry: "* * * * *", + }, + wantErr: false, + }, + { + name: "meets minimum frequency with tag", + args: args{ + minimum: 30 * time.Second, + entry: "@hourly", + }, + wantErr: false, + }, + { + name: "meets minimum frequency with comma entry pattern", + args: args{ + minimum: 15 * time.Minute, + entry: "0,15,30,45 * * * *", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := validateEntry(tt.args.minimum, tt.args.entry); (err != nil) != tt.wantErr { + t.Errorf("validateEntry() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/api/schedule/delete.go b/api/schedule/delete.go new file mode 100644 index 000000000..9c6f1dda8 --- /dev/null +++ b/api/schedule/delete.go @@ -0,0 +1,89 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/schedule" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/repos/{org}/{repo}/{schedule} schedules DeleteSchedule +// +// Delete a schedule in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: schedule +// description: Name of the schedule +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted the schedule +// schema: +// type: string +// '500': +// description: Unable to delete the schedule +// schema: +// "$ref": "#/definitions/Error" +// '510': +// description: Unable to delete the schedule +// schema: +// "$ref": "#/definitions/Error" + +// DeleteSchedule represents the API handler to remove +// a schedule from the configured backend. +func DeleteSchedule(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + s := schedule.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("deleting schedule %s", s.GetName()) + + err := database.FromContext(c).DeleteSchedule(ctx, s) + if err != nil { + retErr := fmt.Errorf("unable to delete schedule %s: %w", s.GetName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("schedule %s deleted", s.GetName())) +} diff --git a/api/schedule/get.go b/api/schedule/get.go new file mode 100644 index 000000000..727de5b84 --- /dev/null +++ b/api/schedule/get.go @@ -0,0 +1,69 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/schedule" + "github.com/go-vela/server/router/middleware/user" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/schedules/{org}/{repo}/{schedule} schedules GetSchedule +// +// Get a schedule in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: schedule +// description: Name of the schedule +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the schedule +// schema: +// "$ref": "#/definitions/Schedule" + +// GetSchedule represents the API handler to +// capture a schedule from the configured backend. +func GetSchedule(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + s := schedule.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + "schedule": s.GetName(), + }).Infof("reading schedule %s", s.GetName()) + + c.JSON(http.StatusOK, s) +} diff --git a/api/schedule/list.go b/api/schedule/list.go new file mode 100644 index 000000000..a3d6d6e14 --- /dev/null +++ b/api/schedule/list.go @@ -0,0 +1,132 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/schedules/{org}/{repo} schedules ListSchedules +// +// Get all schedules in the configured backend +// +// --- +// produces: +// - application/json +// security: +// - ApiKeyAuth: [] +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// responses: +// '200': +// description: Successfully retrieved the schedules +// schema: +// type: array +// items: +// "$ref": "#/definitions/Schedule" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve the schedules +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the schedules +// schema: +// "$ref": "#/definitions/Error" + +// ListSchedules represents the API handler to capture a list +// of schedules for a repo from the configured backend. +func ListSchedules(c *gin.Context) { + // capture middleware values + r := repo.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "repo": r.GetName(), + "org": r.GetOrg(), + }).Infof("listing schedules for repo %s", r.GetFullName()) + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + retErr := fmt.Errorf("unable to convert page query parameter for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // send API call to capture the list of schedules for the repo + s, t, err := database.FromContext(c).ListSchedulesForRepo(ctx, r, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to get schedules for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, s) +} diff --git a/api/schedule/update.go b/api/schedule/update.go new file mode 100644 index 000000000..6c2a8b9b6 --- /dev/null +++ b/api/schedule/update.go @@ -0,0 +1,145 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/schedule" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/schedules/{org}/{repo}/{schedule} schedules UpdateSchedule +// +// Update a schedule for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: schedule +// description: Name of the schedule +// required: true +// type: string +// - in: body +// name: body +// description: Payload containing the schedule to update +// required: true +// schema: +// "$ref": "#/definitions/Schedule" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the schedule +// schema: +// "$ref": "#/definitions/Schedule" +// '400': +// description: Unable to update the schedule +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to update the schedule +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update the schedule +// schema: +// "$ref": "#/definitions/Error" + +// UpdateSchedule represents the API handler to update +// a schedule in the configured backend. +func UpdateSchedule(c *gin.Context) { + // capture middleware values + r := repo.Retrieve(c) + s := schedule.Retrieve(c) + ctx := c.Request.Context() + u := user.Retrieve(c) + scheduleName := util.PathParameter(c, "schedule") + minimumFrequency := c.Value("scheduleminimumfrequency").(time.Duration) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "schedule": scheduleName, + "repo": r.GetName(), + "org": r.GetOrg(), + }).Infof("updating schedule %s", scheduleName) + + // capture body from API request + input := new(library.Schedule) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for schedule %s: %w", scheduleName, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update schedule fields if provided + if input.Active != nil { + // update active if set to true + s.SetActive(input.GetActive()) + } + + if input.GetName() != "" { + // update name if defined + s.SetName(input.GetName()) + } + + if input.GetEntry() != "" { + err = validateEntry(minimumFrequency, input.GetEntry()) + if err != nil { + retErr := fmt.Errorf("schedule entry of %s is invalid: %w", input.GetEntry(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update entry if defined + s.SetEntry(input.GetEntry()) + } + + // set the updated by field using claims + s.SetUpdatedBy(u.GetName()) + if input.GetBranch() != "" { + s.SetBranch(input.GetBranch()) + } + + // update the schedule within the database + s, err = database.FromContext(c).UpdateSchedule(ctx, s, true) + if err != nil { + retErr := fmt.Errorf("unable to update scheduled %s: %w", scheduleName, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, s) +} diff --git a/api/scm/doc.go b/api/scm/doc.go new file mode 100644 index 000000000..5d66cafed --- /dev/null +++ b/api/scm/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package scm provides the scm handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/scm" +package scm diff --git a/api/scm/sync.go b/api/scm/sync.go new file mode 100644 index 000000000..dc264f264 --- /dev/null +++ b/api/scm/sync.go @@ -0,0 +1,138 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package scm + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/scm/repos/{org}/{repo}/sync scm SyncRepo +// +// Sync up scm service and database in the context of a specific repo +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully synchronized repo +// schema: +// type: string +// '500': +// description: Unable to synchronize repo +// schema: +// "$ref": "#/definitions/Error" + +// SyncRepo represents the API handler to +// synchronize a single repository between +// SCM service and the database should a discrepancy +// exist. Primarily used for deleted repos or to align +// subscribed events with allowed events. +func SyncRepo(c *gin.Context) { + // capture middleware values + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logger := logrus.WithFields(logrus.Fields{ + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }) + + logger.Infof("syncing repo %s", r.GetFullName()) + + // retrieve repo from source code manager service + _, err := scm.FromContext(c).GetRepo(u, r) + + // if there is an error retrieving repo, we know it is deleted: set to inactive + if err != nil { + // set repo to inactive - do not delete + r.SetActive(false) + + // update repo in database + _, err := database.FromContext(c).UpdateRepo(ctx, r) + if err != nil { + retErr := fmt.Errorf("unable to update repo for org %s: %w", o, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // exit with success as hook sync will be unnecessary + c.JSON(http.StatusOK, fmt.Sprintf("repo %s synced", r.GetFullName())) + + return + } + + // verify the user is an admin of the repo + // we cannot use our normal permissions check due to the possibility the repo was deleted + perm, err := scm.FromContext(c).RepoAccess(u, u.GetToken(), o, r.GetName()) + if err != nil { + logger.Errorf("unable to get user %s access level for org %s", u.GetName(), o) + } + + if !strings.EqualFold(perm, "admin") { + retErr := fmt.Errorf("user %s does not have 'admin' permissions for the repo %s", u.GetName(), r.GetFullName()) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + + // if we have webhook validation, update the repo hook in the SCM + if c.Value("webhookvalidation").(bool) { + // grab last hook from repo to fetch the webhook ID + lastHook, err := database.FromContext(c).LastHookForRepo(r) + if err != nil { + retErr := fmt.Errorf("unable to retrieve last hook for repo %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // update webhook + err = scm.FromContext(c).Update(u, r, lastHook.GetWebhookID()) + if err != nil { + retErr := fmt.Errorf("unable to update repo webhook for %s: %w", r.GetFullName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + c.JSON(http.StatusOK, fmt.Sprintf("repo %s synced", r.GetFullName())) +} diff --git a/api/scm.go b/api/scm/sync_org.go similarity index 52% rename from api/scm.go rename to api/scm/sync_org.go index 69ed13e2c..944a33adc 100644 --- a/api/scm.go +++ b/api/scm/sync_org.go @@ -1,8 +1,8 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. -package api +package scm import ( "fmt" @@ -11,7 +11,6 @@ import ( "github.com/gin-gonic/gin" "github.com/go-vela/server/database" "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/router/middleware/repo" "github.com/go-vela/server/router/middleware/user" "github.com/go-vela/server/scm" "github.com/go-vela/server/util" @@ -19,7 +18,7 @@ import ( "github.com/sirupsen/logrus" ) -// swagger:operation GET /api/v1/scm/orgs/{org}/sync scm SyncRepos +// swagger:operation GET /api/v1/scm/orgs/{org}/sync scm SyncReposForOrg // // Sync up repos from scm service and database in a specified org // @@ -44,14 +43,16 @@ import ( // schema: // "$ref": "#/definitions/Error" -// SyncRepos represents the API handler to +// SyncReposForOrg represents the API handler to // synchronize organization repositories between // SCM Service and the database should a discrepancy -// exist. Common after deleting SCM repos. -func SyncRepos(c *gin.Context) { +// exist. Primarily used for deleted repos or to align +// subscribed events with allowed events. +func SyncReposForOrg(c *gin.Context) { // capture middleware values o := org.Retrieve(c) u := user.Retrieve(c) + ctx := c.Request.Context() // update engine logger with API metadata // @@ -63,20 +64,23 @@ func SyncRepos(c *gin.Context) { logger.Infof("syncing repos for org %s", o) - // See if the user is an org admin to bypass individual permission checks + // see if the user is an org admin perm, err := scm.FromContext(c).OrgAccess(u, o) if err != nil { logger.Errorf("unable to get user %s access level for org %s", u.GetName(), o) } - filters := map[string]string{} - // Only show public repos to non-admins + // only allow org-wide syncing if user is admin of org if perm != "admin" { - filters["visibility"] = "public" + retErr := fmt.Errorf("unable to sync repos in org %s: must be an org admin", o) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return } // send API call to capture the total number of repos for the org - t, err := database.FromContext(c).GetOrgRepoCount(o, filters) + t, err := database.FromContext(c).CountReposForOrg(ctx, o, map[string]interface{}{}) if err != nil { retErr := fmt.Errorf("unable to get repo count for org %s: %w", o, err) @@ -88,9 +92,8 @@ func SyncRepos(c *gin.Context) { repos := []*library.Repo{} page := 0 // capture all repos belonging to a certain org in database - // nolint: gomnd // ignore magic number for orgRepos := int64(0); orgRepos < t; orgRepos += 100 { - r, err := database.FromContext(c).GetOrgRepoList(o, filters, page, 100) + r, _, err := database.FromContext(c).ListReposForOrg(ctx, o, "name", map[string]interface{}{}, page, 100) if err != nil { retErr := fmt.Errorf("unable to get repo count for org %s: %w", o, err) @@ -98,7 +101,9 @@ func SyncRepos(c *gin.Context) { return } + repos = append(repos, r...) + page++ } @@ -109,7 +114,7 @@ func SyncRepos(c *gin.Context) { if err != nil { repo.SetActive(false) - err := database.FromContext(c).UpdateRepo(repo) + _, err := database.FromContext(c).UpdateRepo(ctx, repo) if err != nil { retErr := fmt.Errorf("unable to update repo for org %s: %w", o, err) @@ -118,80 +123,30 @@ func SyncRepos(c *gin.Context) { return } } - } - c.JSON(http.StatusOK, fmt.Sprintf("org %s repos synced", o)) -} - -// swagger:operation GET /api/v1/scm/repos/{org}/{repo}/sync scm SyncRepo -// -// Sync up scm service and database in the context of a specific repo -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully synchronized repo -// schema: -// type: string -// '500': -// description: Unable to synchronize repo -// schema: -// "$ref": "#/definitions/Error" - -// SyncRepo represents the API handler to -// synchronize a single repository between -// SCM service and the database should a discrepancy -// exist. Common after deleting SCM repos. -func SyncRepo(c *gin.Context) { - // capture middleware values - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logger := logrus.WithFields(logrus.Fields{ - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }) - - logger.Infof("syncing repo %s", r.GetFullName()) + // if we have webhook validation, update the repo hook in the SCM + if c.Value("webhookvalidation").(bool) { + // grab last hook from repo to fetch the webhook ID + lastHook, err := database.FromContext(c).LastHookForRepo(repo) + if err != nil { + retErr := fmt.Errorf("unable to retrieve last hook for repo %s: %w", repo.GetFullName(), err) - // retrieve repo from source code manager service - _, err := scm.FromContext(c).GetRepo(u, r) + util.HandleError(c, http.StatusInternalServerError, retErr) - // if there is an error retrieving repo, we know it is deleted: sync time - if err != nil { - // set repo to inactive - do not delete - r.SetActive(false) + return + } - // update repo in database - err := database.FromContext(c).UpdateRepo(r) - if err != nil { - retErr := fmt.Errorf("unable to update repo for org %s: %w", o, err) + // update webhook + err = scm.FromContext(c).Update(u, repo, lastHook.GetWebhookID()) + if err != nil { + retErr := fmt.Errorf("unable to update repo webhook for %s: %w", repo.GetFullName(), err) - util.HandleError(c, http.StatusInternalServerError, retErr) + util.HandleError(c, http.StatusInternalServerError, retErr) - return + return + } } } - c.JSON(http.StatusOK, fmt.Sprintf("repo %s synced", r.GetFullName())) + c.JSON(http.StatusOK, fmt.Sprintf("org %s repos synced", o)) } diff --git a/api/secret.go b/api/secret.go deleted file mode 100644 index c870a1a7b..000000000 --- a/api/secret.go +++ /dev/null @@ -1,762 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "fmt" - "net/http" - "strconv" - "strings" - "time" - - "github.com/go-vela/server/router/middleware/user" - "github.com/go-vela/server/scm" - "github.com/go-vela/server/secret" - "github.com/go-vela/server/util" - - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -// nolint: lll // ignore long line length due to description -// -// swagger:operation POST /api/v1/secrets/{engine}/{type}/{org}/{name} secrets CreateSecret -// -// Create a secret -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: engine -// description: Secret engine to create a secret in, eg. "native" -// required: true -// type: string -// - in: path -// name: type -// description: Secret type to create -// enum: -// - org -// - repo -// - shared -// required: true -// type: string -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: name -// description: Name of the repo if a repo secret, team name if a shared secret, or '*' if an org secret -// required: true -// type: string -// - in: body -// name: body -// description: Payload containing the secret to create -// required: true -// schema: -// "$ref": "#/definitions/Secret" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully created the secret -// schema: -// "$ref": "#/definitions/Secret" -// '400': -// description: Unable to create the secret -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to create the secret -// schema: -// "$ref": "#/definitions/Error" - -// CreateSecret represents the API handler to -// create a secret in the configured backend. -func CreateSecret(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - e := c.Param("engine") - t := c.Param("type") - o := c.Param("org") - n := c.Param("name") - - entry := fmt.Sprintf("%s/%s/%s", t, o, n) - - // create log fields from API metadata - fields := logrus.Fields{ - "engine": e, - "org": o, - "repo": n, - "type": t, - "user": u.GetName(), - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update log fields from API metadata - fields = logrus.Fields{ - "engine": e, - "org": o, - "team": n, - "type": t, - "user": u.GetName(), - } - } - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(fields).Infof("creating new secret %s for %s service", entry, e) - - // capture body from API request - input := new(library.Secret) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for secret %s for %s service: %w", entry, e, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update fields in secret object - input.SetOrg(o) - input.SetRepo(n) - input.SetType(t) - input.SetCreatedAt(time.Now().UTC().Unix()) - input.SetCreatedBy(u.GetName()) - input.SetUpdatedAt(time.Now().UTC().Unix()) - input.SetUpdatedBy(u.GetName()) - - if len(input.GetImages()) > 0 { - input.SetImages(unique(input.GetImages())) - } - - if len(input.GetEvents()) > 0 { - input.SetEvents(unique(input.GetEvents())) - } - - if len(input.GetEvents()) == 0 { - // set default events to enable for the secret - input.SetEvents([]string{constants.EventPush, constants.EventTag, constants.EventDeploy}) - } - - if input.AllowCommand == nil { - input.SetAllowCommand(true) - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update the team instead of repo - input.SetTeam(n) - input.Repo = nil - } - - // send API call to create the secret - err = secret.FromContext(c, e).Create(t, o, n, input) - if err != nil { - retErr := fmt.Errorf("unable to create secret %s for %s service: %w", entry, e, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - s, _ := secret.FromContext(c, e).Get(t, o, n, input.GetName()) - - c.JSON(http.StatusOK, s.Sanitize()) -} - -// nolint: lll // ignore long line length due to description -// -// swagger:operation GET /api/v1/secrets/{engine}/{type}/{org}/{name} secrets GetSecrets -// -// Retrieve a list of secrets from the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: engine -// description: Secret engine to create a secret in, eg. "native" -// required: true -// type: string -// - in: path -// name: type -// description: Secret type to create -// enum: -// - org -// - repo -// - shared -// required: true -// type: string -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: name -// description: Name of the repo if a repo secret, team name if a shared secret, or '*' if an org secret -// required: true -// type: string -// - in: query -// name: page -// description: The page of results to retrieve -// type: integer -// default: 1 -// - in: query -// name: per_page -// description: How many results per page to return -// type: integer -// maximum: 100 -// default: 10 -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the list of secrets -// schema: -// type: array -// items: -// "$ref": "#/definitions/Secret" -// headers: -// X-Total-Count: -// description: Total number of results -// type: integer -// Link: -// description: see https://tools.ietf.org/html/rfc5988 -// type: string -// '400': -// description: Unable to retrieve the list of secrets -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the list of secrets -// schema: -// "$ref": "#/definitions/Error" - -// GetSecrets represents the API handler to capture -// a list of secrets from the configured backend. -// -// nolint: funlen // ignore function length due to comments -func GetSecrets(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - e := c.Param("engine") - t := c.Param("type") - o := c.Param("org") - n := c.Param("name") - - var teams []string - // get list of user's teams if type is shared secret and team is '*' - if t == constants.SecretShared && n == "*" { - var err error - teams, err = scm.FromContext(c).ListUsersTeamsForOrg(u, o) - if err != nil { - retErr := fmt.Errorf("unable to get users %s teams for org %s: %v", u.GetName(), o, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - } - - entry := fmt.Sprintf("%s/%s/%s", t, o, n) - - // create log fields from API metadata - fields := logrus.Fields{ - "engine": e, - "org": o, - "repo": n, - "type": t, - "user": u.GetName(), - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update log fields from API metadata - fields = logrus.Fields{ - "engine": e, - "org": o, - "team": n, - "type": t, - "user": u.GetName(), - } - } - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(fields).Infof("reading secrets %s from %s service", entry, e) - - // capture page query parameter if present - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to convert page query parameter for %s from %s service: %w", entry, e, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // capture per_page query parameter if present - perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to convert per_page query parameter for %s from %s service: %w", entry, e, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // send API call to capture the total number of secrets - total, err := secret.FromContext(c, e).Count(t, o, n, teams) - if err != nil { - retErr := fmt.Errorf("unable to get secret count for %s from %s service: %w", entry, e, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // ensure per_page isn't above or below allowed values - // - // nolint: gomnd // ignore magic number - perPage = util.MaxInt(1, util.MinInt(100, perPage)) - - // send API call to capture the list of secrets - s, err := secret.FromContext(c, e).List(t, o, n, page, perPage, teams) - if err != nil { - retErr := fmt.Errorf("unable to get secrets for %s from %s service: %w", entry, e, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // create pagination object - pagination := Pagination{ - Page: page, - PerPage: perPage, - Total: total, - } - // set pagination headers - pagination.SetHeaderLink(c) - - // variable we want to return - secrets := []*library.Secret{} - // iterate through all secrets - for _, secret := range s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := secret - - // sanitize secret to ensure no value is provided - secrets = append(secrets, tmp.Sanitize()) - } - - c.JSON(http.StatusOK, secrets) -} - -// nolint: lll // ignore long line length due to description -// -// swagger:operation GET /api/v1/secrets/{engine}/{type}/{org}/{name}/{secret} secrets GetSecret -// -// Retrieve a secret from the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: engine -// description: Secret engine to create a secret in, eg. "native" -// required: true -// type: string -// - in: path -// name: type -// description: Secret type to create -// enum: -// - org -// - repo -// - shared -// required: true -// type: string -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: name -// description: Name of the repo if a repo secret, team name if a shared secret, or '*' if an org secret -// required: true -// type: string -// - in: path -// name: secret -// description: Name of the secret -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the secret -// schema: -// "$ref": "#/definitions/Secret" -// '500': -// description: Unable to retrieve the secret -// schema: -// "$ref": "#/definitions/Error" - -// GetSecret gets a secret from the provided secrets service. -func GetSecret(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - e := c.Param("engine") - t := c.Param("type") - o := c.Param("org") - n := c.Param("name") - s := strings.TrimPrefix(c.Param("secret"), "/") - - entry := fmt.Sprintf("%s/%s/%s/%s", t, o, n, s) - - // create log fields from API metadata - fields := logrus.Fields{ - "engine": e, - "org": o, - "repo": n, - "secret": s, - "type": t, - "user": u.GetName(), - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update log fields from API metadata - fields = logrus.Fields{ - "engine": e, - "org": o, - "secret": s, - "team": n, - "type": t, - "user": u.GetName(), - } - } - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(fields).Infof("reading secret %s from %s service", entry, e) - - // send API call to capture the secret - secret, err := secret.FromContext(c, e).Get(t, o, n, s) - if err != nil { - retErr := fmt.Errorf("unable to get secret %s from %s service: %w", entry, e, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // only allow workers to access the full secret with the value - if u.GetAdmin() && u.GetName() == "vela-worker" { - c.JSON(http.StatusOK, secret) - - return - } - - c.JSON(http.StatusOK, secret.Sanitize()) -} - -// nolint: lll // ignore long line length due to description -// -// swagger:operation PUT /api/v1/secrets/{engine}/{type}/{org}/{name}/{secret} secrets UpdateSecrets -// -// Update a secret on the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: engine -// description: Secret engine to update the secret in, eg. "native" -// required: true -// type: string -// - in: path -// name: type -// description: Secret type to update -// enum: -// - org -// - repo -// - shared -// required: true -// type: string -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: name -// description: Name of the repo if a repo secret, team name if a shared secret, or '*' if an org secret -// required: true -// type: string -// - in: path -// name: secret -// description: Name of the secret -// required: true -// type: string -// - in: body -// name: body -// description: Payload containing the secret to create -// required: true -// schema: -// "$ref": "#/definitions/Secret" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully updated the secret -// schema: -// "$ref": "#/definitions/Secret" -// '400': -// description: Unable to update the secret -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to update the secret -// schema: -// "$ref": "#/definitions/Error" - -// UpdateSecret updates a secret for the provided secrets service. -func UpdateSecret(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - e := c.Param("engine") - t := c.Param("type") - o := c.Param("org") - n := c.Param("name") - s := strings.TrimPrefix(c.Param("secret"), "/") - - entry := fmt.Sprintf("%s/%s/%s/%s", t, o, n, s) - - // create log fields from API metadata - fields := logrus.Fields{ - "engine": e, - "org": o, - "repo": n, - "secret": s, - "type": t, - "user": u.GetName(), - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update log fields from API metadata - fields = logrus.Fields{ - "engine": e, - "org": o, - "secret": s, - "team": n, - "type": t, - "user": u.GetName(), - } - } - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(fields).Infof("updating secret %s for %s service", entry, e) - - // capture body from API request - input := new(library.Secret) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for secret %s for %s service: %v", entry, e, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update secret fields if provided - input.SetName(s) - input.SetOrg(o) - input.SetRepo(n) - input.SetType(t) - input.SetUpdatedAt(time.Now().UTC().Unix()) - input.SetUpdatedBy(u.GetName()) - - if input.Images != nil { - // update images if set - input.SetImages(unique(input.GetImages())) - } - - if len(input.GetEvents()) > 0 { - input.SetEvents(unique(input.GetEvents())) - } - - if input.AllowCommand != nil { - // update allow_command if set - input.SetAllowCommand(input.GetAllowCommand()) - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update the team instead of repo - input.SetTeam(n) - input.Repo = nil - } - - // send API call to update the secret - err = secret.FromContext(c, e).Update(t, o, n, input) - if err != nil { - retErr := fmt.Errorf("unable to update secret %s for %s service: %w", entry, e, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated secret - secret, _ := secret.FromContext(c, e).Get(t, o, n, input.GetName()) - - c.JSON(http.StatusOK, secret.Sanitize()) -} - -// nolint: lll // ignore long line length due to description -// -// swagger:operation DELETE /api/v1/secrets/{engine}/{type}/{org}/{name}/{secret} secrets DeleteSecret -// -// Delete a secret from the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: engine -// description: Secret engine to delete the secret from, eg. "native" -// required: true -// type: string -// - in: path -// name: type -// description: Secret type to delete -// enum: -// - org -// - repo -// - shared -// required: true -// type: string -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: name -// description: Name of the repo if a repo secret, team name if a shared secret, or '*' if an org secret -// required: true -// type: string -// - in: path -// name: secret -// description: Name of the secret -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully deleted the secret -// schema: -// type: string -// '500': -// description: Unable to delete the secret -// schema: -// "$ref": "#/definitions/Error" - -// DeleteSecret deletes a secret from the provided secrets service. -func DeleteSecret(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - e := c.Param("engine") - t := c.Param("type") - o := c.Param("org") - n := c.Param("name") - s := strings.TrimPrefix(c.Param("secret"), "/") - - entry := fmt.Sprintf("%s/%s/%s/%s", t, o, n, s) - - // create log fields from API metadata - fields := logrus.Fields{ - "engine": e, - "org": o, - "repo": n, - "secret": s, - "type": t, - "user": u.GetName(), - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update log fields from API metadata - fields = logrus.Fields{ - "engine": e, - "org": o, - "secret": s, - "team": n, - "type": t, - "user": u.GetName(), - } - } - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(fields).Infof("deleting secret %s from %s service", entry, e) - - // send API call to remove the secret - err := secret.FromContext(c, e).Delete(t, o, n, s) - if err != nil { - retErr := fmt.Errorf("unable to delete secret %s from %s service: %w", entry, e, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, fmt.Sprintf("secret %s deleted from %s service", entry, e)) -} - -// unique is a helper function that takes a slice and -// validates that there are no duplicate entries. -func unique(stringSlice []string) []string { - keys := make(map[string]bool) - list := []string{} - - for _, entry := range stringSlice { - if _, value := keys[entry]; !value { - keys[entry] = true - - list = append(list, entry) - } - } - - return list -} diff --git a/api/secret/create.go b/api/secret/create.go new file mode 100644 index 000000000..82a1cac5a --- /dev/null +++ b/api/secret/create.go @@ -0,0 +1,242 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/secret" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// +// swagger:operation POST /api/v1/secrets/{engine}/{type}/{org}/{name} secrets CreateSecret +// +// Create a secret +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: engine +// description: Secret engine to create a secret in, eg. "native" +// required: true +// type: string +// - in: path +// name: type +// description: Secret type to create +// enum: +// - org +// - repo +// - shared +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: name +// description: Name of the repo if a repo secret, team name if a shared secret, or '*' if an org secret +// required: true +// type: string +// - in: body +// name: body +// description: Payload containing the secret to create +// required: true +// schema: +// "$ref": "#/definitions/Secret" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully created the secret +// schema: +// "$ref": "#/definitions/Secret" +// '400': +// description: Unable to create the secret +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the secret +// schema: +// "$ref": "#/definitions/Error" + +// CreateSecret represents the API handler to +// create a secret in the configured backend. +// +//nolint:funlen // suppress long function error +func CreateSecret(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + e := util.PathParameter(c, "engine") + t := util.PathParameter(c, "type") + o := util.PathParameter(c, "org") + n := util.PathParameter(c, "name") + + entry := fmt.Sprintf("%s/%s/%s", t, o, n) + + // create log fields from API metadata + fields := logrus.Fields{ + "engine": e, + "org": o, + "repo": n, + "type": t, + "user": u.GetName(), + } + + // check if secret is a shared secret + if strings.EqualFold(t, constants.SecretShared) { + // update log fields from API metadata + fields = logrus.Fields{ + "engine": e, + "org": o, + "team": n, + "type": t, + "user": u.GetName(), + } + } + + if strings.EqualFold(t, constants.SecretOrg) { + // retrieve org name from SCM + // + // SCM can be case insensitive, causing access retrieval to work + // but Org/Repo != org/repo in Vela. So this check ensures that + // what a user inputs matches the casing we expect in Vela since + // the SCM will have the source of truth for casing. + org, err := scm.FromContext(c).GetOrgName(u, o) + if err != nil { + retErr := fmt.Errorf("unable to retrieve organization %s", o) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // check if casing is accurate + if org != o { + retErr := fmt.Errorf("unable to retrieve organization %s. Did you mean %s?", o, org) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + } + + if strings.EqualFold(t, constants.SecretRepo) { + // retrieve org and repo name from SCM + // + // same story as org secret. SCM has accurate casing. + scmOrg, scmRepo, err := scm.FromContext(c).GetOrgAndRepoName(u, o, n) + if err != nil { + retErr := fmt.Errorf("unable to retrieve repository %s/%s", o, n) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // check if casing is accurate for org entry + if scmOrg != o { + retErr := fmt.Errorf("unable to retrieve org %s. Did you mean %s?", o, scmOrg) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // check if casing is accurate for repo entry + if scmRepo != n { + retErr := fmt.Errorf("unable to retrieve repository %s. Did you mean %s?", n, scmRepo) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(fields).Infof("creating new secret %s for %s service", entry, e) + + // capture body from API request + input := new(library.Secret) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for secret %s for %s service: %w", entry, e, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // reject secrets with solely whitespace characters as its value + trimmed := strings.TrimSpace(input.GetValue()) + if len(trimmed) == 0 { + retErr := fmt.Errorf("secret value must contain non-whitespace characters") + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update fields in secret object + input.SetOrg(o) + input.SetRepo(n) + input.SetType(t) + input.SetCreatedAt(time.Now().UTC().Unix()) + input.SetCreatedBy(u.GetName()) + input.SetUpdatedAt(time.Now().UTC().Unix()) + input.SetUpdatedBy(u.GetName()) + + if len(input.GetImages()) > 0 { + input.SetImages(util.Unique(input.GetImages())) + } + + if len(input.GetEvents()) > 0 { + input.SetEvents(util.Unique(input.GetEvents())) + } + + if len(input.GetEvents()) == 0 { + // set default events to enable for the secret + input.SetEvents([]string{constants.EventPush, constants.EventTag, constants.EventDeploy}) + } + + if input.AllowCommand == nil { + input.SetAllowCommand(true) + } + + // check if secret is a shared secret + if strings.EqualFold(t, constants.SecretShared) { + // update the team instead of repo + input.SetTeam(n) + input.Repo = nil + } + + // send API call to create the secret + s, err := secret.FromContext(c, e).Create(t, o, n, input) + if err != nil { + retErr := fmt.Errorf("unable to create secret %s for %s service: %w", entry, e, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, s.Sanitize()) +} diff --git a/api/secret/delete.go b/api/secret/delete.go new file mode 100644 index 000000000..f8134ba5f --- /dev/null +++ b/api/secret/delete.go @@ -0,0 +1,121 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/secret" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" +) + +// +// swagger:operation DELETE /api/v1/secrets/{engine}/{type}/{org}/{name}/{secret} secrets DeleteSecret +// +// Delete a secret from the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: engine +// description: Secret engine to delete the secret from, eg. "native" +// required: true +// type: string +// - in: path +// name: type +// description: Secret type to delete +// enum: +// - org +// - repo +// - shared +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: name +// description: Name of the repo if a repo secret, team name if a shared secret, or '*' if an org secret +// required: true +// type: string +// - in: path +// name: secret +// description: Name of the secret +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted the secret +// schema: +// type: string +// '500': +// description: Unable to delete the secret +// schema: +// "$ref": "#/definitions/Error" + +// DeleteSecret deletes a secret from the provided secrets service. +func DeleteSecret(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + e := util.PathParameter(c, "engine") + t := util.PathParameter(c, "type") + o := util.PathParameter(c, "org") + n := util.PathParameter(c, "name") + s := strings.TrimPrefix(util.PathParameter(c, "secret"), "/") + + entry := fmt.Sprintf("%s/%s/%s/%s", t, o, n, s) + + // create log fields from API metadata + fields := logrus.Fields{ + "engine": e, + "org": o, + "repo": n, + "secret": s, + "type": t, + "user": u.GetName(), + } + + // check if secret is a shared secret + if strings.EqualFold(t, constants.SecretShared) { + // update log fields from API metadata + fields = logrus.Fields{ + "engine": e, + "org": o, + "secret": s, + "team": n, + "type": t, + "user": u.GetName(), + } + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(fields).Infof("deleting secret %s from %s service", entry, e) + + // send API call to remove the secret + err := secret.FromContext(c, e).Delete(t, o, n, s) + if err != nil { + retErr := fmt.Errorf("unable to delete secret %s from %s service: %w", entry, e, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("secret %s deleted from %s service", entry, e)) +} diff --git a/api/secret/doc.go b/api/secret/doc.go new file mode 100644 index 000000000..db89d7b55 --- /dev/null +++ b/api/secret/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package secret provides the secret handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/secret" +package secret diff --git a/api/secret/get.go b/api/secret/get.go new file mode 100644 index 000000000..0f24b95c0 --- /dev/null +++ b/api/secret/get.go @@ -0,0 +1,130 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/claims" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/secret" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" +) + +// +// swagger:operation GET /api/v1/secrets/{engine}/{type}/{org}/{name}/{secret} secrets GetSecret +// +// Retrieve a secret from the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: engine +// description: Secret engine to create a secret in, eg. "native" +// required: true +// type: string +// - in: path +// name: type +// description: Secret type to create +// enum: +// - org +// - repo +// - shared +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: name +// description: Name of the repo if a repo secret, team name if a shared secret, or '*' if an org secret +// required: true +// type: string +// - in: path +// name: secret +// description: Name of the secret +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the secret +// schema: +// "$ref": "#/definitions/Secret" +// '500': +// description: Unable to retrieve the secret +// schema: +// "$ref": "#/definitions/Error" + +// GetSecret gets a secret from the provided secrets service. +func GetSecret(c *gin.Context) { + // capture middleware values + cl := claims.Retrieve(c) + u := user.Retrieve(c) + e := util.PathParameter(c, "engine") + t := util.PathParameter(c, "type") + o := util.PathParameter(c, "org") + n := util.PathParameter(c, "name") + s := strings.TrimPrefix(util.PathParameter(c, "secret"), "/") + + entry := fmt.Sprintf("%s/%s/%s/%s", t, o, n, s) + + // create log fields from API metadata + fields := logrus.Fields{ + "engine": e, + "org": o, + "repo": n, + "secret": s, + "type": t, + "user": u.GetName(), + } + + // check if secret is a shared secret + if strings.EqualFold(t, constants.SecretShared) { + // update log fields from API metadata + fields = logrus.Fields{ + "engine": e, + "org": o, + "secret": s, + "team": n, + "type": t, + "user": u.GetName(), + } + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(fields).Infof("reading secret %s from %s service", entry, e) + + // send API call to capture the secret + secret, err := secret.FromContext(c, e).Get(t, o, n, s) + if err != nil { + retErr := fmt.Errorf("unable to get secret %s from %s service: %w", entry, e, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // only allow workers to access the full secret with the value + if strings.EqualFold(cl.TokenType, constants.WorkerBuildTokenType) { + c.JSON(http.StatusOK, secret) + + return + } + + c.JSON(http.StatusOK, secret.Sanitize()) +} diff --git a/api/secret/list.go b/api/secret/list.go new file mode 100644 index 000000000..e603c473f --- /dev/null +++ b/api/secret/list.go @@ -0,0 +1,210 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/secret" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// +// swagger:operation GET /api/v1/secrets/{engine}/{type}/{org}/{name} secrets ListSecrets +// +// Retrieve a list of secrets from the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: engine +// description: Secret engine to create a secret in, eg. "native" +// required: true +// type: string +// - in: path +// name: type +// description: Secret type to create +// enum: +// - org +// - repo +// - shared +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: name +// description: Name of the repo if a repo secret, team name if a shared secret, or '*' if an org secret +// required: true +// type: string +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the list of secrets +// schema: +// type: array +// items: +// "$ref": "#/definitions/Secret" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve the list of secrets +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the list of secrets +// schema: +// "$ref": "#/definitions/Error" + +// ListSecrets represents the API handler to capture +// a list of secrets from the configured backend. +func ListSecrets(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + e := util.PathParameter(c, "engine") + t := util.PathParameter(c, "type") + o := util.PathParameter(c, "org") + n := util.PathParameter(c, "name") + + var teams []string + // get list of user's teams if type is shared secret and team is '*' + if t == constants.SecretShared && n == "*" { + var err error + + teams, err = scm.FromContext(c).ListUsersTeamsForOrg(u, o) + if err != nil { + retErr := fmt.Errorf("unable to list users %s teams for org %s: %w", u.GetName(), o, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + } + + entry := fmt.Sprintf("%s/%s/%s", t, o, n) + + // create log fields from API metadata + fields := logrus.Fields{ + "engine": e, + "org": o, + "repo": n, + "type": t, + "user": u.GetName(), + } + + // check if secret is a shared secret + if strings.EqualFold(t, constants.SecretShared) { + // update log fields from API metadata + fields = logrus.Fields{ + "engine": e, + "org": o, + "team": n, + "type": t, + "user": u.GetName(), + } + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(fields).Infof("listing secrets %s from %s service", entry, e) + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + retErr := fmt.Errorf("unable to convert page query parameter for %s from %s service: %w", entry, e, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for %s from %s service: %w", entry, e, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture the total number of secrets + total, err := secret.FromContext(c, e).Count(t, o, n, teams) + if err != nil { + retErr := fmt.Errorf("unable to get secret count for %s from %s service: %w", entry, e, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // send API call to capture the list of secrets + s, err := secret.FromContext(c, e).List(t, o, n, page, perPage, teams) + if err != nil { + retErr := fmt.Errorf("unable to list secrets for %s from %s service: %w", entry, e, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: total, + } + // set pagination headers + pagination.SetHeaderLink(c) + + // variable we want to return + secrets := []*library.Secret{} + // iterate through all secrets + for _, secret := range s { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := secret + + // sanitize secret to ensure no value is provided + secrets = append(secrets, tmp.Sanitize()) + } + + c.JSON(http.StatusOK, secrets) +} diff --git a/api/secret/update.go b/api/secret/update.go new file mode 100644 index 000000000..1feaae4c3 --- /dev/null +++ b/api/secret/update.go @@ -0,0 +1,174 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/secret" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// +// swagger:operation PUT /api/v1/secrets/{engine}/{type}/{org}/{name}/{secret} secrets UpdateSecret +// +// Update a secret on the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: engine +// description: Secret engine to update the secret in, eg. "native" +// required: true +// type: string +// - in: path +// name: type +// description: Secret type to update +// enum: +// - org +// - repo +// - shared +// required: true +// type: string +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: name +// description: Name of the repo if a repo secret, team name if a shared secret, or '*' if an org secret +// required: true +// type: string +// - in: path +// name: secret +// description: Name of the secret +// required: true +// type: string +// - in: body +// name: body +// description: Payload containing the secret to create +// required: true +// schema: +// "$ref": "#/definitions/Secret" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the secret +// schema: +// "$ref": "#/definitions/Secret" +// '400': +// description: Unable to update the secret +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update the secret +// schema: +// "$ref": "#/definitions/Error" + +// UpdateSecret updates a secret for the provided secrets service. +func UpdateSecret(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + e := util.PathParameter(c, "engine") + t := util.PathParameter(c, "type") + o := util.PathParameter(c, "org") + n := util.PathParameter(c, "name") + s := strings.TrimPrefix(util.PathParameter(c, "secret"), "/") + + entry := fmt.Sprintf("%s/%s/%s/%s", t, o, n, s) + + // create log fields from API metadata + fields := logrus.Fields{ + "engine": e, + "org": o, + "repo": n, + "secret": s, + "type": t, + "user": u.GetName(), + } + + // check if secret is a shared secret + if strings.EqualFold(t, constants.SecretShared) { + // update log fields from API metadata + fields = logrus.Fields{ + "engine": e, + "org": o, + "secret": s, + "team": n, + "type": t, + "user": u.GetName(), + } + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(fields).Infof("updating secret %s for %s service", entry, e) + + // capture body from API request + input := new(library.Secret) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for secret %s for %s service: %w", entry, e, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update secret fields if provided + input.SetName(s) + input.SetOrg(o) + input.SetRepo(n) + input.SetType(t) + input.SetUpdatedAt(time.Now().UTC().Unix()) + input.SetUpdatedBy(u.GetName()) + + if input.Images != nil { + // update images if set + input.SetImages(util.Unique(input.GetImages())) + } + + if len(input.GetEvents()) > 0 { + input.SetEvents(util.Unique(input.GetEvents())) + } + + if input.AllowCommand != nil { + // update allow_command if set + input.SetAllowCommand(input.GetAllowCommand()) + } + + // check if secret is a shared secret + if strings.EqualFold(t, constants.SecretShared) { + // update the team instead of repo + input.SetTeam(n) + input.Repo = nil + } + + // send API call to update the secret + secret, err := secret.FromContext(c, e).Update(t, o, n, input) + if err != nil { + retErr := fmt.Errorf("unable to update secret %s for %s service: %w", entry, e, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, secret.Sanitize()) +} diff --git a/api/service.go b/api/service.go deleted file mode 100644 index 29ece1604..000000000 --- a/api/service.go +++ /dev/null @@ -1,610 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "fmt" - "net/http" - "strconv" - "time" - - "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/router/middleware/user" - - "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/build" - "github.com/go-vela/server/router/middleware/repo" - "github.com/go-vela/server/router/middleware/service" - "github.com/go-vela/server/util" - - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/go-vela/types/pipeline" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/services services CreateService -// -// Create a service for a build in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: body -// name: body -// description: Payload containing the service to create -// required: true -// schema: -// "$ref": "#/definitions/Service" -// security: -// - ApiKeyAuth: [] -// responses: -// '201': -// description: Successfully created the service -// schema: -// "$ref": "#/definitions/Service" -// '400': -// description: Unable to create the service -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to create the service -// schema: -// "$ref": "#/definitions/Error" - -// CreateService represents the API handler to create -// a service for a build in the configured backend. -// -// nolint: dupl // ignore similar code with step -func CreateService(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("creating new service for build %s", entry) - - // capture body from API request - input := new(library.Service) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for new service for build %s: %w", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update fields in service object - input.SetRepoID(r.GetID()) - input.SetBuildID(b.GetID()) - - if len(input.GetStatus()) == 0 { - input.SetStatus(constants.StatusPending) - } - - if input.GetCreated() == 0 { - input.SetCreated(time.Now().UTC().Unix()) - } - - // send API call to create the service - err = database.FromContext(c).CreateService(input) - if err != nil { - retErr := fmt.Errorf("unable to create service for build %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the created service - s, _ := database.FromContext(c).GetService(input.GetNumber(), b) - - c.JSON(http.StatusCreated, s) -} - -// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/services services GetServices -// -// Get a list of all services for a build in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: query -// name: page -// description: The page of results to retrieve -// type: integer -// default: 1 -// - in: query -// name: per_page -// description: How many results per page to return -// type: integer -// maximum: 100 -// default: 10 -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the list of services -// schema: -// type: array -// items: -// "$ref": "#/definitions/Service" -// headers: -// X-Total-Count: -// description: Total number of results -// type: integer -// Link: -// description: see https://tools.ietf.org/html/rfc5988 -// type: string -// '400': -// description: Unable to retrieve the list of services -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the list of services -// schema: -// "$ref": "#/definitions/Error" - -// GetServices represents the API handler to capture a list -// of services for a build from the configured backend. -func GetServices(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading services for build %s", entry) - - // capture page query parameter if present - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) - if err != nil { - retErr := fmt.Errorf("unable to convert page query parameter for build %s: %w", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // capture per_page query parameter if present - perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) - if err != nil { - retErr := fmt.Errorf("unable to convert per_page query parameter for build %s: %w", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // ensure per_page isn't above or below allowed values - // - // nolint: gomnd // ignore magic number - perPage = util.MaxInt(1, util.MinInt(100, perPage)) - - // send API call to capture the total number of services for the build - t, err := database.FromContext(c).GetBuildServiceCount(b) - if err != nil { - retErr := fmt.Errorf("unable to get services count for build %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the list of services for the build - s, err := database.FromContext(c).GetBuildServiceList(b, page, perPage) - if err != nil { - retErr := fmt.Errorf("unable to get services for build %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // create pagination object - pagination := Pagination{ - Page: page, - PerPage: perPage, - Total: t, - } - // set pagination headers - pagination.SetHeaderLink(c) - - c.JSON(http.StatusOK, s) -} - -// nolint: lll // ignore long line length due to API path -// -// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/services/{service} services GetService -// -// Get a service for a build in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: service -// description: Name of the service -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the service -// schema: -// "$ref": "#/definitions/Service" -// '400': -// description: Unable to retrieve the service -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the service -// schema: -// "$ref": "#/definitions/Error" - -// GetService represents the API handler to capture a -// service for a build from the configured backend. -func GetService(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := service.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "service": s.GetNumber(), - "user": u.GetName(), - }).Infof("reading service %s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - c.JSON(http.StatusOK, s) -} - -// nolint: lll // ignore long line length due to API path -// -// swagger:operation PUT /api/v1/repos/{org}/{repo}/builds/{build}/services/{service} services UpdateService -// -// Update a service for a build in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: service -// description: Service number -// required: true -// type: integer -// - in: body -// name: body -// description: Payload containing the service to update -// required: true -// schema: -// "$ref": "#/definitions/Service" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully updated the service -// schema: -// "$ref": "#/definitions/Service" -// '400': -// description: Unable to update the service -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to update the service -// schema: -// "$ref": "#/definitions/Error" - -// UpdateService represents the API handler to update -// a service for a build in the configured backend. -func UpdateService(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := service.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "service": s.GetNumber(), - "user": u.GetName(), - }).Infof("updating service %s", entry) - - // capture body from API request - input := new(library.Service) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for service %s: %w", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update service fields if provided - if len(input.GetStatus()) > 0 { - // update status if set - s.SetStatus(input.GetStatus()) - } - - if len(input.GetError()) > 0 { - // update error if set - s.SetError(input.GetError()) - } - - if input.GetExitCode() > 0 { - // update exit_code if set - s.SetExitCode(input.GetExitCode()) - } - - if input.GetStarted() > 0 { - // update started if set - s.SetStarted(input.GetStarted()) - } - - if input.GetFinished() > 0 { - // update finished if set - s.SetFinished(input.GetFinished()) - } - - // send API call to update the service - err = database.FromContext(c).UpdateService(s) - if err != nil { - retErr := fmt.Errorf("unable to update service %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated service - s, _ = database.FromContext(c).GetService(s.GetNumber(), b) - - c.JSON(http.StatusOK, s) -} - -// nolint: lll // ignore long line length due to API path -// -// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build}/services/{service} services DeleteService -// -// Delete a service for a build in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: service -// description: Service Number -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully deleted the service -// schema: -// type: string -// '500': -// description: Unable to delete the service -// schema: -// "$ref": "#/definitions/Error" - -// DeleteService represents the API handler to remove -// a service for a build from the configured backend. -// -// nolint: dupl // ignore similar code with step -func DeleteService(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := service.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "service": s.GetNumber(), - "user": u.GetName(), - }).Infof("deleting service %s", entry) - - // send API call to remove the service - err := database.FromContext(c).DeleteService(s.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to delete service %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, fmt.Sprintf("service %s deleted", entry)) -} - -// planServices is a helper function to plan all services -// in the build for execution. This creates the services -// for the build in the configured backend. -// -// nolint: lll // ignore long line length due to variable names -func planServices(database database.Service, p *pipeline.Build, b *library.Build) ([]*library.Service, error) { - // variable to store planned services - services := []*library.Service{} - - // iterate through all pipeline services - for _, service := range p.Services { - // create the service object - s := new(library.Service) - s.SetBuildID(b.GetID()) - s.SetRepoID(b.GetRepoID()) - s.SetName(service.Name) - s.SetImage(service.Image) - s.SetNumber(service.Number) - s.SetStatus(constants.StatusPending) - s.SetCreated(time.Now().UTC().Unix()) - - // send API call to create the service - err := database.CreateService(s) - if err != nil { - return services, fmt.Errorf("unable to create service %s: %w", s.GetName(), err) - } - - // send API call to capture the created service - s, err = database.GetService(s.GetNumber(), b) - if err != nil { - return services, fmt.Errorf("unable to get service %s: %w", s.GetName(), err) - } - - // populate environment variables from service library - // - // https://pkg.go.dev/github.com/go-vela/types/library#Service.Environment - err = service.MergeEnv(s.Environment()) - if err != nil { - return services, err - } - - // create the log object - l := new(library.Log) - l.SetServiceID(s.GetID()) - l.SetBuildID(b.GetID()) - l.SetRepoID(b.GetRepoID()) - l.SetData([]byte{}) - - // send API call to create the service logs - err = database.CreateLog(l) - if err != nil { - return services, fmt.Errorf("unable to create service logs for service %s: %w", s.GetName(), err) - } - } - - return services, nil -} diff --git a/api/service/create.go b/api/service/create.go new file mode 100644 index 000000000..2dcccbd26 --- /dev/null +++ b/api/service/create.go @@ -0,0 +1,125 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/services services CreateService +// +// Create a service for a build in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: body +// name: body +// description: Payload containing the service to create +// required: true +// schema: +// "$ref": "#/definitions/Service" +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: Successfully created the service +// schema: +// "$ref": "#/definitions/Service" +// '400': +// description: Unable to create the service +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the service +// schema: +// "$ref": "#/definitions/Error" + +// CreateService represents the API handler to create +// a service for a build in the configured backend. +func CreateService(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("creating new service for build %s", entry) + + // capture body from API request + input := new(library.Service) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for new service for build %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update fields in service object + input.SetRepoID(r.GetID()) + input.SetBuildID(b.GetID()) + + if len(input.GetStatus()) == 0 { + input.SetStatus(constants.StatusPending) + } + + if input.GetCreated() == 0 { + input.SetCreated(time.Now().UTC().Unix()) + } + + // send API call to create the service + s, err := database.FromContext(c).CreateService(input) + if err != nil { + retErr := fmt.Errorf("unable to create service for build %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusCreated, s) +} diff --git a/api/service/delete.go b/api/service/delete.go new file mode 100644 index 000000000..d85aa0fe8 --- /dev/null +++ b/api/service/delete.go @@ -0,0 +1,97 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/service" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// +// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build}/services/{service} services DeleteService +// +// Delete a service for a build in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: service +// description: Service Number +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted the service +// schema: +// type: string +// '500': +// description: Unable to delete the service +// schema: +// "$ref": "#/definitions/Error" + +// DeleteService represents the API handler to remove +// a service for a build from the configured backend. +func DeleteService(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := service.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "service": s.GetNumber(), + "user": u.GetName(), + }).Infof("deleting service %s", entry) + + // send API call to remove the service + err := database.FromContext(c).DeleteService(s) + if err != nil { + retErr := fmt.Errorf("unable to delete service %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("service %s deleted", entry)) +} diff --git a/api/service/doc.go b/api/service/doc.go new file mode 100644 index 000000000..53dc07284 --- /dev/null +++ b/api/service/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package service provides the service handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/service" +package service diff --git a/api/service/get.go b/api/service/get.go new file mode 100644 index 000000000..b2165a69f --- /dev/null +++ b/api/service/get.go @@ -0,0 +1,86 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/service" + "github.com/go-vela/server/router/middleware/user" + "github.com/sirupsen/logrus" +) + +// +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/services/{service} services GetService +// +// Get a service for a build in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: service +// description: Name of the service +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the service +// schema: +// "$ref": "#/definitions/Service" +// '400': +// description: Unable to retrieve the service +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the service +// schema: +// "$ref": "#/definitions/Error" + +// GetService represents the API handler to capture a +// service for a build from the configured backend. +func GetService(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := service.Retrieve(c) + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "service": s.GetNumber(), + "user": u.GetName(), + }).Infof("reading service %s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + c.JSON(http.StatusOK, s) +} diff --git a/api/service/list.go b/api/service/list.go new file mode 100644 index 000000000..22a7b8be7 --- /dev/null +++ b/api/service/list.go @@ -0,0 +1,146 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/services services ListServices +// +// Get a list of all services for a build in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the list of services +// schema: +// type: array +// items: +// "$ref": "#/definitions/Service" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve the list of services +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the list of services +// schema: +// "$ref": "#/definitions/Error" + +// ListServices represents the API handler to capture a list +// of services for a build from the configured backend. +func ListServices(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("reading services for build %s", entry) + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + retErr := fmt.Errorf("unable to convert page query parameter for build %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for build %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // send API call to capture the list of services for the build + s, t, err := database.FromContext(c).ListServicesForBuild(b, map[string]interface{}{}, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to get services for build %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, s) +} diff --git a/api/service/plan.go b/api/service/plan.go new file mode 100644 index 000000000..a0200767e --- /dev/null +++ b/api/service/plan.go @@ -0,0 +1,65 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "fmt" + "time" + + "github.com/go-vela/server/database" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" +) + +// PlanServices is a helper function to plan all services +// in the build for execution. This creates the services +// for the build in the configured backend. +func PlanServices(database database.Interface, p *pipeline.Build, b *library.Build) ([]*library.Service, error) { + // variable to store planned services + services := []*library.Service{} + + // iterate through all pipeline services + for _, service := range p.Services { + // create the service object + s := new(library.Service) + s.SetBuildID(b.GetID()) + s.SetRepoID(b.GetRepoID()) + s.SetName(service.Name) + s.SetImage(service.Image) + s.SetNumber(service.Number) + s.SetStatus(constants.StatusPending) + s.SetCreated(time.Now().UTC().Unix()) + + // send API call to create the service + s, err := database.CreateService(s) + if err != nil { + return services, fmt.Errorf("unable to create service %s: %w", s.GetName(), err) + } + + // populate environment variables from service library + // + // https://pkg.go.dev/github.com/go-vela/types/library#Service.Environment + err = service.MergeEnv(s.Environment()) + if err != nil { + return services, err + } + + // create the log object + l := new(library.Log) + l.SetServiceID(s.GetID()) + l.SetBuildID(b.GetID()) + l.SetRepoID(b.GetRepoID()) + l.SetData([]byte{}) + + // send API call to create the service logs + err = database.CreateLog(l) + if err != nil { + return services, fmt.Errorf("unable to create service logs for service %s: %w", s.GetName(), err) + } + } + + return services, nil +} diff --git a/api/service/update.go b/api/service/update.go new file mode 100644 index 000000000..7c1a5859f --- /dev/null +++ b/api/service/update.go @@ -0,0 +1,146 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/service" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// +// swagger:operation PUT /api/v1/repos/{org}/{repo}/builds/{build}/services/{service} services UpdateService +// +// Update a service for a build in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: service +// description: Service number +// required: true +// type: integer +// - in: body +// name: body +// description: Payload containing the service to update +// required: true +// schema: +// "$ref": "#/definitions/Service" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the service +// schema: +// "$ref": "#/definitions/Service" +// '400': +// description: Unable to update the service +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update the service +// schema: +// "$ref": "#/definitions/Error" + +// UpdateService represents the API handler to update +// a service for a build in the configured backend. +func UpdateService(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := service.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "service": s.GetNumber(), + "user": u.GetName(), + }).Infof("updating service %s", entry) + + // capture body from API request + input := new(library.Service) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for service %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update service fields if provided + if len(input.GetStatus()) > 0 { + // update status if set + s.SetStatus(input.GetStatus()) + } + + if len(input.GetError()) > 0 { + // update error if set + s.SetError(input.GetError()) + } + + if input.GetExitCode() > 0 { + // update exit_code if set + s.SetExitCode(input.GetExitCode()) + } + + if input.GetStarted() > 0 { + // update started if set + s.SetStarted(input.GetStarted()) + } + + if input.GetFinished() > 0 { + // update finished if set + s.SetFinished(input.GetFinished()) + } + + // send API call to update the service + s, err = database.FromContext(c).UpdateService(s) + if err != nil { + retErr := fmt.Errorf("unable to update service %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, s) +} diff --git a/api/step.go b/api/step.go deleted file mode 100644 index 32c111af5..000000000 --- a/api/step.go +++ /dev/null @@ -1,662 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "fmt" - "net/http" - "strconv" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/build" - "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/router/middleware/repo" - "github.com/go-vela/server/router/middleware/step" - "github.com/go-vela/server/router/middleware/user" - "github.com/go-vela/server/util" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/go-vela/types/pipeline" - "github.com/sirupsen/logrus" -) - -// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/steps steps CreateStep -// -// Create a step for a build -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: body -// name: body -// description: Payload containing the step to create -// required: true -// schema: -// "$ref": "#/definitions/Step" -// security: -// - ApiKeyAuth: [] -// responses: -// '201': -// description: Successfully created the step -// schema: -// "$ref": "#/definitions/Step" -// '400': -// description: Unable to create the step -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to create the step -// schema: -// "$ref": "#/definitions/Error" - -// CreateStep represents the API handler to create -// a step for a build in the configured backend. -// -// nolint: dupl // ignore similar code with service -func CreateStep(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("creating new step for build %s", entry) - - // capture body from API request - input := new(library.Step) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for new step for build %s: %w", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update fields in step object - input.SetRepoID(r.GetID()) - input.SetBuildID(b.GetID()) - - if len(input.GetStatus()) == 0 { - input.SetStatus(constants.StatusPending) - } - - if input.GetCreated() == 0 { - input.SetCreated(time.Now().UTC().Unix()) - } - - // send API call to create the step - err = database.FromContext(c).CreateStep(input) - if err != nil { - retErr := fmt.Errorf("unable to create step for build %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the created step - s, _ := database.FromContext(c).GetStep(input.GetNumber(), b) - - c.JSON(http.StatusCreated, s) -} - -// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/steps steps GetSteps -// -// Retrieve a list of steps for a build -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: query -// name: page -// description: The page of results to retrieve -// type: integer -// default: 1 -// - in: query -// name: per_page -// description: How many results per page to return -// type: integer -// maximum: 100 -// default: 10 -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the list of steps -// schema: -// type: array -// items: -// "$ref": "#/definitions/Step" -// headers: -// X-Total-Count: -// description: Total number of results -// type: integer -// Link: -// description: see https://tools.ietf.org/html/rfc5988 -// type: string -// '400': -// description: Unable to retrieve the list of steps -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the list of steps -// schema: -// "$ref": "#/definitions/Error" - -// GetSteps represents the API handler to capture a list -// of steps for a build from the configured backend. -func GetSteps(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "user": u.GetName(), - }).Infof("reading steps for build %s", entry) - - // capture page query parameter if present - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) - if err != nil { - retErr := fmt.Errorf("unable to convert page query parameter for build %s: %w", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // capture per_page query parameter if present - perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) - if err != nil { - retErr := fmt.Errorf("unable to convert per_page query parameter for build %s: %w", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // ensure per_page isn't above or below allowed values - // - // nolint: gomnd // ignore magic number - perPage = util.MaxInt(1, util.MinInt(100, perPage)) - - // send API call to capture the total number of steps for the build - t, err := database.FromContext(c).GetBuildStepCount(b) - if err != nil { - retErr := fmt.Errorf("unable to get steps count for build %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the list of steps for the build - s, err := database.FromContext(c).GetBuildStepList(b, page, perPage) - if err != nil { - retErr := fmt.Errorf("unable to get steps for build %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // create pagination object - pagination := Pagination{ - Page: page, - PerPage: perPage, - Total: t, - } - // set pagination headers - pagination.SetHeaderLink(c) - - c.JSON(http.StatusOK, s) -} - -// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step} steps GetStep -// -// Retrieve a step for a build -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: step -// description: Build number -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the step -// schema: -// "$ref": "#/definitions/Step" - -// GetStep represents the API handler to capture a -// step for a build from the configured backend. -func GetStep(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := step.Retrieve(c) - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "step": s.GetNumber(), - "user": u.GetName(), - }).Infof("reading step %s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - c.JSON(http.StatusOK, s) -} - -// swagger:operation PUT /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step} steps UpdateStep -// -// Update a step for a build -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: step -// description: Step number -// required: true -// type: integer -// - in: body -// name: body -// description: Payload containing the step to update -// required: true -// schema: -// "$ref": "#/definitions/Step" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully updated the step -// schema: -// "$ref": "#/definitions/Step" -// '400': -// description: Unable to update the step -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to update the step -// schema: -// "$ref": "#/definitions/Error" - -// UpdateStep represents the API handler to update -// a step for a build in the configured backend. -func UpdateStep(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := step.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "step": s.GetNumber(), - "user": u.GetName(), - }).Infof("updating step %s", entry) - - // capture body from API request - input := new(library.Step) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for step %s: %v", entry, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update step fields if provided - if len(input.GetStatus()) > 0 { - // update status if set - s.SetStatus(input.GetStatus()) - } - - if len(input.GetError()) > 0 { - // update error if set - s.SetError(input.GetError()) - } - - if input.GetExitCode() > 0 { - // update exit_code if set - s.SetExitCode(input.GetExitCode()) - } - - if input.GetStarted() > 0 { - // update started if set - s.SetStarted(input.GetStarted()) - } - - if input.GetFinished() > 0 { - // update finished if set - s.SetFinished(input.GetFinished()) - } - - if len(input.GetHost()) > 0 { - // update host if set - s.SetHost(input.GetHost()) - } - - if len(input.GetRuntime()) > 0 { - // update runtime if set - s.SetRuntime(input.GetRuntime()) - } - - if len(input.GetDistribution()) > 0 { - // update distribution if set - s.SetDistribution(input.GetDistribution()) - } - - // send API call to update the step - err = database.FromContext(c).UpdateStep(s) - if err != nil { - retErr := fmt.Errorf("unable to update step %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated step - s, _ = database.FromContext(c).GetStep(s.GetNumber(), b) - - c.JSON(http.StatusOK, s) -} - -// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step} steps DeleteStep -// -// Delete a step for a build -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: step -// description: Step number -// required: true -// type: integer -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully deleted the step -// schema: -// type: string -// '500': -// description: Successfully deleted the step -// schema: -// "$ref": "#/definitions/Error" - -// DeleteStep represents the API handler to remove -// a step for a build from the configured backend. -// -// nolint: dupl // ignore similar code with service -func DeleteStep(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := step.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "step": s.GetNumber(), - "user": u.GetName(), - }).Infof("deleting step %s", entry) - - // send API call to remove the step - err := database.FromContext(c).DeleteStep(s.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to delete step %s: %w", entry, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, fmt.Sprintf("step %s deleted", entry)) -} - -// planSteps is a helper function to plan all steps -// in the build for execution. This creates the steps -// for the build in the configured backend. -// -// nolint: funlen,lll // ignore function length and long line length -func planSteps(database database.Service, p *pipeline.Build, b *library.Build) ([]*library.Step, error) { - // variable to store planned steps - steps := []*library.Step{} - - // iterate through all pipeline stages - for _, stage := range p.Stages { - // iterate through all steps for each pipeline stage - for _, step := range stage.Steps { - // create the step object - s := new(library.Step) - s.SetBuildID(b.GetID()) - s.SetRepoID(b.GetRepoID()) - s.SetNumber(step.Number) - s.SetName(step.Name) - s.SetImage(step.Image) - s.SetStage(stage.Name) - s.SetStatus(constants.StatusPending) - s.SetCreated(time.Now().UTC().Unix()) - - // send API call to create the step - err := database.CreateStep(s) - if err != nil { - return steps, fmt.Errorf("unable to create step %s: %w", s.GetName(), err) - } - - // send API call to capture the created step - s, err = database.GetStep(s.GetNumber(), b) - if err != nil { - return steps, fmt.Errorf("unable to get step %s: %w", s.GetName(), err) - } - - // populate environment variables from step library - // - // https://pkg.go.dev/github.com/go-vela/types/library#step.Environment - err = step.MergeEnv(s.Environment()) - if err != nil { - return steps, err - } - - // create the log object - l := new(library.Log) - l.SetStepID(s.GetID()) - l.SetBuildID(b.GetID()) - l.SetRepoID(b.GetRepoID()) - l.SetData([]byte{}) - - // send API call to create the step logs - err = database.CreateLog(l) - if err != nil { - return nil, fmt.Errorf("unable to create logs for step %s: %w", s.GetName(), err) - } - - steps = append(steps, s) - } - } - - // iterate through all pipeline steps - for _, step := range p.Steps { - // create the step object - s := new(library.Step) - s.SetBuildID(b.GetID()) - s.SetRepoID(b.GetRepoID()) - s.SetNumber(step.Number) - s.SetName(step.Name) - s.SetImage(step.Image) - s.SetStatus(constants.StatusPending) - s.SetCreated(time.Now().UTC().Unix()) - - // send API call to create the step - err := database.CreateStep(s) - if err != nil { - return steps, fmt.Errorf("unable to create step %s: %w", s.GetName(), err) - } - - // send API call to capture the created step - s, err = database.GetStep(s.GetNumber(), b) - if err != nil { - return steps, fmt.Errorf("unable to get step %s: %w", s.GetName(), err) - } - - // populate environment variables from step library - // - // https://pkg.go.dev/github.com/go-vela/types/library#step.Environment - err = step.MergeEnv(s.Environment()) - if err != nil { - return steps, err - } - - // create the log object - l := new(library.Log) - l.SetStepID(s.GetID()) - l.SetBuildID(b.GetID()) - l.SetRepoID(b.GetRepoID()) - l.SetData([]byte{}) - - // send API call to create the step logs - err = database.CreateLog(l) - if err != nil { - return steps, fmt.Errorf("unable to create logs for step %s: %w", s.GetName(), err) - } - - steps = append(steps, s) - } - - return steps, nil -} diff --git a/api/step/create.go b/api/step/create.go new file mode 100644 index 000000000..58e71a99e --- /dev/null +++ b/api/step/create.go @@ -0,0 +1,125 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/steps steps CreateStep +// +// Create a step for a build +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: body +// name: body +// description: Payload containing the step to create +// required: true +// schema: +// "$ref": "#/definitions/Step" +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: Successfully created the step +// schema: +// "$ref": "#/definitions/Step" +// '400': +// description: Unable to create the step +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the step +// schema: +// "$ref": "#/definitions/Error" + +// CreateStep represents the API handler to create +// a step for a build in the configured backend. +func CreateStep(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("creating new step for build %s", entry) + + // capture body from API request + input := new(library.Step) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for new step for build %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update fields in step object + input.SetRepoID(r.GetID()) + input.SetBuildID(b.GetID()) + + if len(input.GetStatus()) == 0 { + input.SetStatus(constants.StatusPending) + } + + if input.GetCreated() == 0 { + input.SetCreated(time.Now().UTC().Unix()) + } + + // send API call to create the step + s, err := database.FromContext(c).CreateStep(input) + if err != nil { + retErr := fmt.Errorf("unable to create step for build %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusCreated, s) +} diff --git a/api/step/delete.go b/api/step/delete.go new file mode 100644 index 000000000..1a43c30b3 --- /dev/null +++ b/api/step/delete.go @@ -0,0 +1,96 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/step" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step} steps DeleteStep +// +// Delete a step for a build +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: step +// description: Step number +// required: true +// type: integer +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted the step +// schema: +// type: string +// '500': +// description: Successfully deleted the step +// schema: +// "$ref": "#/definitions/Error" + +// DeleteStep represents the API handler to remove +// a step for a build from the configured backend. +func DeleteStep(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := step.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "step": s.GetNumber(), + "user": u.GetName(), + }).Infof("deleting step %s", entry) + + // send API call to remove the step + err := database.FromContext(c).DeleteStep(s) + if err != nil { + retErr := fmt.Errorf("unable to delete step %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("step %s deleted", entry)) +} diff --git a/api/step/doc.go b/api/step/doc.go new file mode 100644 index 000000000..fad7dce79 --- /dev/null +++ b/api/step/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package step provides the step handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/step" +package step diff --git a/api/step/get.go b/api/step/get.go new file mode 100644 index 000000000..cf55c1bb5 --- /dev/null +++ b/api/step/get.go @@ -0,0 +1,77 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/step" + "github.com/go-vela/server/router/middleware/user" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step} steps GetStep +// +// Retrieve a step for a build +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: step +// description: Step number +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the step +// schema: +// "$ref": "#/definitions/Step" + +// GetStep represents the API handler to capture a +// step for a build from the configured backend. +func GetStep(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := step.Retrieve(c) + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "step": s.GetNumber(), + "user": u.GetName(), + }).Infof("reading step %s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + c.JSON(http.StatusOK, s) +} diff --git a/api/step/list.go b/api/step/list.go new file mode 100644 index 000000000..5d76fb33c --- /dev/null +++ b/api/step/list.go @@ -0,0 +1,146 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/repos/{org}/{repo}/builds/{build}/steps steps ListSteps +// +// Retrieve a list of steps for a build +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the list of steps +// schema: +// type: array +// items: +// "$ref": "#/definitions/Step" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve the list of steps +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the list of steps +// schema: +// "$ref": "#/definitions/Error" + +// ListSteps represents the API handler to capture a list +// of steps for a build from the configured backend. +func ListSteps(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "user": u.GetName(), + }).Infof("listing steps for build %s", entry) + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + retErr := fmt.Errorf("unable to convert page query parameter for build %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for build %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // send API call to capture the list of steps for the build + s, t, err := database.FromContext(c).ListStepsForBuild(b, map[string]interface{}{}, page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to list steps for build %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, s) +} diff --git a/api/step/plan.go b/api/step/plan.go new file mode 100644 index 000000000..daf795284 --- /dev/null +++ b/api/step/plan.go @@ -0,0 +1,91 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "fmt" + "time" + + "github.com/go-vela/server/database" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" +) + +// PlanSteps is a helper function to plan all steps +// in the build for execution. This creates the steps +// for the build in the configured backend. +func PlanSteps(database database.Interface, p *pipeline.Build, b *library.Build) ([]*library.Step, error) { + // variable to store planned steps + steps := []*library.Step{} + + // iterate through all pipeline stages + for _, stage := range p.Stages { + // iterate through all steps for each pipeline stage + for _, step := range stage.Steps { + // create the step object + s, err := planStep(database, b, step, stage.Name) + if err != nil { + return steps, err + } + + steps = append(steps, s) + } + } + + // iterate through all pipeline steps + for _, step := range p.Steps { + s, err := planStep(database, b, step, "") + if err != nil { + return steps, err + } + + steps = append(steps, s) + } + + return steps, nil +} + +func planStep(database database.Interface, b *library.Build, c *pipeline.Container, stage string) (*library.Step, error) { + // create the step object + s := new(library.Step) + s.SetBuildID(b.GetID()) + s.SetRepoID(b.GetRepoID()) + s.SetNumber(c.Number) + s.SetName(c.Name) + s.SetImage(c.Image) + s.SetStage(stage) + s.SetStatus(constants.StatusPending) + s.SetCreated(time.Now().UTC().Unix()) + + // send API call to create the step + s, err := database.CreateStep(s) + if err != nil { + return nil, fmt.Errorf("unable to create step %s: %w", s.GetName(), err) + } + + // populate environment variables from step library + // + // https://pkg.go.dev/github.com/go-vela/types/library#step.Environment + err = c.MergeEnv(s.Environment()) + if err != nil { + return nil, err + } + + // create the log object + l := new(library.Log) + l.SetStepID(s.GetID()) + l.SetBuildID(b.GetID()) + l.SetRepoID(b.GetRepoID()) + l.SetData([]byte{}) + + // send API call to create the step logs + err = database.CreateLog(l) + if err != nil { + return nil, fmt.Errorf("unable to create logs for step %s: %w", s.GetName(), err) + } + + return s, nil +} diff --git a/api/step/update.go b/api/step/update.go new file mode 100644 index 000000000..d0563d98d --- /dev/null +++ b/api/step/update.go @@ -0,0 +1,160 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/step" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step} steps UpdateStep +// +// Update a step for a build +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: org +// description: Name of the org +// required: true +// type: string +// - in: path +// name: repo +// description: Name of the repo +// required: true +// type: string +// - in: path +// name: build +// description: Build number +// required: true +// type: integer +// - in: path +// name: step +// description: Step number +// required: true +// type: integer +// - in: body +// name: body +// description: Payload containing the step to update +// required: true +// schema: +// "$ref": "#/definitions/Step" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the step +// schema: +// "$ref": "#/definitions/Step" +// '400': +// description: Unable to update the step +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update the step +// schema: +// "$ref": "#/definitions/Error" + +// UpdateStep represents the API handler to update +// a step for a build in the configured backend. +func UpdateStep(c *gin.Context) { + // capture middleware values + b := build.Retrieve(c) + o := org.Retrieve(c) + r := repo.Retrieve(c) + s := step.Retrieve(c) + u := user.Retrieve(c) + + entry := fmt.Sprintf("%s/%d/%d", r.GetFullName(), b.GetNumber(), s.GetNumber()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "org": o, + "repo": r.GetName(), + "step": s.GetNumber(), + "user": u.GetName(), + }).Infof("updating step %s", entry) + + // capture body from API request + input := new(library.Step) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for step %s: %w", entry, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update step fields if provided + if len(input.GetStatus()) > 0 { + // update status if set + s.SetStatus(input.GetStatus()) + } + + if len(input.GetError()) > 0 { + // update error if set + s.SetError(input.GetError()) + } + + if input.GetExitCode() > 0 { + // update exit_code if set + s.SetExitCode(input.GetExitCode()) + } + + if input.GetStarted() > 0 { + // update started if set + s.SetStarted(input.GetStarted()) + } + + if input.GetFinished() > 0 { + // update finished if set + s.SetFinished(input.GetFinished()) + } + + if len(input.GetHost()) > 0 { + // update host if set + s.SetHost(input.GetHost()) + } + + if len(input.GetRuntime()) > 0 { + // update runtime if set + s.SetRuntime(input.GetRuntime()) + } + + if len(input.GetDistribution()) > 0 { + // update distribution if set + s.SetDistribution(input.GetDistribution()) + } + + // send API call to update the step + s, err = database.FromContext(c).UpdateStep(s) + if err != nil { + retErr := fmt.Errorf("unable to update step %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, s) +} diff --git a/api/stream.go b/api/stream.go deleted file mode 100644 index 22cf1e5b7..000000000 --- a/api/stream.go +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "bufio" - "bytes" - "fmt" - "net/http" - "time" - - "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/router/middleware/user" - - "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/build" - "github.com/go-vela/server/router/middleware/repo" - "github.com/go-vela/server/router/middleware/service" - "github.com/go-vela/server/router/middleware/step" - "github.com/go-vela/server/util" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -const logUpdateInterval = 1 * time.Second - -// nolint:lll // due to api endpoint parameters -// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/service/{service}/stream stream PostServiceStream -// -// Stream the logs for a service -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: service -// description: Service number -// required: true -// type: integer -// - in: body -// name: body -// description: Payload containing logs -// required: true -// schema: -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '204': -// description: Successfully received logs -// '400': -// description: Unable to stream the logs -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to stream the logs -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to stream the logs -// schema: -// "$ref": "#/definitions/Error" - -// PostServiceStream represents the API handler that -// streams service logs to the database. -// nolint: dupl // separate service/step functions for consistency with API -func PostServiceStream(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := service.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logger := logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "service": s.GetNumber(), - "user": u.GetName(), - }) - - logger.Infof("streaming logs for service %s/%d", entry, s.GetNumber()) - - // create new buffer for uploading logs - logs := new(bytes.Buffer) - // create new channel for processing logs - done := make(chan bool) - // defer closing channel to stop processing logs - defer close(done) - - // send API call to capture the service logs - _log, err := database.FromContext(c).GetServiceLog(s.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to get logs for service %s/%d: %w", entry, s.GetNumber(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - go func() { - logger.Debugf("polling request body buffer for service %s/%d", entry, s.GetNumber()) - - // spawn "infinite" loop that will upload logs - // from the buffer until the channel is closed - for { - // sleep before attempting to upload logs - time.Sleep(logUpdateInterval) - - // create a non-blocking select to check if the channel is closed - select { - // after repo timeout of idle (no response) end the stream - // - // this is a safety mechanism - case <-time.After(time.Duration(r.GetTimeout()) * time.Minute): - logger.Tracef("repo timeout of %d exceeded", r.GetTimeout()) - - return - // channel is closed - case <-done: - logger.Trace("channel closed for polling container logs") - - // return out of the go routine - return - // channel is not closed - default: - // get the current size of log data - currBytesSize := len(_log.GetData()) - - // update the existing log with the new bytes if there is new data to add - if len(logs.Bytes()) > currBytesSize { - // https://pkg.go.dev/github.com/go-vela/types/library?tab=doc#Log.SetData - _log.SetData(logs.Bytes()) - - // update the log in the database - err = database.FromContext(c).UpdateLog(_log) - if err != nil { - retErr := fmt.Errorf("unable to update logs for service %s/%d: %w", entry, s.GetNumber(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - } - } - } - }() - - logger.Debugf("scanning request body for service %s/%d", entry, s.GetNumber()) - - scanner := bufio.NewScanner(c.Request.Body) - for scanner.Scan() { - // write all the logs from the scanner - logs.Write(append(scanner.Bytes(), []byte("\n")...)) - } - - c.JSON(http.StatusNoContent, nil) -} - -// nolint:lll // due to api endpoint parameters -// swagger:operation POST /api/v1/repos/{org}/{repo}/builds/{build}/steps/{step}/stream stream PostStepStream -// -// Stream the logs for a step -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: org -// description: Name of the org -// required: true -// type: string -// - in: path -// name: repo -// description: Name of the repo -// required: true -// type: string -// - in: path -// name: build -// description: Build number -// required: true -// type: integer -// - in: path -// name: step -// description: Step number -// required: true -// type: integer -// - in: body -// name: body -// description: Payload containing logs -// required: true -// schema: -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '204': -// description: Successfully received logs -// '400': -// description: Unable to stream the logs -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to stream the logs -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to stream the logs -// schema: -// "$ref": "#/definitions/Error" - -// PostStepStream represents the API handler that -// streams service logs to the database. -// nolint: dupl // separate service/step functions for consistency with API -func PostStepStream(c *gin.Context) { - // capture middleware values - b := build.Retrieve(c) - o := org.Retrieve(c) - r := repo.Retrieve(c) - s := step.Retrieve(c) - u := user.Retrieve(c) - - entry := fmt.Sprintf("%s/%d", r.GetFullName(), b.GetNumber()) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logger := logrus.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "org": o, - "repo": r.GetName(), - "step": s.GetNumber(), - "user": u.GetName(), - }) - - logger.Infof("streaming logs for step %s/%d", entry, s.GetNumber()) - - // create new buffer for uploading logs - logs := new(bytes.Buffer) - // create new channel for processing logs - done := make(chan bool) - // defer closing channel to stop processing logs - defer close(done) - - // send API call to capture the step logs - _log, err := database.FromContext(c).GetStepLog(s.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to get logs for step %s/%d: %w", entry, s.GetNumber(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - go func() { - logger.Debugf("polling request body buffer for step %s/%d", entry, s.GetNumber()) - - // spawn "infinite" loop that will upload logs - // from the buffer until the channel is closed - for { - // sleep before attempting to upload logs - time.Sleep(logUpdateInterval) - - // create a non-blocking select to check if the channel is closed - select { - // after repo timeout of idle (no response) end the stream - // - // this is a safety mechanism - case <-time.After(time.Duration(r.GetTimeout()) * time.Minute): - logger.Tracef("repo timeout of %d exceeded", r.GetTimeout()) - - return - // channel is closed - case <-done: - logger.Trace("channel closed for polling container logs") - - // return out of the go routine - return - // channel is not closed - default: - // get the current size of log data - currBytesSize := len(_log.GetData()) - - // update the existing log with the new bytes if there is new data to add - if len(logs.Bytes()) > currBytesSize { - // https://pkg.go.dev/github.com/go-vela/types/library?tab=doc#Log.SetData - _log.SetData(logs.Bytes()) - - // update the log in the database - err = database.FromContext(c).UpdateLog(_log) - if err != nil { - retErr := fmt.Errorf("unable to update logs for step %s/%d: %w", entry, s.GetNumber(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - } - } - } - }() - - logger.Debugf("scanning request body for step %s/%d", entry, s.GetNumber()) - - scanner := bufio.NewScanner(c.Request.Body) - for scanner.Scan() { - // write all the logs from the scanner - logs.Write(append(scanner.Bytes(), []byte("\n")...)) - } - - c.JSON(http.StatusNoContent, nil) -} diff --git a/api/user.go b/api/user.go deleted file mode 100644 index d9aaba8a0..000000000 --- a/api/user.go +++ /dev/null @@ -1,792 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "encoding/base64" - "fmt" - "net/http" - "strconv" - - "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/token" - "github.com/go-vela/server/router/middleware/user" - "github.com/go-vela/server/scm" - "github.com/go-vela/server/util" - - "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" - "github.com/google/uuid" - "github.com/sirupsen/logrus" -) - -// swagger:operation POST /api/v1/users users CreateUser -// -// Create a user for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: body -// name: body -// description: Payload containing the user to create -// required: true -// schema: -// "$ref": "#/definitions/User" -// security: -// - ApiKeyAuth: [] -// responses: -// '201': -// description: Successfully created the user -// schema: -// "$ref": "#/definitions/User" -// '400': -// description: Unable to create the user -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to create the user -// schema: -// "$ref": "#/definitions/Error" - -// CreateUser represents the API handler to create -// a user in the configured backend. -func CreateUser(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - - // capture body from API request - input := new(library.User) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for new user: %w", err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Infof("creating new user %s", input.GetName()) - - // send API call to create the user - err = database.FromContext(c).CreateUser(input) - if err != nil { - retErr := fmt.Errorf("unable to create user: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the created user - user, _ := database.FromContext(c).GetUserName(input.GetName()) - - c.JSON(http.StatusCreated, user) -} - -// swagger:operation GET /api/v1/users users GetUsers -// -// Retrieve a list of users for the configured backend -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// parameters: -// - in: query -// name: page -// description: The page of results to retrieve -// type: integer -// default: 1 -// - in: query -// name: per_page -// description: How many results per page to return -// type: integer -// maximum: 100 -// default: 10 -// responses: -// '200': -// description: Successfully retrieved the list of users -// schema: -// type: array -// items: -// "$ref": "#/definitions/User" -// headers: -// X-Total-Count: -// description: Total number of results -// type: integer -// Link: -// description: see https://tools.ietf.org/html/rfc5988 -// type: string -// '400': -// description: Unable to retrieve the list of users -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to retrieve the list of users -// schema: -// "$ref": "#/definitions/Error" - -// GetUsers represents the API handler to capture a list -// of users from the configured backend. -func GetUsers(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Info("reading lite users") - - // capture page query parameter if present - page, err := strconv.Atoi(c.DefaultQuery("page", "1")) - if err != nil { - retErr := fmt.Errorf("unable to convert page query parameter for users: %w", err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // capture per_page query parameter if present - perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) - if err != nil { - retErr := fmt.Errorf("unable to convert per_page query parameter for users: %w", err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // ensure per_page isn't above or below allowed values - // - // nolint: gomnd // ignore magic number - perPage = util.MaxInt(1, util.MinInt(100, perPage)) - - // send API call to capture the total number of users - t, err := database.FromContext(c).GetUserCount() - if err != nil { - retErr := fmt.Errorf("unable to get users count: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the list of users - users, err := database.FromContext(c).GetUserLiteList(page, perPage) - if err != nil { - retErr := fmt.Errorf("unable to get users: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // create pagination object - pagination := Pagination{ - Page: page, - PerPage: perPage, - Total: t, - } - // set pagination headers - pagination.SetHeaderLink(c) - - c.JSON(http.StatusOK, users) -} - -// swagger:operation GET /api/v1/user users GetCurrentUser -// -// Retrieve the current authenticated user from the configured backend -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the current user -// schema: -// "$ref": "#/definitions/User" - -// GetCurrentUser represents the API handler to capture the -// currently authenticated user from the configured backend. -func GetCurrentUser(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Infof("reading current user %s", u.GetName()) - - c.JSON(http.StatusOK, u) -} - -// swagger:operation PUT /api/v1/user users UpdateCurrentUser -// -// Update the current authenticated user in the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: body -// name: body -// description: Payload containing the user to update -// required: true -// schema: -// "$ref": "#/definitions/User" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully updated the current user -// schema: -// "$ref": "#/definitions/User" -// '400': -// description: Unable to update the current user -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to update the current user -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to update the current user -// schema: -// "$ref": "#/definitions/Error" - -// UpdateCurrentUser represents the API handler to capture and -// update the currently authenticated user from the configured backend. -func UpdateCurrentUser(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Infof("updating current user %s", u.GetName()) - - // capture body from API request - input := new(library.User) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update user fields if provided - if input.Favorites != nil { - // update favorites if set - u.SetFavorites(input.GetFavorites()) - } - - // send API call to update the user - err = database.FromContext(c).UpdateUser(u) - if err != nil { - retErr := fmt.Errorf("unable to update user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated user - u, err = database.FromContext(c).GetUserName(u.GetName()) - if err != nil { - retErr := fmt.Errorf("unable to get updated user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - c.JSON(http.StatusOK, u) -} - -// swagger:operation GET /api/v1/users/{user} users GetUser -// -// Retrieve a user for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: user -// description: Name of the user -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the user -// schema: -// "$ref": "#/definitions/User" -// '404': -// description: Unable to retrieve the user -// schema: -// "$ref": "#/definitions/Error" - -// GetUser represents the API handler to capture a -// user from the configured backend. -func GetUser(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - user := c.Param("user") - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Infof("reading user %s", user) - - // send API call to capture the user - u, err := database.FromContext(c).GetUserName(user) - if err != nil { - retErr := fmt.Errorf("unable to get user %s: %w", user, err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - c.JSON(http.StatusOK, u) -} - -// swagger:operation GET /api/v1/user/source/repos users GetUserSourceRepos -// -// Retrieve a list of repos for the current authenticated user -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved a list of repos for the current user -// schema: -// "$ref": "#/definitions/Repo" -// '404': -// description: Unable to retrieve a list of repos for the current user -// schema: -// "$ref": "#/definitions/Error" - -// GetUserSourceRepos represents the API handler to capture -// the list of repos for a user from the configured backend. -func GetUserSourceRepos(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Infof("reading available SCM repos for user %s", u.GetName()) - - // variables to capture requested data - dbRepos := []*library.Repo{} - output := make(map[string][]library.Repo) - - // send API call to capture the list of repos for the user - srcRepos, err := scm.FromContext(c).ListUserRepos(u) - if err != nil { - retErr := fmt.Errorf("unable to get SCM repos for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - // create a map - // TODO: clean this up - for _, srepo := range srcRepos { - // local variables to avoid bad memory address de-referencing - // initialize active to false - org := srepo.Org - name := srepo.Name - active := false - - // library struct to omit optional fields - repo := library.Repo{ - Org: org, - Name: name, - Active: &active, - } - output[srepo.GetOrg()] = append(output[srepo.GetOrg()], repo) - } - - for org := range output { - // capture source repos from the database backend, grouped by org - page := 1 - filters := map[string]string{} - for page > 0 { - // send API call to capture the list of repos for the org - // nolint: gomnd // ignore magic number - dbReposPart, err := database.FromContext(c).GetOrgRepoList(org, filters, page, 100) - if err != nil { - retErr := fmt.Errorf("unable to get repos for org %s: %w", org, err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - // add repos to list of database org repos - dbRepos = append(dbRepos, dbReposPart...) - - // assume no more pages exist if under 100 results are returned - // - // nolint: gomnd // ignore magic number - if len(dbReposPart) < 100 { - page = 0 - } else { - page++ - } - } - - // apply org repos active status to output map - for _, dbRepo := range dbRepos { - if orgRepos, ok := output[dbRepo.GetOrg()]; ok { - for i := range orgRepos { - if orgRepos[i].GetName() == dbRepo.GetName() { - active := dbRepo.GetActive() - (&orgRepos[i]).Active = &active - } - } - } - } - } - - c.JSON(http.StatusOK, output) -} - -// swagger:operation PUT /api/v1/users/{user} users UpdateUser -// -// Update a user for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: user -// description: Name of the user -// required: true -// type: string -// - in: body -// name: body -// description: Payload containing the user to update -// required: true -// schema: -// "$ref": "#/definitions/User" -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully updated the user -// schema: -// "$ref": "#/definitions/User" -// '400': -// description: Unable to update the user -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to update the user -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to update the user -// schema: -// "$ref": "#/definitions/Error" - -// UpdateUser represents the API handler to update -// a user in the configured backend. -func UpdateUser(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - user := c.Param("user") - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Infof("updating user %s", user) - - // capture body from API request - input := new(library.User) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for user %s: %w", user, err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // send API call to capture the user - u, err = database.FromContext(c).GetUserName(user) - if err != nil { - retErr := fmt.Errorf("unable to get user %s: %w", user, err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - // update user fields if provided - if input.GetActive() { - // update active if set to true - u.SetActive(input.GetActive()) - } - - if input.GetAdmin() { - // update admin if set to true - u.SetAdmin(input.GetAdmin()) - } - - if input.Favorites != nil { - // update favorites if set - u.SetFavorites(input.GetFavorites()) - } - - // send API call to update the user - err = database.FromContext(c).UpdateUser(u) - if err != nil { - retErr := fmt.Errorf("unable to update user %s: %w", user, err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated user - u, _ = database.FromContext(c).GetUserName(user) - - c.JSON(http.StatusOK, u) -} - -// swagger:operation DELETE /api/v1/users/{user} users DeleteUser -// -// Delete a user for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: user -// description: Name of the user -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully deleted of user -// schema: -// type: string -// '404': -// description: Unable to delete user -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to delete user -// schema: -// "$ref": "#/definitions/Error" - -// DeleteUser represents the API handler to remove -// a user from the configured backend. -func DeleteUser(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - user := c.Param("user") - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Infof("deleting user %s", user) - - // send API call to capture the user - u, err := database.FromContext(c).GetUserName(user) - if err != nil { - retErr := fmt.Errorf("unable to get user %s: %w", user, err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - // send API call to remove the user - err = database.FromContext(c).DeleteUser(u.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to delete user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, fmt.Sprintf("user %s deleted", u.GetName())) -} - -// swagger:operation POST /api/v1/user/token users CreateToken -// -// Create a token for the current authenticated user -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully created a token for the current user -// schema: -// "$ref": "#/definitions/Login" -// '503': -// description: Unable to create a token for the current user -// schema: -// "$ref": "#/definitions/Error" - -// CreateToken represents the API handler to create -// a user token in the configured backend. -func CreateToken(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Infof("composing token for user %s", u.GetName()) - - // compose JWT token for user - rt, at, err := token.Compose(c, u) - if err != nil { - retErr := fmt.Errorf("unable to compose token for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - u.SetRefreshToken(rt) - - // send API call to update the user - err = database.FromContext(c).UpdateUser(u) - if err != nil { - retErr := fmt.Errorf("unable to update user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - c.JSON(http.StatusOK, library.Login{Token: &at}) -} - -// swagger:operation DELETE /api/v1/user/token users DeleteToken -// -// Delete a token for the current authenticated user -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully delete a token for the current user -// schema: -// type: string -// '500': -// description: Unable to delete a token for the current user -// schema: -// "$ref": "#/definitions/Error" - -// DeleteToken represents the API handler to revoke -// and recreate a user token in the configured backend. -func DeleteToken(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Infof("revoking token for user %s", u.GetName()) - - // create unique id for the user - uid, err := uuid.NewRandom() - if err != nil { - retErr := fmt.Errorf("unable to create UID for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - u.SetHash( - base64.StdEncoding.EncodeToString( - []byte(uid.String()), - ), - ) - - // compose JWT token for user - rt, at, err := token.Compose(c, u) - if err != nil { - retErr := fmt.Errorf("unable to compose token for user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - u.SetRefreshToken(rt) - - // send API call to update the user - err = database.FromContext(c).UpdateUser(u) - if err != nil { - retErr := fmt.Errorf("unable to update user %s: %w", u.GetName(), err) - - util.HandleError(c, http.StatusServiceUnavailable, retErr) - - return - } - - c.JSON(http.StatusOK, library.Login{Token: &at}) -} diff --git a/api/user/create.go b/api/user/create.go new file mode 100644 index 000000000..d6348810e --- /dev/null +++ b/api/user/create.go @@ -0,0 +1,88 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/users users CreateUser +// +// Create a user for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: Payload containing the user to create +// required: true +// schema: +// "$ref": "#/definitions/User" +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: Successfully created the user +// schema: +// "$ref": "#/definitions/User" +// '400': +// description: Unable to create the user +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the user +// schema: +// "$ref": "#/definitions/Error" + +// CreateUser represents the API handler to create +// a user in the configured backend. +func CreateUser(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + + // capture body from API request + input := new(library.User) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for new user: %w", err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Infof("creating new user %s", input.GetName()) + + // send API call to create the user + err = database.FromContext(c).CreateUser(input) + if err != nil { + retErr := fmt.Errorf("unable to create user: %w", err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // send API call to capture the created user + user, _ := database.FromContext(c).GetUserForName(input.GetName()) + + c.JSON(http.StatusCreated, user) +} diff --git a/api/user/create_token.go b/api/user/create_token.go new file mode 100644 index 000000000..dee7044cb --- /dev/null +++ b/api/user/create_token.go @@ -0,0 +1,78 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with delete token +package user + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/user/token users CreateToken +// +// Create a token for the current authenticated user +// +// --- +// produces: +// - application/json +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully created a token for the current user +// schema: +// "$ref": "#/definitions/Token" +// '503': +// description: Unable to create a token for the current user +// schema: +// "$ref": "#/definitions/Error" + +// CreateToken represents the API handler to create +// a user token in the configured backend. +func CreateToken(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Infof("composing token for user %s", u.GetName()) + + tm := c.MustGet("token-manager").(*token.Manager) + + // compose JWT token for user + rt, at, err := tm.Compose(c, u) + if err != nil { + retErr := fmt.Errorf("unable to compose token for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + + return + } + + u.SetRefreshToken(rt) + + // send API call to update the user + err = database.FromContext(c).UpdateUser(u) + if err != nil { + retErr := fmt.Errorf("unable to update user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + + return + } + + c.JSON(http.StatusOK, library.Token{Token: &at}) +} diff --git a/api/user/delete.go b/api/user/delete.go new file mode 100644 index 000000000..9e0651e7e --- /dev/null +++ b/api/user/delete.go @@ -0,0 +1,82 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/users/{user} users DeleteUser +// +// Delete a user for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: user +// description: Name of the user +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted of user +// schema: +// type: string +// '404': +// description: Unable to delete user +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to delete user +// schema: +// "$ref": "#/definitions/Error" + +// DeleteUser represents the API handler to remove +// a user from the configured backend. +func DeleteUser(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + user := util.PathParameter(c, "user") + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Infof("deleting user %s", user) + + // send API call to capture the user + u, err := database.FromContext(c).GetUserForName(user) + if err != nil { + retErr := fmt.Errorf("unable to get user %s: %w", user, err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // send API call to remove the user + err = database.FromContext(c).DeleteUser(u) + if err != nil { + retErr := fmt.Errorf("unable to delete user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("user %s deleted", u.GetName())) +} diff --git a/api/user/delete_token.go b/api/user/delete_token.go new file mode 100644 index 000000000..22fa9724d --- /dev/null +++ b/api/user/delete_token.go @@ -0,0 +1,78 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with create token +package user + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/user/token users DeleteToken +// +// Delete a token for the current authenticated user +// +// --- +// produces: +// - application/json +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully delete a token for the current user +// schema: +// type: string +// '500': +// description: Unable to delete a token for the current user +// schema: +// "$ref": "#/definitions/Error" + +// DeleteToken represents the API handler to revoke +// and recreate a user token in the configured backend. +func DeleteToken(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Infof("revoking token for user %s", u.GetName()) + + tm := c.MustGet("token-manager").(*token.Manager) + + // compose JWT token for user + rt, at, err := tm.Compose(c, u) + if err != nil { + retErr := fmt.Errorf("unable to compose token for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + + return + } + + u.SetRefreshToken(rt) + + // send API call to update the user + err = database.FromContext(c).UpdateUser(u) + if err != nil { + retErr := fmt.Errorf("unable to update user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusServiceUnavailable, retErr) + + return + } + + c.JSON(http.StatusOK, library.Token{Token: &at}) +} diff --git a/api/user/doc.go b/api/user/doc.go new file mode 100644 index 000000000..7f1ce6bc5 --- /dev/null +++ b/api/user/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package user provides the user handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/user" +package user diff --git a/api/user/get.go b/api/user/get.go new file mode 100644 index 000000000..9f0b723d3 --- /dev/null +++ b/api/user/get.go @@ -0,0 +1,68 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/users/{user} users GetUser +// +// Retrieve a user for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: user +// description: Name of the user +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the user +// schema: +// "$ref": "#/definitions/User" +// '404': +// description: Unable to retrieve the user +// schema: +// "$ref": "#/definitions/Error" + +// GetUser represents the API handler to capture a +// user from the configured backend. +func GetUser(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + user := util.PathParameter(c, "user") + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Infof("reading user %s", user) + + // send API call to capture the user + u, err := database.FromContext(c).GetUserForName(user) + if err != nil { + retErr := fmt.Errorf("unable to get user %s: %w", user, err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + c.JSON(http.StatusOK, u) +} diff --git a/api/user/get_current.go b/api/user/get_current.go new file mode 100644 index 000000000..b4b1ef4c3 --- /dev/null +++ b/api/user/get_current.go @@ -0,0 +1,44 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/router/middleware/user" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/user users GetCurrentUser +// +// Retrieve the current authenticated user from the configured backend +// +// --- +// produces: +// - application/json +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the current user +// schema: +// "$ref": "#/definitions/User" + +// GetCurrentUser represents the API handler to capture the +// currently authenticated user from the configured backend. +func GetCurrentUser(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Infof("reading current user %s", u.GetName()) + + c.JSON(http.StatusOK, u) +} diff --git a/api/user/get_source.go b/api/user/get_source.go new file mode 100644 index 000000000..9892f319e --- /dev/null +++ b/api/user/get_source.go @@ -0,0 +1,126 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/user/source/repos users GetSourceRepos +// +// Retrieve a list of repos for the current authenticated user +// +// --- +// produces: +// - application/json +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved a list of repos for the current user +// schema: +// "$ref": "#/definitions/Repo" +// '404': +// description: Unable to retrieve a list of repos for the current user +// schema: +// "$ref": "#/definitions/Error" + +// GetSourceRepos represents the API handler to capture +// the list of repos for a user from the configured backend. +func GetSourceRepos(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + ctx := c.Request.Context() + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Infof("reading available SCM repos for user %s", u.GetName()) + + // variables to capture requested data + dbRepos := []*library.Repo{} + output := make(map[string][]library.Repo) + + // send API call to capture the list of repos for the user + srcRepos, err := scm.FromContext(c).ListUserRepos(u) + if err != nil { + retErr := fmt.Errorf("unable to get SCM repos for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // create a map + // TODO: clean this up + for _, srepo := range srcRepos { + // local variables to avoid bad memory address de-referencing + // initialize active to false + org := srepo.Org + name := srepo.Name + active := false + + // library struct to omit optional fields + repo := library.Repo{ + Org: org, + Name: name, + Active: &active, + } + output[srepo.GetOrg()] = append(output[srepo.GetOrg()], repo) + } + + for org := range output { + // capture source repos from the database backend, grouped by org + page := 1 + filters := map[string]interface{}{} + + for page > 0 { + // send API call to capture the list of repos for the org + dbReposPart, _, err := database.FromContext(c).ListReposForOrg(ctx, org, "name", filters, page, 100) + if err != nil { + retErr := fmt.Errorf("unable to get repos for org %s: %w", org, err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // add repos to list of database org repos + dbRepos = append(dbRepos, dbReposPart...) + + // assume no more pages exist if under 100 results are returned + if len(dbReposPart) < 100 { + page = 0 + } else { + page++ + } + } + + // apply org repos active status to output map + for _, dbRepo := range dbRepos { + if orgRepos, ok := output[dbRepo.GetOrg()]; ok { + for i := range orgRepos { + if orgRepos[i].GetName() == dbRepo.GetName() { + active := dbRepo.GetActive() + (&orgRepos[i]).Active = &active + } + } + } + } + } + + c.JSON(http.StatusOK, output) +} diff --git a/api/user/list.go b/api/user/list.go new file mode 100644 index 000000000..1f879bfef --- /dev/null +++ b/api/user/list.go @@ -0,0 +1,120 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/users users ListUsers +// +// Retrieve a list of users for the configured backend +// +// --- +// produces: +// - application/json +// security: +// - ApiKeyAuth: [] +// parameters: +// - in: query +// name: page +// description: The page of results to retrieve +// type: integer +// default: 1 +// - in: query +// name: per_page +// description: How many results per page to return +// type: integer +// maximum: 100 +// default: 10 +// responses: +// '200': +// description: Successfully retrieved the list of users +// schema: +// type: array +// items: +// "$ref": "#/definitions/User" +// headers: +// X-Total-Count: +// description: Total number of results +// type: integer +// Link: +// description: see https://tools.ietf.org/html/rfc5988 +// type: string +// '400': +// description: Unable to retrieve the list of users +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to retrieve the list of users +// schema: +// "$ref": "#/definitions/Error" + +// ListUsers represents the API handler to capture a list +// of users from the configured backend. +func ListUsers(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Info("reading lite users") + + // capture page query parameter if present + page, err := strconv.Atoi(c.DefaultQuery("page", "1")) + if err != nil { + retErr := fmt.Errorf("unable to convert page query parameter for users: %w", err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // capture per_page query parameter if present + perPage, err := strconv.Atoi(c.DefaultQuery("per_page", "10")) + if err != nil { + retErr := fmt.Errorf("unable to convert per_page query parameter for users: %w", err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // ensure per_page isn't above or below allowed values + perPage = util.MaxInt(1, util.MinInt(100, perPage)) + + // send API call to capture the list of users + users, t, err := database.FromContext(c).ListLiteUsers(page, perPage) + if err != nil { + retErr := fmt.Errorf("unable to get users: %w", err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // create pagination object + pagination := api.Pagination{ + Page: page, + PerPage: perPage, + Total: t, + } + // set pagination headers + pagination.SetHeaderLink(c) + + c.JSON(http.StatusOK, users) +} diff --git a/api/user/update.go b/api/user/update.go new file mode 100644 index 000000000..d2fb631ab --- /dev/null +++ b/api/user/update.go @@ -0,0 +1,124 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/users/{user} users UpdateUser +// +// Update a user for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: user +// description: Name of the user +// required: true +// type: string +// - in: body +// name: body +// description: Payload containing the user to update +// required: true +// schema: +// "$ref": "#/definitions/User" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the user +// schema: +// "$ref": "#/definitions/User" +// '400': +// description: Unable to update the user +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to update the user +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update the user +// schema: +// "$ref": "#/definitions/Error" + +// UpdateUser represents the API handler to update +// a user in the configured backend. +func UpdateUser(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + user := util.PathParameter(c, "user") + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Infof("updating user %s", user) + + // capture body from API request + input := new(library.User) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for user %s: %w", user, err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // send API call to capture the user + u, err = database.FromContext(c).GetUserForName(user) + if err != nil { + retErr := fmt.Errorf("unable to get user %s: %w", user, err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // update user fields if provided + if input.GetActive() { + // update active if set to true + u.SetActive(input.GetActive()) + } + + if input.GetAdmin() { + // update admin if set to true + u.SetAdmin(input.GetAdmin()) + } + + if input.Favorites != nil { + // update favorites if set + u.SetFavorites(input.GetFavorites()) + } + + // send API call to update the user + err = database.FromContext(c).UpdateUser(u) + if err != nil { + retErr := fmt.Errorf("unable to update user %s: %w", user, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // send API call to capture the updated user + u, _ = database.FromContext(c).GetUserForName(user) + + c.JSON(http.StatusOK, u) +} diff --git a/api/user/update_current.go b/api/user/update_current.go new file mode 100644 index 000000000..ed861ae61 --- /dev/null +++ b/api/user/update_current.go @@ -0,0 +1,105 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/user users UpdateCurrentUser +// +// Update the current authenticated user in the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: Payload containing the user to update +// required: true +// schema: +// "$ref": "#/definitions/User" +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the current user +// schema: +// "$ref": "#/definitions/User" +// '400': +// description: Unable to update the current user +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to update the current user +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update the current user +// schema: +// "$ref": "#/definitions/Error" + +// UpdateCurrentUser represents the API handler to capture and +// update the currently authenticated user from the configured backend. +func UpdateCurrentUser(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Infof("updating current user %s", u.GetName()) + + // capture body from API request + input := new(library.User) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update user fields if provided + if input.Favorites != nil { + // update favorites if set + u.SetFavorites(input.GetFavorites()) + } + + // send API call to update the user + err = database.FromContext(c).UpdateUser(u) + if err != nil { + retErr := fmt.Errorf("unable to update user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // send API call to capture the updated user + u, err = database.FromContext(c).GetUserForName(u.GetName()) + if err != nil { + retErr := fmt.Errorf("unable to get updated user %s: %w", u.GetName(), err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + c.JSON(http.StatusOK, u) +} diff --git a/api/webhook.go b/api/webhook.go deleted file mode 100644 index d5641e677..000000000 --- a/api/webhook.go +++ /dev/null @@ -1,679 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "strings" - "time" - - "github.com/go-vela/server/compiler" - "github.com/go-vela/server/database" - "github.com/go-vela/server/queue" - "github.com/go-vela/server/scm" - "github.com/go-vela/server/util" - - "github.com/go-vela/types" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/go-vela/types/pipeline" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -var baseErr = "unable to process webhook" - -// swagger:operation POST /webhook base PostWebhook -// -// Deliver a webhook to the vela api -// -// --- -// produces: -// - application/json -// parameters: -// - in: body -// name: body -// description: Webhook payload that we expect from the user or VCS -// required: true -// schema: -// "$ref": "#/definitions/Webhook" -// responses: -// '200': -// description: Successfully received the webhook -// schema: -// "$ref": "#/definitions/Build" -// '400': -// description: Malformed webhook payload -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to receive the webhook -// schema: -// "$ref": "#/definitions/Error" -// '401': -// description: Unauthenticated -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to receive the webhook -// schema: -// "$ref": "#/definitions/Error" - -// PostWebhook represents the API handler to capture -// a webhook from a source control provider and -// publish it to the configure queue. -// -// nolint: funlen,gocyclo // ignore function length and cyclomatic complexity -func PostWebhook(c *gin.Context) { - logrus.Info("webhook received") - - // capture middleware values - m := c.MustGet("metadata").(*types.Metadata) - - // duplicate request so we can perform operations on the request body - // - // https://golang.org/pkg/net/http/#Request.Clone - dupRequest := c.Request.Clone(context.TODO()) - - // -------------------- Start of TODO: -------------------- - // - // Remove the below code once http.Request.Clone() - // actually performs a deep clone. - // - // This code is required due to a known bug: - // - // * https://github.com/golang/go/issues/36095 - - // create buffer for reading request body - var buf bytes.Buffer - - // read the request body for duplication - _, err := buf.ReadFrom(c.Request.Body) - if err != nil { - retErr := fmt.Errorf("unable to read webhook body: %v", err) - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // add the request body to the original request - c.Request.Body = ioutil.NopCloser(&buf) - - // add the request body to the duplicate request - dupRequest.Body = ioutil.NopCloser(bytes.NewReader(buf.Bytes())) - // - // -------------------- End of TODO: -------------------- - - // process the webhook from the source control provider - // comment, number, h, r, b - webhook, err := scm.FromContext(c).ProcessWebhook(c.Request) - if err != nil { - retErr := fmt.Errorf("unable to parse webhook: %v", err) - util.HandleError(c, http.StatusBadRequest, retErr) - return - } - - // check if the hook should be skipped - if skip, skipReason := webhook.ShouldSkip(); skip { - c.JSON(http.StatusOK, fmt.Sprintf("skipping build: %s", skipReason)) - - return - } - - h, r, b := webhook.Hook, webhook.Repo, webhook.Build - - defer func() { - // send API call to update the webhook - err = database.FromContext(c).UpdateHook(h) - if err != nil { - logrus.Errorf("unable to update webhook %s/%s: %v", r.GetFullName(), h.GetSourceID(), err) - } - }() - - // check if build was parsed from webhook - if b == nil && h.GetEvent() != constants.EventRepositoryRename { - // typically, this should only happen on a webhook - // "ping" which gets sent when the webhook is created - c.JSON(http.StatusOK, "no build to process") - - return - } - - // check if repo was parsed from webhook - if r == nil { - retErr := fmt.Errorf("%s: failed to parse repo from webhook", baseErr) - util.HandleError(c, http.StatusBadRequest, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - if h.GetEvent() == constants.EventRepositoryRename { - err = renameRepository(h, r, c) - if err != nil { - util.HandleError(c, http.StatusBadRequest, err) - h.SetStatus(constants.StatusFailure) - h.SetError(err.Error()) - return - } - return - } - - // send API call to capture parsed repo from webhook - r, err = database.FromContext(c).GetRepo(r.GetOrg(), r.GetName()) - - if err != nil { - retErr := fmt.Errorf("%s: failed to get repo %s: %v", baseErr, r.GetFullName(), err) - util.HandleError(c, http.StatusBadRequest, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // set the RepoID fields - b.SetRepoID(r.GetID()) - h.SetRepoID(r.GetID()) - - // send API call to capture the last hook for the repo - lastHook, err := database.FromContext(c).GetLastHook(r) - if err != nil { - retErr := fmt.Errorf("unable to get last hook for repo %s: %v", r.GetFullName(), err) - util.HandleError(c, http.StatusInternalServerError, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // set the Number field - if lastHook != nil { - h.SetNumber( - lastHook.GetNumber() + 1, - ) - } - - // send API call to create the webhook - err = database.FromContext(c).CreateHook(h) - if err != nil { - retErr := fmt.Errorf("unable to create webhook %s/%d: %v", r.GetFullName(), h.GetNumber(), err) - util.HandleError(c, http.StatusInternalServerError, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // send API call to capture the created webhook - h, _ = database.FromContext(c).GetHook(h.GetNumber(), r) - - // verify the webhook from the source control provider - if c.Value("webhookvalidation").(bool) { - err = scm.FromContext(c).VerifyWebhook(dupRequest, r) - if err != nil { - retErr := fmt.Errorf("unable to verify webhook: %v", err) - util.HandleError(c, http.StatusUnauthorized, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - } - - // check if the repo is active - if !r.GetActive() { - retErr := fmt.Errorf("%s: %s is not an active repo", baseErr, r.GetFullName()) - util.HandleError(c, http.StatusBadRequest, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // verify the build has a valid event and the repo allows that event type - if (b.GetEvent() == constants.EventPush && !r.GetAllowPush()) || - (b.GetEvent() == constants.EventPull && !r.GetAllowPull()) || - (b.GetEvent() == constants.EventComment && !r.GetAllowComment()) || - (b.GetEvent() == constants.EventTag && !r.GetAllowTag()) || - (b.GetEvent() == constants.EventDeploy && !r.GetAllowDeploy()) { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("%s: %s does not have %s events enabled", baseErr, r.GetFullName(), b.GetEvent()) - util.HandleError(c, http.StatusBadRequest, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // check if the repo has a valid owner - if r.GetUserID() == 0 { - retErr := fmt.Errorf("%s: %s has no valid owner", baseErr, r.GetFullName()) - util.HandleError(c, http.StatusBadRequest, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // send API call to capture repo owner - u, err := database.FromContext(c).GetUser(r.GetUserID()) - if err != nil { - retErr := fmt.Errorf("%s: failed to get owner for %s: %v", baseErr, r.GetFullName(), err) - util.HandleError(c, http.StatusBadRequest, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // create SQL filters for querying pending and running builds for repo - filters := map[string]interface{}{ - "status": []string{constants.StatusPending, constants.StatusRunning}, - } - - // send API call to capture the number of pending or running builds for the repo - builds, err := database.FromContext(c).GetRepoBuildCount(r, filters) - if err != nil { - retErr := fmt.Errorf("%s: unable to get count of builds for repo %s", baseErr, r.GetFullName()) - util.HandleError(c, http.StatusBadRequest, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // check if the number of pending and running builds exceeds the limit for the repo - if builds >= r.GetBuildLimit() { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("%s: repo %s has exceeded the concurrent build limit of %d", baseErr, r.GetFullName(), r.GetBuildLimit()) - util.HandleError(c, http.StatusBadRequest, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // update fields in build object - b.SetNumber(r.GetCounter()) - b.SetParent(b.GetNumber()) - b.SetStatus(constants.StatusPending) - - // if this is a comment on a pull_request event - if strings.EqualFold(b.GetEvent(), constants.EventComment) && webhook.PRNumber > 0 { - commit, branch, baseref, headref, err := scm.FromContext(c).GetPullRequest(u, r, webhook.PRNumber) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("%s: failed to get pull request info for %s: %v", baseErr, r.GetFullName(), err) - util.HandleError(c, http.StatusInternalServerError, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - b.SetCommit(commit) - b.SetBranch(strings.Replace(branch, "refs/heads/", "", -1)) - b.SetBaseRef(baseref) - b.SetHeadRef(headref) - } - - // variable to store changeset files - var files []string - // check if the build event is not issue_comment - if !strings.EqualFold(b.GetEvent(), constants.EventComment) { - // check if the build event is not pull_request - if !strings.EqualFold(b.GetEvent(), constants.EventPull) { - // send API call to capture list of files changed for the commit - files, err = scm.FromContext(c).Changeset(u, r, b.GetCommit()) - if err != nil { - retErr := fmt.Errorf("%s: failed to get changeset for %s: %v", baseErr, r.GetFullName(), err) - util.HandleError(c, http.StatusInternalServerError, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - } - } - - // check if the build event is a pull_request - if strings.EqualFold(b.GetEvent(), constants.EventPull) && webhook.PRNumber > 0 { - // send API call to capture list of files changed for the pull request - files, err = scm.FromContext(c).ChangesetPR(u, r, webhook.PRNumber) - if err != nil { - retErr := fmt.Errorf("%s: failed to get changeset for %s: %v", baseErr, r.GetFullName(), err) - util.HandleError(c, http.StatusInternalServerError, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - } - - // send API call to capture the pipeline configuration file - config, err := scm.FromContext(c).ConfigBackoff(u, r, b.GetCommit()) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("%s: failed to get pipeline configuration for %s: %v", baseErr, r.GetFullName(), err) - util.HandleError(c, http.StatusNotFound, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // variable to store pipeline - var p *pipeline.Build - // number of times to retry - retryLimit := 3 - - // iterate through with a retryLimit - for i := 0; i < retryLimit; i++ { - // check if we're on the first iteration of the loop - if i > 0 { - // incrementally sleep in between retries - time.Sleep(time.Duration(i) * time.Second) - } - - // send API call to capture repo for the counter - r, err = database.FromContext(c).GetRepo(r.GetOrg(), r.GetName()) - if err != nil { - retErr := fmt.Errorf("%s: failed to get repo %s: %v", baseErr, r.GetFullName(), err) - util.HandleError(c, http.StatusBadRequest, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // set the parent equal to the current repo counter - b.SetParent(r.GetCounter()) - - // check if the parent is set to 0 - if b.GetParent() == 0 { - // parent should be "1" if it's the first build ran - b.SetParent(1) - } - - // update the build numbers based off repo counter - inc := r.GetCounter() + 1 - - r.SetCounter(inc) - b.SetNumber(inc) - - // populate the build link if a web address is provided - if len(m.Vela.WebAddress) > 0 { - b.SetLink( - fmt.Sprintf("%s/%s/%d", m.Vela.WebAddress, r.GetFullName(), b.GetNumber()), - ) - } - - // parse and compile the pipeline configuration file - p, err = compiler.FromContext(c). - Duplicate(). - WithBuild(b). - WithComment(webhook.Comment). - WithFiles(files). - WithMetadata(m). - WithRepo(r). - WithUser(u). - Compile(config) - if err != nil { - // format the error message with extra information - err = fmt.Errorf("unable to compile pipeline configuration for %s: %v", r.GetFullName(), err) - - // log the error for traceability - logrus.Error(err.Error()) - - retErr := fmt.Errorf("%s: %v", baseErr, err) - util.HandleError(c, http.StatusInternalServerError, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // skip the build if only the init or clone steps are found - skip := skipEmptyBuild(p) - if skip != "" { - // set build to successful status - b.SetStatus(constants.StatusSkipped) - - // send API call to set the status on the commit - err = scm.FromContext(c).Status(u, b, r.GetOrg(), r.GetName()) - if err != nil { - logrus.Errorf("unable to set commit status for %s/%d: %v", r.GetFullName(), b.GetNumber(), err) - } - - c.JSON(http.StatusOK, skip) - return - } - - // create the objects from the pipeline in the database - err = planBuild(database.FromContext(c), p, b, r) - if err != nil { - // log the error for traceability - logrus.Error(err.Error()) - - // check if the retry limit has been exceeded - if i < retryLimit { - // reset fields set by cleanBuild for retry - b.SetError("") - b.SetStatus(constants.StatusPending) - b.SetFinished(0) - - // continue to the next iteration of the loop - continue - } - - retErr := fmt.Errorf("%s: %v", baseErr, err) - util.HandleError(c, http.StatusInternalServerError, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // break the loop because everything was successful - break - } - - // send API call to update repo for ensuring counter is incremented - err = database.FromContext(c).UpdateRepo(r) - if err != nil { - retErr := fmt.Errorf("%s: failed to update repo %s: %v", baseErr, r.GetFullName(), err) - util.HandleError(c, http.StatusBadRequest, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - - return - } - - // send API call to capture the triggered build - b, err = database.FromContext(c).GetBuild(b.GetNumber(), r) - if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("%s: failed to get new build %s/%d: %v", baseErr, r.GetFullName(), b.GetNumber(), err) - util.HandleError(c, http.StatusInternalServerError, retErr) - - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - } - - // set the BuildID field - h.SetBuildID(b.GetID()) - - c.JSON(http.StatusOK, b) - - // send API call to set the status on the commit - err = scm.FromContext(c).Status(u, b, r.GetOrg(), r.GetName()) - if err != nil { - logrus.Errorf("unable to set commit status for %s/%d: %v", r.GetFullName(), b.GetNumber(), err) - } - - // publish the build to the queue - go publishToQueue( - queue.FromGinContext(c), - database.FromContext(c), - p, - b, - r, - u, - ) -} - -// publishToQueue is a helper function that creates -// a build item and publishes it to the queue. -// -// nolint: lll // ignore long line length due to variables -func publishToQueue(queue queue.Service, db database.Service, p *pipeline.Build, b *library.Build, r *library.Repo, u *library.User) { - item := types.ToItem(p, b, r, u) - - logrus.Infof("Converting queue item to json for build %d for %s", b.GetNumber(), r.GetFullName()) - - byteItem, err := json.Marshal(item) - if err != nil { - logrus.Errorf("Failed to convert item to json for build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) - - // error out the build - cleanBuild(db, b, nil, nil) - - return - } - - logrus.Infof("Establishing route for build %d for %s", b.GetNumber(), r.GetFullName()) - - route, err := queue.Route(&p.Worker) - if err != nil { - logrus.Errorf("unable to set route for build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) - - // error out the build - cleanBuild(db, b, nil, nil) - - return - } - - logrus.Infof("Publishing item for build %d for %s to queue %s", b.GetNumber(), r.GetFullName(), route) - - err = queue.Push(context.Background(), route, byteItem) - if err != nil { - logrus.Errorf("Retrying; Failed to publish build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) - - err = queue.Push(context.Background(), route, byteItem) - if err != nil { - logrus.Errorf("Failed to publish build %d for %s: %v", b.GetNumber(), r.GetFullName(), err) - - // error out the build - cleanBuild(db, b, nil, nil) - - return - } - } - - // update fields in build object - b.SetEnqueued(time.Now().UTC().Unix()) - - // update the build in the db to reflect the time it was enqueued - err = db.UpdateBuild(b) - if err != nil { - logrus.Errorf("Failed to update build %d during publish to queue for %s: %v", b.GetNumber(), r.GetFullName(), err) - } -} - -// renameRepository is a helper function that takes the old name of the repo, -// queries the database for the repo that matches that name and org, and updates -// that repo to its new name in order to preserve it. It also updates the secrets -// associated with that repo. -func renameRepository(h *library.Hook, r *library.Repo, c *gin.Context) error { - // get the old name of the repo - previousName := r.GetPreviousName() - // get the repo from the database that matches the old name - dbR, err := database.FromContext(c).GetRepo(r.GetOrg(), previousName) - if err != nil { - // nolint: lll // ignore long line for error formatting - retErr := fmt.Errorf("%s: failed to get repo %s/%s from database", baseErr, r.GetOrg(), previousName) - util.HandleError(c, http.StatusBadRequest, retErr) - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - return retErr - } - - // update the repo name information - dbR.SetName(r.GetName()) - dbR.SetFullName(r.GetFullName()) - dbR.SetClone(r.GetClone()) - dbR.SetLink(r.GetLink()) - dbR.SetPreviousName(previousName) - - // update the repo in the database - err = database.FromContext(c).UpdateRepo(dbR) - if err != nil { - // nolint: lll // ignore long line for error formatting - retErr := fmt.Errorf("%s: failed to update repo %s/%s in database", baseErr, r.GetOrg(), previousName) - util.HandleError(c, http.StatusBadRequest, retErr) - h.SetStatus(constants.StatusFailure) - h.SetError(retErr.Error()) - return retErr - } - - // get total number of secrets associated with repository - // nolint: lll // ignore long line due to extensive function arguments - t, err := database.FromContext(c).GetTypeSecretCount(constants.SecretRepo, r.GetOrg(), previousName, []string{}) - if err != nil { - return fmt.Errorf("unable to get secret count for repo %s/%s: %w", r.GetOrg(), previousName, err) - } - secrets := []*library.Secret{} - - page := 1 - // capture all secrets belonging to certain repo in database - // nolint: gomnd // ignore magic number - for repoSecrets := int64(0); repoSecrets < t; repoSecrets += 100 { - // nolint: lll // ignore long line due to extensive function arguments - s, err := database.FromContext(c).GetTypeSecretList(constants.SecretRepo, r.GetOrg(), previousName, page, 100, []string{}) - if err != nil { - return fmt.Errorf("unable to get secret list for repo %s/%s: %w", r.GetOrg(), previousName, err) - } - secrets = append(secrets, s...) - page++ - } - - // update secrets to point to the new repository name - for _, secret := range secrets { - secret.SetRepo(r.GetName()) - err = database.FromContext(c).UpdateSecret(secret) - if err != nil { - return fmt.Errorf("unable to update secret for repo %s/%s: %w", r.GetOrg(), previousName, err) - } - } - - c.JSON(http.StatusOK, r) - return nil -} diff --git a/api/webhook/doc.go b/api/webhook/doc.go new file mode 100644 index 000000000..0acc05476 --- /dev/null +++ b/api/webhook/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package webhook provides the webhook handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/webhook" +package webhook diff --git a/api/webhook/post.go b/api/webhook/post.go new file mode 100644 index 000000000..ce11a614f --- /dev/null +++ b/api/webhook/post.go @@ -0,0 +1,909 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package webhook + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "reflect" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api/build" + "github.com/go-vela/server/compiler" + "github.com/go-vela/server/database" + "github.com/go-vela/server/queue" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" + "github.com/sirupsen/logrus" +) + +var baseErr = "unable to process webhook" + +// swagger:operation POST /webhook base PostWebhook +// +// Deliver a webhook to the vela api +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: Webhook payload that we expect from the user or VCS +// required: true +// schema: +// "$ref": "#/definitions/Webhook" +// responses: +// '200': +// description: Successfully received the webhook +// schema: +// "$ref": "#/definitions/Build" +// '400': +// description: Malformed webhook payload +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to receive the webhook +// schema: +// "$ref": "#/definitions/Error" +// '401': +// description: Unauthenticated +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to receive the webhook +// schema: +// "$ref": "#/definitions/Error" + +// PostWebhook represents the API handler to capture +// a webhook from a source control provider and +// publish it to the configure queue. +// +//nolint:funlen,gocyclo // ignore function length and cyclomatic complexity +func PostWebhook(c *gin.Context) { + logrus.Info("webhook received") + + // capture middleware values + m := c.MustGet("metadata").(*types.Metadata) + ctx := c.Request.Context() + + // duplicate request so we can perform operations on the request body + // + // https://golang.org/pkg/net/http/#Request.Clone + dupRequest := c.Request.Clone(ctx) + + // -------------------- Start of TODO: -------------------- + // + // Remove the below code once http.Request.Clone() + // actually performs a deep clone. + // + // This code is required due to a known bug: + // + // * https://github.com/golang/go/issues/36095 + + // create buffer for reading request body + var buf bytes.Buffer + + // read the request body for duplication + _, err := buf.ReadFrom(c.Request.Body) + if err != nil { + retErr := fmt.Errorf("unable to read webhook body: %w", err) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // add the request body to the original request + c.Request.Body = io.NopCloser(&buf) + + // add the request body to the duplicate request + dupRequest.Body = io.NopCloser(bytes.NewReader(buf.Bytes())) + // + // -------------------- End of TODO: -------------------- + + // process the webhook from the source control provider + // comment, number, h, r, b + webhook, err := scm.FromContext(c).ProcessWebhook(c.Request) + if err != nil { + retErr := fmt.Errorf("unable to parse webhook: %w", err) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // check if the hook should be skipped + if skip, skipReason := webhook.ShouldSkip(); skip { + c.JSON(http.StatusOK, fmt.Sprintf("skipping build: %s", skipReason)) + + return + } + + h, r, b := webhook.Hook, webhook.Repo, webhook.Build + + logrus.Debugf("hook generated from SCM: %v", h) + logrus.Debugf("repo generated from SCM: %v", r) + + // if event is repository event, handle separately and return + if strings.EqualFold(h.GetEvent(), constants.EventRepository) { + r, err = handleRepositoryEvent(ctx, c, m, h, r) + if err != nil { + util.HandleError(c, http.StatusInternalServerError, err) + return + } + + // if there were actual changes to the repo, return the repo object + if r.GetID() != 0 { + c.JSON(http.StatusOK, r) + return + } + + c.JSON(http.StatusOK, "handled repository event, no build to process") + + return + } + + // check if build was parsed from webhook. + if b == nil { + // typically, this should only happen on a webhook + // "ping" which gets sent when the webhook is created + c.JSON(http.StatusOK, "no build to process") + + return + } + + logrus.Debugf(`build author: %s, + build branch: %s, + build commit: %s, + build ref: %s`, + b.GetAuthor(), b.GetBranch(), b.GetCommit(), b.GetRef()) + + // check if repo was parsed from webhook + if r == nil { + retErr := fmt.Errorf("%s: failed to parse repo from webhook", baseErr) + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + defer func() { + // send API call to update the webhook + _, err = database.FromContext(c).UpdateHook(h) + if err != nil { + logrus.Errorf("unable to update webhook %s/%d: %v", r.GetFullName(), h.GetNumber(), err) + } + }() + + // send API call to capture parsed repo from webhook + repo, err := database.FromContext(c).GetRepoForOrg(ctx, r.GetOrg(), r.GetName()) + if err != nil { + retErr := fmt.Errorf("%s: failed to get repo %s: %w", baseErr, r.GetFullName(), err) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // set the RepoID fields + b.SetRepoID(repo.GetID()) + h.SetRepoID(repo.GetID()) + + // send API call to capture the last hook for the repo + lastHook, err := database.FromContext(c).LastHookForRepo(repo) + if err != nil { + retErr := fmt.Errorf("unable to get last hook for repo %s: %w", repo.GetFullName(), err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // set the Number field + if lastHook != nil { + h.SetNumber( + lastHook.GetNumber() + 1, + ) + } + + // send API call to create the webhook + h, err = database.FromContext(c).CreateHook(h) + if err != nil { + retErr := fmt.Errorf("unable to create webhook %s/%d: %w", repo.GetFullName(), h.GetNumber(), err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // verify the webhook from the source control provider + if c.Value("webhookvalidation").(bool) { + err = scm.FromContext(c).VerifyWebhook(dupRequest, repo) + if err != nil { + retErr := fmt.Errorf("unable to verify webhook: %w", err) + util.HandleError(c, http.StatusUnauthorized, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + } + + // check if the repo is active + if !repo.GetActive() { + retErr := fmt.Errorf("%s: %s is not an active repo", baseErr, repo.GetFullName()) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // verify the build has a valid event and the repo allows that event type + if (b.GetEvent() == constants.EventPush && !repo.GetAllowPush()) || + (b.GetEvent() == constants.EventPull && !repo.GetAllowPull()) || + (b.GetEvent() == constants.EventComment && !repo.GetAllowComment()) || + (b.GetEvent() == constants.EventTag && !repo.GetAllowTag()) || + (b.GetEvent() == constants.EventDeploy && !repo.GetAllowDeploy()) { + retErr := fmt.Errorf("%s: %s does not have %s events enabled", baseErr, repo.GetFullName(), b.GetEvent()) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // check if the repo has a valid owner + if repo.GetUserID() == 0 { + retErr := fmt.Errorf("%s: %s has no valid owner", baseErr, repo.GetFullName()) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // send API call to capture repo owner + logrus.Debugf("capturing owner of repository %s", repo.GetFullName()) + + u, err := database.FromContext(c).GetUser(repo.GetUserID()) + if err != nil { + retErr := fmt.Errorf("%s: failed to get owner for %s: %w", baseErr, repo.GetFullName(), err) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // confirm current repo owner has at least write access to repo (needed for status update later) + _, err = scm.FromContext(c).RepoAccess(u, u.GetToken(), r.GetOrg(), r.GetName()) + if err != nil { + retErr := fmt.Errorf("unable to publish build to queue: repository owner %s no longer has write access to repository %s", u.GetName(), r.GetFullName()) + util.HandleError(c, http.StatusUnauthorized, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // create SQL filters for querying pending and running builds for repo + filters := map[string]interface{}{ + "status": []string{constants.StatusPending, constants.StatusRunning}, + } + + // send API call to capture the number of pending or running builds for the repo + builds, err := database.FromContext(c).CountBuildsForRepo(ctx, repo, filters) + if err != nil { + retErr := fmt.Errorf("%s: unable to get count of builds for repo %s", baseErr, repo.GetFullName()) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + logrus.Debugf("currently %d builds running on repo %s", builds, repo.GetFullName()) + + // check if the number of pending and running builds exceeds the limit for the repo + if builds >= repo.GetBuildLimit() { + retErr := fmt.Errorf("%s: repo %s has exceeded the concurrent build limit of %d", baseErr, repo.GetFullName(), repo.GetBuildLimit()) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // update fields in build object + logrus.Debugf("updating build number to %d", repo.GetCounter()) + b.SetNumber(repo.GetCounter()) + + logrus.Debugf("updating parent number to %d", b.GetNumber()) + b.SetParent(b.GetNumber()) + + logrus.Debug("updating status to pending") + b.SetStatus(constants.StatusPending) + + // if this is a comment on a pull_request event + if strings.EqualFold(b.GetEvent(), constants.EventComment) && webhook.PRNumber > 0 { + commit, branch, baseref, headref, err := scm.FromContext(c).GetPullRequest(u, repo, webhook.PRNumber) + if err != nil { + retErr := fmt.Errorf("%s: failed to get pull request info for %s: %w", baseErr, repo.GetFullName(), err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + b.SetCommit(commit) + b.SetBranch(strings.Replace(branch, "refs/heads/", "", -1)) + b.SetBaseRef(baseref) + b.SetHeadRef(headref) + } + + // variable to store changeset files + var files []string + // check if the build event is not issue_comment or pull_request + if !strings.EqualFold(b.GetEvent(), constants.EventComment) && + !strings.EqualFold(b.GetEvent(), constants.EventPull) { + // send API call to capture list of files changed for the commit + files, err = scm.FromContext(c).Changeset(u, repo, b.GetCommit()) + if err != nil { + retErr := fmt.Errorf("%s: failed to get changeset for %s: %w", baseErr, repo.GetFullName(), err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + } + + // check if the build event is a pull_request + if strings.EqualFold(b.GetEvent(), constants.EventPull) && webhook.PRNumber > 0 { + // send API call to capture list of files changed for the pull request + files, err = scm.FromContext(c).ChangesetPR(u, repo, webhook.PRNumber) + if err != nil { + retErr := fmt.Errorf("%s: failed to get changeset for %s: %w", baseErr, repo.GetFullName(), err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + } + + var ( + // variable to store the raw pipeline configuration + config []byte + // variable to store executable pipeline + p *pipeline.Build + // variable to store pipeline configuration + pipeline *library.Pipeline + // variable to control number of times to retry processing pipeline + retryLimit = 3 + // variable to store the pipeline type for the repository + pipelineType = repo.GetPipelineType() + ) + + // implement a loop to process asynchronous operations with a retry limit + // + // Some operations taken during the webhook workflow can lead to race conditions + // failing to successfully process the request. This logic ensures we attempt our + // best efforts to handle these cases gracefully. + for i := 0; i < retryLimit; i++ { + logrus.Debugf("compilation loop - attempt %d", i+1) + // check if we're on the first iteration of the loop + if i > 0 { + // incrementally sleep in between retries + time.Sleep(time.Duration(i) * time.Second) + } + + // send API call to attempt to capture the pipeline + pipeline, err = database.FromContext(c).GetPipelineForRepo(ctx, b.GetCommit(), repo) + if err != nil { // assume the pipeline doesn't exist in the database yet + // send API call to capture the pipeline configuration file + config, err = scm.FromContext(c).ConfigBackoff(u, repo, b.GetCommit()) + if err != nil { + retErr := fmt.Errorf("%s: unable to get pipeline configuration for %s: %w", baseErr, repo.GetFullName(), err) + + util.HandleError(c, http.StatusNotFound, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + } else { + config = pipeline.GetData() + } + + // send API call to capture repo for the counter (grabbing repo again to ensure counter is correct) + repo, err = database.FromContext(c).GetRepoForOrg(ctx, repo.GetOrg(), repo.GetName()) + if err != nil { + retErr := fmt.Errorf("%s: unable to get repo %s: %w", baseErr, r.GetFullName(), err) + + // check if the retry limit has been exceeded + if i < retryLimit-1 { + logrus.WithError(retErr).Warningf("retrying #%d", i+1) + + // continue to the next iteration of the loop + continue + } + + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // update repo fields with any changes from SCM process + repo.SetTopics(r.GetTopics()) + repo.SetBranch(r.GetBranch()) + + // set the parent equal to the current repo counter + b.SetParent(repo.GetCounter()) + + // check if the parent is set to 0 + if b.GetParent() == 0 { + // parent should be "1" if it's the first build ran + b.SetParent(1) + } + + // update the build numbers based off repo counter + inc := repo.GetCounter() + 1 + repo.SetCounter(inc) + b.SetNumber(inc) + + // populate the build link if a web address is provided + if len(m.Vela.WebAddress) > 0 { + b.SetLink( + fmt.Sprintf("%s/%s/%d", m.Vela.WebAddress, repo.GetFullName(), b.GetNumber()), + ) + } + + // ensure we use the expected pipeline type when compiling + // + // The pipeline type for a repo can change at any time which can break compiling + // existing pipelines in the system for that repo. To account for this, we update + // the repo pipeline type to match what was defined for the existing pipeline + // before compiling. After we're done compiling, we reset the pipeline type. + if len(pipeline.GetType()) > 0 { + repo.SetPipelineType(pipeline.GetType()) + } + + var compiled *library.Pipeline + // parse and compile the pipeline configuration file + p, compiled, err = compiler.FromContext(c). + Duplicate(). + WithBuild(b). + WithComment(webhook.Comment). + WithCommit(b.GetCommit()). + WithFiles(files). + WithMetadata(m). + WithRepo(repo). + WithUser(u). + Compile(config) + if err != nil { + // format the error message with extra information + err = fmt.Errorf("unable to compile pipeline configuration for %s: %w", repo.GetFullName(), err) + + // log the error for traceability + logrus.Error(err.Error()) + + retErr := fmt.Errorf("%s: %w", baseErr, err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // reset the pipeline type for the repo + // + // The pipeline type for a repo can change at any time which can break compiling + // existing pipelines in the system for that repo. To account for this, we update + // the repo pipeline type to match what was defined for the existing pipeline + // before compiling. After we're done compiling, we reset the pipeline type. + repo.SetPipelineType(pipelineType) + + // skip the build if only the init or clone steps are found + skip := build.SkipEmptyBuild(p) + if skip != "" { + // set build to successful status + b.SetStatus(constants.StatusSkipped) + + // send API call to set the status on the commit + err = scm.FromContext(c).Status(u, b, repo.GetOrg(), repo.GetName()) + if err != nil { + logrus.Errorf("unable to set commit status for %s/%d: %v", repo.GetFullName(), b.GetNumber(), err) + } + + c.JSON(http.StatusOK, skip) + + return + } + + // check if the pipeline did not already exist in the database + if pipeline == nil { + pipeline = compiled + pipeline.SetRepoID(repo.GetID()) + pipeline.SetCommit(b.GetCommit()) + pipeline.SetRef(b.GetRef()) + + // send API call to create the pipeline + pipeline, err = database.FromContext(c).CreatePipeline(ctx, pipeline) + if err != nil { + retErr := fmt.Errorf("%s: failed to create pipeline for %s: %w", baseErr, repo.GetFullName(), err) + + // check if the retry limit has been exceeded + if i < retryLimit-1 { + logrus.WithError(retErr).Warningf("retrying #%d", i+1) + + // continue to the next iteration of the loop + continue + } + + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + } + + b.SetPipelineID(pipeline.GetID()) + + // create the objects from the pipeline in the database + // TODO: + // - if a build gets created and something else fails midway, + // the next loop will attempt to create the same build, + // using the same Number and thus create a constraint + // conflict; consider deleting the partially created + // build object in the database + err = build.PlanBuild(ctx, database.FromContext(c), p, b, repo) + if err != nil { + retErr := fmt.Errorf("%s: %w", baseErr, err) + + // check if the retry limit has been exceeded + if i < retryLimit-1 { + logrus.WithError(retErr).Warningf("retrying #%d", i+1) + + // reset fields set by cleanBuild for retry + b.SetError("") + b.SetStatus(constants.StatusPending) + b.SetFinished(0) + + // continue to the next iteration of the loop + continue + } + + util.HandleError(c, http.StatusInternalServerError, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // break the loop because everything was successful + break + } // end of retry loop + + // send API call to update repo for ensuring counter is incremented + repo, err = database.FromContext(c).UpdateRepo(ctx, repo) + if err != nil { + retErr := fmt.Errorf("%s: failed to update repo %s: %w", baseErr, repo.GetFullName(), err) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // return error if pipeline didn't get populated + if p == nil { + retErr := fmt.Errorf("%s: failed to set pipeline for %s: %w", baseErr, repo.GetFullName(), err) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // return error if build didn't get populated + if b == nil { + retErr := fmt.Errorf("%s: failed to set build for %s: %w", baseErr, repo.GetFullName(), err) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // send API call to capture the triggered build + b, err = database.FromContext(c).GetBuildForRepo(ctx, repo, b.GetNumber()) + if err != nil { + retErr := fmt.Errorf("%s: failed to get new build %s/%d: %w", baseErr, repo.GetFullName(), b.GetNumber(), err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return + } + + // set the BuildID field + h.SetBuildID(b.GetID()) + + c.JSON(http.StatusOK, b) + + // send API call to set the status on the commit + err = scm.FromContext(c).Status(u, b, repo.GetOrg(), repo.GetName()) + if err != nil { + logrus.Errorf("unable to set commit status for %s/%d: %v", repo.GetFullName(), b.GetNumber(), err) + } + + // publish the build to the queue + go build.PublishToQueue( + ctx, + queue.FromGinContext(c), + database.FromContext(c), + p, + b, + repo, + u, + ) +} + +func handleRepositoryEvent(ctx context.Context, c *gin.Context, m *types.Metadata, h *library.Hook, r *library.Repo) (*library.Repo, error) { + logrus.Debugf("webhook is repository event, making necessary updates to repo %s", r.GetFullName()) + + defer func() { + // send API call to update the webhook + _, err := database.FromContext(c).CreateHook(h) + if err != nil { + logrus.Errorf("unable to create webhook %s/%d: %v", r.GetFullName(), h.GetNumber(), err) + } + }() + + switch h.GetEventAction() { + // if action is rename, go through rename routine + case constants.ActionRenamed, constants.ActionTransferred: + r, err := renameRepository(ctx, h, r, c, m) + if err != nil { + h.SetStatus(constants.StatusFailure) + h.SetError(err.Error()) + + return nil, err + } + + return r, nil + // if action is archived, unarchived, or edited, perform edits to relevant repo fields + case "archived", "unarchived", constants.ActionEdited: + logrus.Debugf("repository action %s for %s", h.GetEventAction(), r.GetFullName()) + // send call to get repository from database + dbRepo, err := database.FromContext(c).GetRepoForOrg(ctx, r.GetOrg(), r.GetName()) + if err != nil { + retErr := fmt.Errorf("%s: failed to get repo %s: %w", baseErr, r.GetFullName(), err) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return nil, retErr + } + + // send API call to capture the last hook for the repo + lastHook, err := database.FromContext(c).LastHookForRepo(dbRepo) + if err != nil { + retErr := fmt.Errorf("unable to get last hook for repo %s: %w", r.GetFullName(), err) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return nil, retErr + } + + // set the Number field + if lastHook != nil { + h.SetNumber( + lastHook.GetNumber() + 1, + ) + } + + h.SetRepoID(dbRepo.GetID()) + + // the only edits to a repo that impact Vela are to these three fields + if !strings.EqualFold(dbRepo.GetBranch(), r.GetBranch()) { + dbRepo.SetBranch(r.GetBranch()) + } + + if dbRepo.GetActive() != r.GetActive() { + dbRepo.SetActive(r.GetActive()) + } + + if !reflect.DeepEqual(dbRepo.GetTopics(), r.GetTopics()) { + dbRepo.SetTopics(r.GetTopics()) + } + + // update repo object in the database after applying edits + dbRepo, err = database.FromContext(c).UpdateRepo(ctx, dbRepo) + if err != nil { + retErr := fmt.Errorf("%s: failed to update repo %s: %w", baseErr, r.GetFullName(), err) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return nil, err + } + + return dbRepo, nil + // all other repo event actions are skippable + default: + return r, nil + } +} + +// renameRepository is a helper function that takes the old name of the repo, +// queries the database for the repo that matches that name and org, and updates +// that repo to its new name in order to preserve it. It also updates the secrets +// associated with that repo as well as build links for the UI. +func renameRepository(ctx context.Context, h *library.Hook, r *library.Repo, c *gin.Context, m *types.Metadata) (*library.Repo, error) { + logrus.Infof("renaming repository from %s to %s", r.GetPreviousName(), r.GetName()) + + // get the old name of the repo + prevOrg, prevRepo := util.SplitFullName(r.GetPreviousName()) + + // get the repo from the database that matches the old name + dbR, err := database.FromContext(c).GetRepoForOrg(ctx, prevOrg, prevRepo) + if err != nil { + retErr := fmt.Errorf("%s: failed to get repo %s/%s from database", baseErr, prevOrg, prevRepo) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return nil, retErr + } + + // update hook object which will be added to DB upon reaching deferred function in PostWebhook + h.SetRepoID(r.GetID()) + + // send API call to capture the last hook for the repo + lastHook, err := database.FromContext(c).LastHookForRepo(dbR) + if err != nil { + retErr := fmt.Errorf("unable to get last hook for repo %s: %w", r.GetFullName(), err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return nil, retErr + } + + // set the Number field + if lastHook != nil { + h.SetNumber( + lastHook.GetNumber() + 1, + ) + } + + // get total number of secrets associated with repository + t, err := database.FromContext(c).CountSecretsForRepo(dbR, map[string]interface{}{}) + if err != nil { + return nil, fmt.Errorf("unable to get secret count for repo %s/%s: %w", prevOrg, prevRepo, err) + } + + secrets := []*library.Secret{} + page := 1 + // capture all secrets belonging to certain repo in database + for repoSecrets := int64(0); repoSecrets < t; repoSecrets += 100 { + s, _, err := database.FromContext(c).ListSecretsForRepo(dbR, map[string]interface{}{}, page, 100) + if err != nil { + return nil, fmt.Errorf("unable to get secret list for repo %s/%s: %w", prevOrg, prevRepo, err) + } + + secrets = append(secrets, s...) + + page++ + } + + // update secrets to point to the new repository name + for _, secret := range secrets { + secret.SetOrg(r.GetOrg()) + secret.SetRepo(r.GetName()) + + _, err = database.FromContext(c).UpdateSecret(secret) + if err != nil { + return nil, fmt.Errorf("unable to update secret for repo %s/%s: %w", prevOrg, prevRepo, err) + } + } + + // get total number of builds associated with repository + t, err = database.FromContext(c).CountBuildsForRepo(ctx, dbR, nil) + if err != nil { + return nil, fmt.Errorf("unable to get build count for repo %s: %w", dbR.GetFullName(), err) + } + + builds := []*library.Build{} + page = 1 + // capture all builds belonging to repo in database + for build := int64(0); build < t; build += 100 { + b, _, err := database.FromContext(c).ListBuildsForRepo(ctx, dbR, nil, time.Now().Unix(), 0, page, 100) + if err != nil { + return nil, fmt.Errorf("unable to get build list for repo %s: %w", dbR.GetFullName(), err) + } + + builds = append(builds, b...) + + page++ + } + + // update build link to route to proper repo name + for _, build := range builds { + build.SetLink( + fmt.Sprintf("%s/%s/%d", m.Vela.WebAddress, r.GetFullName(), build.GetNumber()), + ) + + _, err = database.FromContext(c).UpdateBuild(ctx, build) + if err != nil { + return nil, fmt.Errorf("unable to update build for repo %s: %w", dbR.GetFullName(), err) + } + } + + // update the repo name information + dbR.SetName(r.GetName()) + dbR.SetOrg(r.GetOrg()) + dbR.SetFullName(r.GetFullName()) + dbR.SetClone(r.GetClone()) + dbR.SetLink(r.GetLink()) + dbR.SetPreviousName(r.GetPreviousName()) + + // update the repo in the database + dbR, err = database.FromContext(c).UpdateRepo(ctx, dbR) + if err != nil { + retErr := fmt.Errorf("%s: failed to update repo %s/%s in database", baseErr, prevOrg, prevRepo) + util.HandleError(c, http.StatusBadRequest, retErr) + + h.SetStatus(constants.StatusFailure) + h.SetError(retErr.Error()) + + return nil, retErr + } + + return dbR, nil +} diff --git a/api/worker.go b/api/worker.go deleted file mode 100644 index 6f5b65636..000000000 --- a/api/worker.go +++ /dev/null @@ -1,342 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package api - -import ( - "fmt" - "net/http" - - "github.com/go-vela/server/router/middleware/user" - - "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/worker" - "github.com/go-vela/server/util" - - "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" - "github.com/sirupsen/logrus" -) - -// swagger:operation POST /api/v1/workers workers CreateWorker -// -// Create a worker for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: body -// name: body -// description: Payload containing the worker to create -// required: true -// schema: -// "$ref": "#/definitions/Worker" -// security: -// - ApiKeyAuth: [] -// responses: -// '201': -// description: Successfully created the worker -// schema: -// type: string -// '400': -// description: Unable to create the worker -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to create the worker -// schema: -// "$ref": "#/definitions/Error" - -// CreateWorker represents the API handler to -// create a worker in the configured backend. -func CreateWorker(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - - // capture body from API request - input := new(library.Worker) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for new worker: %w", err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - "worker": input.GetHostname(), - }).Infof("creating new worker %s", input.GetHostname()) - - err = database.FromContext(c).CreateWorker(input) - if err != nil { - retErr := fmt.Errorf("unable to create worker: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusCreated, fmt.Sprintf("worker %s created", input.GetHostname())) -} - -// swagger:operation GET /api/v1/workers workers GetWorkers -// -// Retrieve a list of workers for the configured backend -// -// --- -// produces: -// - application/json -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the list of workers -// schema: -// type: array -// items: -// "$ref": "#/definitions/Worker" -// '500': -// description: Unable to retrieve the list of workers -// schema: -// "$ref": "#/definitions/Error" - -// GetWorkers represents the API handler to capture a -// list of workers from the configured backend. -func GetWorkers(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Info("reading workers") - - w, err := database.FromContext(c).GetWorkerList() - if err != nil { - retErr := fmt.Errorf("unable to get workers: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, w) -} - -// swagger:operation GET /api/v1/workers/{worker} workers GetWorker -// -// Retrieve a worker for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: worker -// description: Hostname of the worker -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully retrieved the worker -// schema: -// "$ref": "#/definitions/Worker" -// '404': -// description: Unable to retrieve the worker -// schema: -// "$ref": "#/definitions/Error" - -// GetWorker represents the API handler to capture a -// worker from the configured backend. -func GetWorker(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - w := worker.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - "worker": w.GetHostname(), - }).Infof("reading worker %s", w.GetHostname()) - - w, err := database.FromContext(c).GetWorker(w.GetHostname()) - if err != nil { - retErr := fmt.Errorf("unable to get workers: %w", err) - - util.HandleError(c, http.StatusNotFound, retErr) - - return - } - - c.JSON(http.StatusOK, w) -} - -// swagger:operation PUT /api/v1/workers/{worker} workers UpdateWorker -// -// Update a worker for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: body -// name: body -// description: Payload containing the worker to update -// required: true -// schema: -// "$ref": "#/definitions/Worker" -// - in: path -// name: worker -// description: Name of the worker -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully updated the worker -// schema: -// "$ref": "#/definitions/Worker" -// '400': -// description: Unable to update the worker -// schema: -// "$ref": "#/definitions/Error" -// '404': -// description: Unable to update the worker -// schema: -// "$ref": "#/definitions/Error" -// '500': -// description: Unable to update the worker -// schema: -// "$ref": "#/definitions/Error" - -// UpdateWorker represents the API handler to -// create a worker in the configured backend. -func UpdateWorker(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - w := worker.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - "worker": w.GetHostname(), - }).Infof("updating worker %s", w.GetHostname()) - - // capture body from API request - input := new(library.Worker) - - err := c.Bind(input) - if err != nil { - retErr := fmt.Errorf("unable to decode JSON for worker %s: %w", w.GetHostname(), err) - - util.HandleError(c, http.StatusBadRequest, retErr) - - return - } - - if len(input.GetAddress()) > 0 { - // update admin if set - w.SetAddress(input.GetAddress()) - } - - if len(input.GetRoutes()) > 0 { - // update routes if set - w.SetRoutes(input.GetRoutes()) - } - - if input.GetActive() { - // update active if set - w.SetActive(input.GetActive()) - } - - if input.GetLastCheckedIn() > 0 { - // update LastCheckedIn if set - w.SetLastCheckedIn(input.GetLastCheckedIn()) - } - - // send API call to update the worker - err = database.FromContext(c).UpdateWorker(w) - if err != nil { - retErr := fmt.Errorf("unable to update worker %s: %w", w.GetHostname(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - // send API call to capture the updated worker - w, _ = database.FromContext(c).GetWorker(w.GetHostname()) - - c.JSON(http.StatusOK, w) -} - -// swagger:operation DELETE /api/v1/workers/{worker} workers DeleteWorker -// -// Delete a worker for the configured backend -// -// --- -// produces: -// - application/json -// parameters: -// - in: path -// name: worker -// description: Name of the worker -// required: true -// type: string -// security: -// - ApiKeyAuth: [] -// responses: -// '200': -// description: Successfully deleted of worker -// schema: -// type: string -// '500': -// description: Unable to delete worker -// schema: -// "$ref": "#/definitions/Error" - -// DeleteWorker represents the API handler to remove -// a worker from the configured backend. -func DeleteWorker(c *gin.Context) { - // capture middleware values - u := user.Retrieve(c) - w := worker.Retrieve(c) - - // update engine logger with API metadata - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields - logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - "worker": w.GetHostname(), - }).Infof("deleting worker %s", w.GetHostname()) - - // send API call to remove the step - err := database.FromContext(c).DeleteWorker(w.GetID()) - if err != nil { - retErr := fmt.Errorf("unable to delete worker %s: %w", w.GetHostname(), err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - - c.JSON(http.StatusOK, fmt.Sprintf("worker %s deleted", w.GetHostname())) -} diff --git a/api/worker/create.go b/api/worker/create.go new file mode 100644 index 000000000..b7d1bf286 --- /dev/null +++ b/api/worker/create.go @@ -0,0 +1,141 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/claims" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/workers workers CreateWorker +// +// Create a worker for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: Payload containing the worker to create +// required: true +// schema: +// "$ref": "#/definitions/Worker" +// security: +// - ApiKeyAuth: [] +// responses: +// '201': +// description: Successfully created the worker and retrieved auth token +// schema: +// "$ref": "#definitions/Token" +// '400': +// description: Unable to create the worker +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to create the worker +// schema: +// "$ref": "#/definitions/Error" + +// CreateWorker represents the API handler to +// create a worker in the configured backend. +func CreateWorker(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + cl := claims.Retrieve(c) + + // capture body from API request + input := new(library.Worker) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for new worker: %w", err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // verify input host name matches worker hostname + if !strings.EqualFold(cl.TokenType, constants.ServerWorkerTokenType) && !strings.EqualFold(cl.Subject, input.GetHostname()) { + retErr := fmt.Errorf("unable to add worker; claims subject %s does not match worker hostname %s", cl.Subject, input.GetHostname()) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + input.SetLastCheckedIn(time.Now().Unix()) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + "worker": input.GetHostname(), + }).Infof("creating new worker %s", input.GetHostname()) + + err = database.FromContext(c).CreateWorker(input) + if err != nil { + retErr := fmt.Errorf("unable to create worker: %w", err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + switch cl.TokenType { + // if symmetric token configured, send back symmetric token + case constants.ServerWorkerTokenType: + if secret, ok := c.Value("secret").(string); ok { + tkn := new(library.Token) + tkn.SetToken(secret) + c.JSON(http.StatusCreated, tkn) + + return + } + + retErr := fmt.Errorf("symmetric token provided but not configured in server") + util.HandleError(c, http.StatusBadRequest, retErr) + + return + // if worker register token, send back auth token + default: + tm := c.MustGet("token-manager").(*token.Manager) + + wmto := &token.MintTokenOpts{ + TokenType: constants.WorkerAuthTokenType, + TokenDuration: tm.WorkerAuthTokenDuration, + Hostname: cl.Subject, + } + + tkn := new(library.Token) + + wt, err := tm.MintToken(wmto) + if err != nil { + retErr := fmt.Errorf("unable to generate auth token for worker %s: %w", input.GetHostname(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + tkn.SetToken(wt) + + c.JSON(http.StatusCreated, tkn) + } +} diff --git a/api/worker/delete.go b/api/worker/delete.go new file mode 100644 index 000000000..fa0ad5e65 --- /dev/null +++ b/api/worker/delete.go @@ -0,0 +1,70 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/router/middleware/worker" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation DELETE /api/v1/workers/{worker} workers DeleteWorker +// +// Delete a worker for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: worker +// description: Name of the worker +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully deleted of worker +// schema: +// type: string +// '500': +// description: Unable to delete worker +// schema: +// "$ref": "#/definitions/Error" + +// DeleteWorker represents the API handler to remove +// a worker from the configured backend. +func DeleteWorker(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + w := worker.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + "worker": w.GetHostname(), + }).Infof("deleting worker %s", w.GetHostname()) + + // send API call to remove the step + err := database.FromContext(c).DeleteWorker(w) + if err != nil { + retErr := fmt.Errorf("unable to delete worker %s: %w", w.GetHostname(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("worker %s deleted", w.GetHostname())) +} diff --git a/api/worker/doc.go b/api/worker/doc.go new file mode 100644 index 000000000..86da09ed7 --- /dev/null +++ b/api/worker/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package worker provides the worker handlers for the Vela API. +// +// Usage: +// +// import "github.com/go-vela/server/api/worker" +package worker diff --git a/api/worker/get.go b/api/worker/get.go new file mode 100644 index 000000000..88b323532 --- /dev/null +++ b/api/worker/get.go @@ -0,0 +1,69 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/router/middleware/worker" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/workers/{worker} workers GetWorker +// +// Retrieve a worker for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: worker +// description: Hostname of the worker +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the worker +// schema: +// "$ref": "#/definitions/Worker" +// '404': +// description: Unable to retrieve the worker +// schema: +// "$ref": "#/definitions/Error" + +// GetWorker represents the API handler to capture a +// worker from the configured backend. +func GetWorker(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + w := worker.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + "worker": w.GetHostname(), + }).Infof("reading worker %s", w.GetHostname()) + + w, err := database.FromContext(c).GetWorkerForHostname(w.GetHostname()) + if err != nil { + retErr := fmt.Errorf("unable to get workers: %w", err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + c.JSON(http.StatusOK, w) +} diff --git a/api/worker/list.go b/api/worker/list.go new file mode 100644 index 000000000..2587ba07f --- /dev/null +++ b/api/worker/list.go @@ -0,0 +1,62 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/sirupsen/logrus" +) + +// swagger:operation GET /api/v1/workers workers ListWorkers +// +// Retrieve a list of workers for the configured backend +// +// --- +// produces: +// - application/json +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully retrieved the list of workers +// schema: +// type: array +// items: +// "$ref": "#/definitions/Worker" +// '500': +// description: Unable to retrieve the list of workers +// schema: +// "$ref": "#/definitions/Error" + +// ListWorkers represents the API handler to capture a +// list of workers from the configured backend. +func ListWorkers(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Info("reading workers") + + w, err := database.FromContext(c).ListWorkers() + if err != nil { + retErr := fmt.Errorf("unable to get workers: %w", err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + c.JSON(http.StatusOK, w) +} diff --git a/api/worker/refresh.go b/api/worker/refresh.go new file mode 100644 index 000000000..cd4aa7ef3 --- /dev/null +++ b/api/worker/refresh.go @@ -0,0 +1,138 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/claims" + "github.com/go-vela/server/router/middleware/worker" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation POST /api/v1/workers/{worker}/refresh workers RefreshWorkerAuth +// +// Refresh authorization token for worker +// +// --- +// produces: +// - application/json +// parameters: +// - in: path +// name: worker +// description: Name of the worker +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully refreshed auth +// schema: +// "$ref": "#/definitions/Token" +// '400': +// description: Unable to refresh worker auth +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to refresh worker auth +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to refresh worker auth +// schema: +// "$ref": "#/definitions/Error" + +// Refresh represents the API handler to +// refresh the auth token for a worker. +func Refresh(c *gin.Context) { + // capture middleware values + w := worker.Retrieve(c) + cl := claims.Retrieve(c) + + // if we are not using a symmetric token, and the subject does not match the input, request should be denied + if !strings.EqualFold(cl.TokenType, constants.ServerWorkerTokenType) && !strings.EqualFold(cl.Subject, w.GetHostname()) { + retErr := fmt.Errorf("unable to refresh worker auth: claims subject %s does not match worker hostname %s", cl.Subject, w.GetHostname()) + + logrus.WithFields(logrus.Fields{ + "subject": cl.Subject, + "worker": w.GetHostname(), + }).Warnf("attempted refresh of worker %s using token from worker %s", w.GetHostname(), cl.Subject) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // set last checked in time + w.SetLastCheckedIn(time.Now().Unix()) + + // send API call to update the worker + err := database.FromContext(c).UpdateWorker(w) + if err != nil { + retErr := fmt.Errorf("unable to update worker %s: %w", w.GetHostname(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "worker": w.GetHostname(), + }).Infof("refreshing worker %s authentication", w.GetHostname()) + + switch cl.TokenType { + // if symmetric token configured, send back symmetric token + case constants.ServerWorkerTokenType: + if secret, ok := c.Value("secret").(string); ok { + tkn := new(library.Token) + tkn.SetToken(secret) + c.JSON(http.StatusOK, tkn) + + return + } + + retErr := fmt.Errorf("symmetric token provided but not configured in server") + util.HandleError(c, http.StatusBadRequest, retErr) + + return + // if worker auth / register token, send back auth token + case constants.WorkerAuthTokenType, constants.WorkerRegisterTokenType: + tm := c.MustGet("token-manager").(*token.Manager) + + wmto := &token.MintTokenOpts{ + TokenType: constants.WorkerAuthTokenType, + TokenDuration: tm.WorkerAuthTokenDuration, + Hostname: cl.Subject, + } + + tkn := new(library.Token) + + wt, err := tm.MintToken(wmto) + if err != nil { + retErr := fmt.Errorf("unable to generate auth token for worker %s: %w", w.GetHostname(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + tkn.SetToken(wt) + + c.JSON(http.StatusOK, tkn) + } +} diff --git a/api/worker/update.go b/api/worker/update.go new file mode 100644 index 000000000..b3a8d5130 --- /dev/null +++ b/api/worker/update.go @@ -0,0 +1,140 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/router/middleware/worker" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// swagger:operation PUT /api/v1/workers/{worker} workers UpdateWorker +// +// Update a worker for the configured backend +// +// --- +// produces: +// - application/json +// parameters: +// - in: body +// name: body +// description: Payload containing the worker to update +// required: true +// schema: +// "$ref": "#/definitions/Worker" +// - in: path +// name: worker +// description: Name of the worker +// required: true +// type: string +// security: +// - ApiKeyAuth: [] +// responses: +// '200': +// description: Successfully updated the worker +// schema: +// "$ref": "#/definitions/Worker" +// '400': +// description: Unable to update the worker +// schema: +// "$ref": "#/definitions/Error" +// '404': +// description: Unable to update the worker +// schema: +// "$ref": "#/definitions/Error" +// '500': +// description: Unable to update the worker +// schema: +// "$ref": "#/definitions/Error" + +// UpdateWorker represents the API handler to +// update a worker in the configured backend. +func UpdateWorker(c *gin.Context) { + // capture middleware values + u := user.Retrieve(c) + w := worker.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": u.GetName(), + "worker": w.GetHostname(), + }).Infof("updating worker %s", w.GetHostname()) + + // capture body from API request + input := new(library.Worker) + + err := c.Bind(input) + if err != nil { + retErr := fmt.Errorf("unable to decode JSON for worker %s: %w", w.GetHostname(), err) + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + if len(input.GetAddress()) > 0 { + // update address if set + w.SetAddress(input.GetAddress()) + } + + if len(input.GetRoutes()) > 0 { + // update routes if set + w.SetRoutes(input.GetRoutes()) + } + + if input.Active != nil { + // update active if set + w.SetActive(input.GetActive()) + } + + if input.RunningBuildIDs != nil { + // update runningBuildIDs if set + w.SetRunningBuildIDs(input.GetRunningBuildIDs()) + } + + if len(input.GetStatus()) > 0 { + // update status if set + w.SetStatus(input.GetStatus()) + } + + if input.GetLastStatusUpdateAt() > 0 { + // update lastStatusUpdateAt if set + w.SetLastStatusUpdateAt(input.GetLastStatusUpdateAt()) + } + + if input.GetLastBuildStartedAt() > 0 { + // update lastBuildStartedAt if set + w.SetLastBuildStartedAt(input.GetLastBuildStartedAt()) + } + + if input.GetLastBuildFinishedAt() > 0 { + // update lastBuildFinishedAt if set + w.SetLastBuildFinishedAt(input.GetLastBuildFinishedAt()) + } + + // send API call to update the worker + err = database.FromContext(c).UpdateWorker(w) + if err != nil { + retErr := fmt.Errorf("unable to update worker %s: %w", w.GetHostname(), err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // send API call to capture the updated worker + w, _ = database.FromContext(c).GetWorkerForHostname(w.GetHostname()) + + c.JSON(http.StatusOK, w) +} diff --git a/cmd/vela-server/database.go b/cmd/vela-server/database.go deleted file mode 100644 index cee4f9a83..000000000 --- a/cmd/vela-server/database.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package main - -import ( - "github.com/go-vela/server/database" - - "github.com/sirupsen/logrus" - - "github.com/urfave/cli/v2" -) - -// helper function to setup the database from the CLI arguments. -func setupDatabase(c *cli.Context) (database.Service, error) { - logrus.Debug("Creating database client from CLI configuration") - - // database configuration - _setup := &database.Setup{ - Driver: c.String("database.driver"), - Address: c.String("database.addr"), - CompressionLevel: c.Int("database.compression.level"), - ConnectionLife: c.Duration("database.connection.life"), - ConnectionIdle: c.Int("database.connection.idle"), - ConnectionOpen: c.Int("database.connection.open"), - EncryptionKey: c.String("database.encryption.key"), - SkipCreation: c.Bool("database.skip_creation"), - } - - // setup the database - // - // https://pkg.go.dev/github.com/go-vela/server/database?tab=doc#New - return database.New(_setup) -} diff --git a/cmd/vela-server/main.go b/cmd/vela-server/main.go index 31f0b4286..788f6c26b 100644 --- a/cmd/vela-server/main.go +++ b/cmd/vela-server/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -23,7 +23,7 @@ import ( _ "github.com/joho/godotenv/autoload" ) -// nolint: funlen // ignore function length due to flags +//nolint:funlen // ignore line length func main() { // capture application version information v := version.New() @@ -41,7 +41,6 @@ func main() { app.Name = "vela-server" app.Action = server app.Version = v.Semantic() - app.Flags = []cli.Flag{ &cli.StringFlag{ EnvVars: []string{"VELA_LOG_LEVEL", "LOG_LEVEL"}, @@ -76,6 +75,17 @@ func main() { Name: "vela-secret", Usage: "secret used for server <-> agent communication", }, + &cli.StringFlag{ + EnvVars: []string{"VELA_SERVER_PRIVATE_KEY"}, + Name: "vela-server-private-key", + Usage: "private key used for signing tokens", + }, + &cli.StringFlag{ + EnvVars: []string{"VELA_CLONE_IMAGE"}, + Name: "clone-image", + Usage: "the clone image to use for the injected clone step", + Value: "target/vela-git:v0.8.0@sha256:02de004ae9dbf184c70039cb9ce431c31d6e7580eb9e6ec64a97ebf108aa65cb", + }, &cli.StringSliceFlag{ EnvVars: []string{"VELA_REPO_ALLOWLIST"}, Name: "vela-repo-allowlist", @@ -85,9 +95,8 @@ func main() { &cli.BoolFlag{ EnvVars: []string{"VELA_DISABLE_WEBHOOK_VALIDATION"}, Name: "vela-disable-webhook-validation", - // nolint: lll // ignore long line length due to description - Usage: "determines whether or not webhook validation is disabled. useful for local development.", - Value: false, + Usage: "determines whether or not webhook validation is disabled. useful for local development.", + Value: false, }, &cli.BoolFlag{ EnvVars: []string{"VELA_ENABLE_SECURE_COOKIE"}, @@ -113,24 +122,44 @@ func main() { Usage: "override default build timeout (minutes)", Value: constants.BuildTimeoutDefault, }, - - // Security Flags - + &cli.StringSliceFlag{ + EnvVars: []string{"VELA_DEFAULT_REPO_EVENTS"}, + Name: "default-repo-events", + Usage: "override default events for newly activated repositories", + Value: cli.NewStringSlice(constants.EventPush), + }, + // Token Manager Flags &cli.DurationFlag{ - EnvVars: []string{"VELA_ACCESS_TOKEN_DURATION", "ACCESS_TOKEN_DURATION"}, - Name: "access-token-duration", - Usage: "sets the duration of the access token", + EnvVars: []string{"VELA_USER_ACCESS_TOKEN_DURATION", "USER_ACCESS_TOKEN_DURATION"}, + Name: "user-access-token-duration", + Usage: "sets the duration of the user access token", Value: 15 * time.Minute, }, &cli.DurationFlag{ - EnvVars: []string{"VELA_REFRESH_TOKEN_DURATION", "REFRESH_TOKEN_DURATION"}, - Name: "refresh-token-duration", - Usage: "sets the duration of the refresh token", + EnvVars: []string{"VELA_USER_REFRESH_TOKEN_DURATION", "USER_REFRESH_TOKEN_DURATION"}, + Name: "user-refresh-token-duration", + Usage: "sets the duration of the user refresh token", Value: 8 * time.Hour, }, - + &cli.DurationFlag{ + EnvVars: []string{"VELA_BUILD_TOKEN_BUFFER_DURATION", "BUILD_TOKEN_BUFFER_DURATION"}, + Name: "build-token-buffer-duration", + Usage: "sets the duration of the buffer for build token expiration based on repo build timeout", + Value: 5 * time.Minute, + }, + &cli.DurationFlag{ + EnvVars: []string{"VELA_WORKER_AUTH_TOKEN_DURATION", "WORKER_AUTH_TOKEN_DURATION"}, + Name: "worker-auth-token-duration", + Usage: "sets the duration of the worker auth token", + Value: 20 * time.Minute, + }, + &cli.DurationFlag{ + EnvVars: []string{"VELA_WORKER_REGISTER_TOKEN_DURATION", "WORKER_REGISTER_TOKEN_DURATION"}, + Name: "worker-register-token-duration", + Usage: "sets the duration of the worker register token", + Value: 1 * time.Minute, + }, // Compiler Flags - &cli.BoolFlag{ EnvVars: []string{"VELA_COMPILER_GITHUB", "COMPILER_GITHUB"}, Name: "github-driver", @@ -146,7 +175,6 @@ func main() { Name: "github-token", Usage: "github token, used by compiler, for pulling registry templates", }, - &cli.StringFlag{ EnvVars: []string{"VELA_MODIFICATION_ADDR", "MODIFICATION_ADDR"}, Name: "modification-addr", @@ -155,53 +183,68 @@ func main() { &cli.StringFlag{ EnvVars: []string{"VELA_MODIFICATION_SECRET", "MODIFICATION_SECRET"}, Name: "modification-secret", - // nolint: lll // ignore long line length due to description - Usage: "modification secret, used by compiler, secret to allow connectivity between compiler and modification endpoint", + Usage: "modification secret, used by compiler, secret to allow connectivity between compiler and modification endpoint", }, &cli.DurationFlag{ EnvVars: []string{"VELA_MODIFICATION_TIMEOUT", "MODIFICATION_TIMEOUT"}, Name: "modification-timeout", - // nolint: lll // ignore long line length due to description - Usage: "modification timeout, used by compiler, duration that the modification http request will timeout after", - Value: 8 * time.Second, + Usage: "modification timeout, used by compiler, duration that the modification http request will timeout after", + Value: 8 * time.Second, }, &cli.IntFlag{ EnvVars: []string{"VELA_MODIFICATION_RETRIES", "MODIFICATION_RETRIES"}, Name: "modification-retries", - // nolint: lll // ignore long line length due to description - Usage: "modification retries, used by compiler, number of http requires that the modification http request will fail after", - Value: 5, + Usage: "modification retries, used by compiler, number of http requires that the modification http request will fail after", + Value: 5, + }, + &cli.IntFlag{ + EnvVars: []string{"VELA_MAX_TEMPLATE_DEPTH", "MAX_TEMPLATE_DEPTH"}, + Name: "max-template-depth", + Usage: "max template depth, used by compiler, maximum number of templates that can be called in a template chain", + Value: 3, }, - &cli.DurationFlag{ EnvVars: []string{"VELA_WORKER_ACTIVE_INTERVAL", "WORKER_ACTIVE_INTERVAL"}, Name: "worker-active-interval", Usage: "interval at which workers will show as active within the /metrics endpoint", Value: 5 * time.Minute, }, + // schedule flags + &cli.DurationFlag{ + EnvVars: []string{"VELA_SCHEDULE_MINIMUM_FREQUENCY", "SCHEDULE_MINIMUM_FREQUENCY"}, + Name: "schedule-minimum-frequency", + Usage: "minimum time allowed between each build triggered for a schedule", + Value: 1 * time.Hour, + }, + &cli.DurationFlag{ + EnvVars: []string{"VELA_SCHEDULE_INTERVAL", "SCHEDULE_INTERVAL"}, + Name: "schedule-interval", + Usage: "interval at which schedules will be processed by the server to trigger builds", + Value: 5 * time.Minute, + }, + &cli.StringSliceFlag{ + EnvVars: []string{"VELA_SCHEDULE_ALLOWLIST"}, + Name: "vela-schedule-allowlist", + Usage: "limit which repos can be utilize the schedule feature within the system", + Value: &cli.StringSlice{}, + }, } - - // Database Flags - + // Add Database Flags app.Flags = append(app.Flags, database.Flags...) - // Queue Flags - + // Add Queue Flags app.Flags = append(app.Flags, queue.Flags...) - // Secret Flags - + // Add Secret Flags app.Flags = append(app.Flags, secret.Flags...) - // Source Flags - + // Add Source Flags app.Flags = append(app.Flags, scm.Flags...) // set logrus to log in JSON format logrus.SetFormatter(&logrus.JSONFormatter{}) - err = app.Run(os.Args) - if err != nil { + if err = app.Run(os.Args); err != nil { logrus.Fatal(err) } } diff --git a/cmd/vela-server/metadata.go b/cmd/vela-server/metadata.go index 93f821d0c..4ce86ea31 100644 --- a/cmd/vela-server/metadata.go +++ b/cmd/vela-server/metadata.go @@ -98,7 +98,7 @@ func metadataSource(c *cli.Context) (*types.Source, error) { // helper function to capture the Vela metadata from the CLI arguments. // -// nolint: unparam // ignore unparam for now +//nolint:unparam // ignore unparam for now func metadataVela(c *cli.Context) (*types.Vela, error) { logrus.Trace("Creating Vela metadata from CLI configuration") diff --git a/cmd/vela-server/queue.go b/cmd/vela-server/queue.go index 677137e18..edd703e69 100644 --- a/cmd/vela-server/queue.go +++ b/cmd/vela-server/queue.go @@ -18,11 +18,12 @@ func setupQueue(c *cli.Context) (queue.Service, error) { // queue configuration _setup := &queue.Setup{ - Driver: c.String("queue.driver"), - Address: c.String("queue.addr"), - Cluster: c.Bool("queue.cluster"), - Routes: c.StringSlice("queue.routes"), - Timeout: c.Duration("queue.pop.timeout"), + Driver: c.String("queue.driver"), + Address: c.String("queue.addr"), + Cluster: c.Bool("queue.cluster"), + Routes: c.StringSlice("queue.routes"), + Timeout: c.Duration("queue.pop.timeout"), + PrivateKey: c.String("queue.private-key"), } // setup the queue diff --git a/cmd/vela-server/schedule.go b/cmd/vela-server/schedule.go new file mode 100644 index 000000000..b65723177 --- /dev/null +++ b/cmd/vela-server/schedule.go @@ -0,0 +1,400 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package main + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/adhocore/gronx" + "github.com/go-vela/server/api/build" + "github.com/go-vela/server/compiler" + "github.com/go-vela/server/database" + "github.com/go-vela/server/queue" + "github.com/go-vela/server/scm" + "github.com/go-vela/types" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" + "github.com/sirupsen/logrus" + + "k8s.io/apimachinery/pkg/util/wait" +) + +const ( + scheduleErr = "unable to trigger build for schedule" + + scheduleWait = "waiting to trigger build for schedule" +) + +func processSchedules(ctx context.Context, start time.Time, compiler compiler.Engine, database database.Interface, metadata *types.Metadata, queue queue.Service, scm scm.Service) error { + logrus.Infof("processing active schedules to create builds") + + // send API call to capture the list of active schedules + schedules, err := database.ListActiveSchedules(ctx) + if err != nil { + return err + } + + // iterate through the list of active schedules + for _, s := range schedules { + // sleep for 1s - 2s before processing the active schedule + // + // This should prevent multiple servers from processing a schedule at the same time by + // leveraging a base duration along with a standard deviation of randomness a.k.a. + // "jitter". To create the jitter, we use a base duration of 1s with a scale factor of 1.0. + time.Sleep(wait.Jitter(time.Second, 1.0)) + + // send API call to capture the schedule + // + // This is needed to ensure we are not dealing with a stale schedule since we fetch + // all schedules once and iterate through that list which can take a significant + // amount of time to get to the end of the list. + schedule, err := database.GetSchedule(ctx, s.GetID()) + if err != nil { + logrus.WithError(err).Warnf("%s %s", scheduleErr, schedule.GetName()) + + continue + } + + // ignore triggering a build if the schedule is no longer active + if !schedule.GetActive() { + logrus.Tracef("skipping to trigger build for inactive schedule %s", schedule.GetName()) + + continue + } + + // capture the last time a build was triggered for the schedule in UTC + scheduled := time.Unix(schedule.GetScheduledAt(), 0).UTC() + + // capture the previous occurrence of the entry rounded to the nearest whole interval + // + // i.e. if it's 4:02 on five minute intervals, this will be 4:00 + prevTime, err := gronx.PrevTick(schedule.GetEntry(), true) + if err != nil { + logrus.WithError(err).Warnf("%s %s", scheduleErr, schedule.GetName()) + + continue + } + + // capture the next occurrence of the entry after the last schedule rounded to the nearest whole interval + // + // i.e. if it's 4:02 on five minute intervals, this will be 4:05 + nextTime, err := gronx.NextTickAfter(schedule.GetEntry(), scheduled, true) + if err != nil { + logrus.WithError(err).Warnf("%s %s", scheduleErr, schedule.GetName()) + + continue + } + + // check if we should wait to trigger a build for the schedule + // + // The current time must be after the next occurrence of the schedule. + if !time.Now().After(nextTime) { + logrus.Tracef("%s %s: current time not past next occurrence", scheduleWait, schedule.GetName()) + + continue + } + + // check if we should wait to trigger a build for the schedule + // + // The previous occurrence of the schedule must be after the starting time of processing schedules. + if !prevTime.After(start) { + logrus.Tracef("%s %s: previous occurence not after starting point", scheduleWait, schedule.GetName()) + + continue + } + + // update the scheduled_at field with the current timestamp + // + // This should help prevent multiple servers from processing a schedule at the same time + // by updating the schedule with a new timestamp to reflect the current state. + schedule.SetScheduledAt(time.Now().UTC().Unix()) + + // send API call to update schedule for ensuring scheduled_at field is set + _, err = database.UpdateSchedule(ctx, schedule, false) + if err != nil { + logrus.WithError(err).Warnf("%s %s", scheduleErr, schedule.GetName()) + + continue + } + + // process the schedule and trigger a new build + err = processSchedule(ctx, schedule, compiler, database, metadata, queue, scm) + if err != nil { + logrus.WithError(err).Warnf("%s %s", scheduleErr, schedule.GetName()) + + continue + } + } + + return nil +} + +//nolint:funlen // ignore function length and number of statements +func processSchedule(ctx context.Context, s *library.Schedule, compiler compiler.Engine, database database.Interface, metadata *types.Metadata, queue queue.Service, scm scm.Service) error { + // send API call to capture the repo for the schedule + r, err := database.GetRepo(ctx, s.GetRepoID()) + if err != nil { + return fmt.Errorf("unable to fetch repo: %w", err) + } + + logrus.Tracef("processing schedule %s/%s", r.GetFullName(), s.GetName()) + + // check if the repo is active + if !r.GetActive() { + return fmt.Errorf("repo %s is not active", r.GetFullName()) + } + + // check if the repo has a valid owner + if r.GetUserID() == 0 { + return fmt.Errorf("repo %s does not have a valid owner", r.GetFullName()) + } + + // send API call to capture the owner for the repo + u, err := database.GetUser(r.GetUserID()) + if err != nil { + return fmt.Errorf("unable to get owner for repo %s: %w", r.GetFullName(), err) + } + + // send API call to confirm repo owner has at least write access to repo + _, err = scm.RepoAccess(u, u.GetToken(), r.GetOrg(), r.GetName()) + if err != nil { + return fmt.Errorf("%s does not have at least write access for repo %s", u.GetName(), r.GetFullName()) + } + + // create SQL filters for querying pending and running builds for repo + filters := map[string]interface{}{ + "status": []string{constants.StatusPending, constants.StatusRunning}, + } + + // send API call to capture the number of pending or running builds for the repo + builds, err := database.CountBuildsForRepo(context.TODO(), r, filters) + if err != nil { + return fmt.Errorf("unable to get count of builds for repo %s: %w", r.GetFullName(), err) + } + + // check if the number of pending and running builds exceeds the limit for the repo + if builds >= r.GetBuildLimit() { + return fmt.Errorf("repo %s has excceded the concurrent build limit of %d", r.GetFullName(), r.GetBuildLimit()) + } + + // send API call to capture the commit sha for the branch + _, commit, err := scm.GetBranch(u, r, s.GetBranch()) + if err != nil { + return fmt.Errorf("failed to get commit for repo %s on %s branch: %w", r.GetFullName(), r.GetBranch(), err) + } + + url := strings.TrimSuffix(r.GetClone(), ".git") + + b := new(library.Build) + b.SetAuthor(s.GetCreatedBy()) + b.SetBranch(s.GetBranch()) + b.SetClone(r.GetClone()) + b.SetCommit(commit) + b.SetDeploy(s.GetName()) + b.SetEvent(constants.EventSchedule) + b.SetMessage(fmt.Sprintf("triggered for %s schedule with %s entry", s.GetName(), s.GetEntry())) + b.SetRef(fmt.Sprintf("refs/heads/%s", b.GetBranch())) + b.SetRepoID(r.GetID()) + b.SetSender(s.GetUpdatedBy()) + b.SetSource(fmt.Sprintf("%s/tree/%s", url, b.GetBranch())) + b.SetStatus(constants.StatusPending) + b.SetTitle(fmt.Sprintf("%s received from %s", constants.EventSchedule, url)) + + // populate the build link if a web address is provided + if len(metadata.Vela.WebAddress) > 0 { + b.SetLink(fmt.Sprintf("%s/%s/%d", metadata.Vela.WebAddress, r.GetFullName(), b.GetNumber())) + } + + var ( + // variable to store the raw pipeline configuration + config []byte + // variable to store executable pipeline + p *pipeline.Build + // variable to store pipeline configuration + pipeline *library.Pipeline + // variable to control number of times to retry processing pipeline + retryLimit = 5 + // variable to store the pipeline type for the repository + pipelineType = r.GetPipelineType() + ) + + // implement a loop to process asynchronous operations with a retry limit + // + // Some operations taken during this workflow can lead to race conditions failing to successfully process + // the request. This logic ensures we attempt our best efforts to handle these cases gracefully. + for i := 0; i < retryLimit; i++ { + logrus.Debugf("compilation loop - attempt %d", i+1) + // check if we're on the first iteration of the loop + if i > 0 { + // incrementally sleep in between retries + time.Sleep(time.Duration(i) * time.Second) + } + + // send API call to attempt to capture the pipeline + pipeline, err = database.GetPipelineForRepo(context.TODO(), b.GetCommit(), r) + if err != nil { // assume the pipeline doesn't exist in the database yet + // send API call to capture the pipeline configuration file + config, err = scm.ConfigBackoff(u, r, b.GetCommit()) + if err != nil { + return fmt.Errorf("unable to get pipeline config for %s/%s: %w", r.GetFullName(), b.GetCommit(), err) + } + } else { + config = pipeline.GetData() + } + + // send API call to capture repo for the counter (grabbing repo again to ensure counter is correct) + r, err = database.GetRepoForOrg(ctx, r.GetOrg(), r.GetName()) + if err != nil { + err = fmt.Errorf("unable to get repo %s: %w", r.GetFullName(), err) + + // check if the retry limit has been exceeded + if i < retryLimit-1 { + logrus.WithError(err).Warningf("retrying #%d", i+1) + + // continue to the next iteration of the loop + continue + } + + return err + } + + // set the build numbers based off repo counter + b.SetNumber(r.GetCounter() + 1) + // set the parent equal to the current repo counter + b.SetParent(r.GetCounter()) + // check if the parent is set to 0 + if b.GetParent() == 0 { + // parent should be "1" if it's the first build ran + b.SetParent(1) + } + r.SetCounter(r.GetCounter() + 1) + + // set the build link if a web address is provided + if len(metadata.Vela.WebAddress) > 0 { + b.SetLink(fmt.Sprintf("%s/%s/%d", metadata.Vela.WebAddress, r.GetFullName(), b.GetNumber())) + } + + // ensure we use the expected pipeline type when compiling + // + // The pipeline type for a repo can change at any time which can break compiling + // existing pipelines in the system for that repo. To account for this, we update + // the repo pipeline type to match what was defined for the existing pipeline + // before compiling. After we're done compiling, we reset the pipeline type. + if len(pipeline.GetType()) > 0 { + r.SetPipelineType(pipeline.GetType()) + } + + var compiled *library.Pipeline + // parse and compile the pipeline configuration file + p, compiled, err = compiler. + Duplicate(). + WithBuild(b). + WithCommit(b.GetCommit()). + WithMetadata(metadata). + WithRepo(r). + WithUser(u). + Compile(config) + if err != nil { + return fmt.Errorf("unable to compile pipeline config for %s/%s: %w", r.GetFullName(), b.GetCommit(), err) + } + + // reset the pipeline type for the repo + // + // The pipeline type for a repo can change at any time which can break compiling + // existing pipelines in the system for that repo. To account for this, we update + // the repo pipeline type to match what was defined for the existing pipeline + // before compiling. After we're done compiling, we reset the pipeline type. + r.SetPipelineType(pipelineType) + + // skip the build if only the init or clone steps are found + skip := build.SkipEmptyBuild(p) + if skip != "" { + return nil + } + + // check if the pipeline did not already exist in the database + if pipeline == nil { + pipeline = compiled + pipeline.SetRepoID(r.GetID()) + pipeline.SetCommit(b.GetCommit()) + pipeline.SetRef(b.GetRef()) + + // send API call to create the pipeline + pipeline, err = database.CreatePipeline(context.TODO(), pipeline) + if err != nil { + err = fmt.Errorf("failed to create pipeline for %s: %w", r.GetFullName(), err) + + // check if the retry limit has been exceeded + if i < retryLimit-1 { + logrus.WithError(err).Warningf("retrying #%d", i+1) + + // continue to the next iteration of the loop + continue + } + + return err + } + } + + b.SetPipelineID(pipeline.GetID()) + + // create the objects from the pipeline in the database + // TODO: + // - if a build gets created and something else fails midway, + // the next loop will attempt to create the same build, + // using the same Number and thus create a constraint + // conflict; consider deleting the partially created + // build object in the database + err = build.PlanBuild(context.TODO(), database, p, b, r) + if err != nil { + // check if the retry limit has been exceeded + if i < retryLimit-1 { + logrus.WithError(err).Warningf("retrying #%d", i+1) + + // reset fields set by cleanBuild for retry + b.SetError("") + b.SetStatus(constants.StatusPending) + b.SetFinished(0) + + // continue to the next iteration of the loop + continue + } + + return err + } + + // break the loop because everything was successful + break + } // end of retry loop + + // send API call to update repo for ensuring counter is incremented + r, err = database.UpdateRepo(ctx, r) + if err != nil { + return fmt.Errorf("unable to update repo %s: %w", r.GetFullName(), err) + } + + // send API call to capture the triggered build + b, err = database.GetBuildForRepo(context.TODO(), r, b.GetNumber()) + if err != nil { + return fmt.Errorf("unable to get new build %s/%d: %w", r.GetFullName(), b.GetNumber(), err) + } + + // publish the build to the queue + go build.PublishToQueue( + context.TODO(), + queue, + database, + p, + b, + r, + u, + ) + + return nil +} diff --git a/cmd/vela-server/secret.go b/cmd/vela-server/secret.go index ee780dce0..56e6c0572 100644 --- a/cmd/vela-server/secret.go +++ b/cmd/vela-server/secret.go @@ -15,7 +15,7 @@ import ( ) // helper function to setup the secrets engines from the CLI arguments. -func setupSecrets(c *cli.Context, d database.Service) (map[string]secret.Service, error) { +func setupSecrets(c *cli.Context, d database.Interface) (map[string]secret.Service, error) { logrus.Debug("Creating secret clients from CLI configuration") secrets := make(map[string]secret.Service) diff --git a/cmd/vela-server/server.go b/cmd/vela-server/server.go index 9de6747c6..cb9f0edd1 100644 --- a/cmd/vela-server/server.go +++ b/cmd/vela-server/server.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -9,19 +9,22 @@ import ( "fmt" "net/http" "net/url" + "os" + "os/signal" + "syscall" "time" + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" "github.com/go-vela/server/router" "github.com/go-vela/server/router/middleware" - - "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" - "gopkg.in/tomb.v2" + "golang.org/x/sync/errgroup" + + "k8s.io/apimachinery/pkg/util/wait" ) -// nolint: funlen // ignore function length func server(c *cli.Context) error { // validate all input err := validate(c) @@ -59,7 +62,7 @@ func server(c *cli.Context) error { return err } - database, err := setupDatabase(c) + database, err := database.FromCLIContext(c) if err != nil { return err } @@ -87,13 +90,15 @@ func server(c *cli.Context) error { router := router.Load( middleware.Compiler(compiler), middleware.Database(database), - middleware.Logger(logrus.StandardLogger(), time.RFC3339, true), + middleware.Logger(logrus.StandardLogger(), time.RFC3339), middleware.Metadata(metadata), + middleware.TokenManager(setupTokenManager(c)), middleware.Queue(queue), middleware.RequestVersion, middleware.Secret(c.String("vela-secret")), middleware.Secrets(secrets), middleware.Scm(scm), + middleware.QueueSigningPrivateKey(c.String("queue.private-key")), middleware.Allowlist(c.StringSlice("vela-repo-allowlist")), middleware.DefaultBuildLimit(c.Int64("default-build-limit")), middleware.DefaultTimeout(c.Int64("default-build-timeout")), @@ -101,6 +106,9 @@ func server(c *cli.Context) error { middleware.WebhookValidation(!c.Bool("vela-disable-webhook-validation")), middleware.SecureCookie(c.Bool("vela-enable-secure-cookie")), middleware.Worker(c.Duration("worker-active-interval")), + middleware.DefaultRepoEvents(c.StringSlice("default-repo-events")), + middleware.AllowlistSchedule(c.StringSlice("vela-schedule-allowlist")), + middleware.ScheduleFrequency(c.Duration("schedule-minimum-frequency")), ) addr, err := url.Parse(c.String("server-addr")) @@ -108,43 +116,99 @@ func server(c *cli.Context) error { return err } - var tomb tomb.Tomb - // start http server - tomb.Go(func() error { - port := addr.Port() - - // check if a port is part of the address - if len(port) == 0 { - port = c.String("server-port") - } + port := addr.Port() + // check if a port is part of the address + if len(port) == 0 { + port = c.String("server-port") + } - // gin expects the address to be ":" ie ":8080" - srv := &http.Server{Addr: fmt.Sprintf(":%s", port), Handler: router} + // gin expects the address to be ":" ie ":8080" + srv := &http.Server{ + Addr: fmt.Sprintf(":%s", port), + Handler: router, + ReadHeaderTimeout: 60 * time.Second, + } - logrus.Infof("running server on %s", addr.Host) - go func() { - logrus.Info("Starting HTTP server...") - err := srv.ListenAndServe() + // create the context for controlling the worker subprocesses + ctx, done := context.WithCancel(context.Background()) + // create the errgroup for managing worker subprocesses + // + // https://pkg.go.dev/golang.org/x/sync/errgroup?tab=doc#Group + g, gctx := errgroup.WithContext(ctx) + + // spawn goroutine to check for signals to gracefully shutdown + g.Go(func() error { + signalChannel := make(chan os.Signal, 1) + signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) + + select { + case sig := <-signalChannel: + logrus.Infof("received signal: %s", sig) + err := srv.Shutdown(ctx) if err != nil { - tomb.Kill(err) + logrus.Error(err) } - }() - - // nolint: gosimple // ignore this for now - for { - select { - case <-tomb.Dying(): - logrus.Info("Stopping HTTP server...") - return srv.Shutdown(context.Background()) + done() + case <-gctx.Done(): + logrus.Info("closing signal goroutine") + err := srv.Shutdown(ctx) + if err != nil { + logrus.Error(err) } + return gctx.Err() } + + return nil }) - // Wait for stuff and watch for errors - err = tomb.Wait() - if err != nil { + // spawn goroutine for starting the server + g.Go(func() error { + logrus.Infof("starting server on %s", addr.Host) + err = srv.ListenAndServe() + if err != nil { + // log a message indicating the failure of the server + logrus.Errorf("failing server: %v", err) + } + return err - } + }) + + // spawn goroutine for starting the scheduler + g.Go(func() error { + logrus.Info("starting scheduler") + for { + // track the starting time for when the server begins processing schedules + // + // This will be used to control which schedules will have a build triggered based + // off the configured entry and last time a build was triggered for the schedule. + start := time.Now().UTC() + + // capture the interval of time to wait before processing schedules + // + // We need to sleep for some amount of time before we attempt to process schedules + // setup in the database. Since the schedule interval is configurable, we use that + // as the base duration to determine how long to sleep for. + interval := c.Duration("schedule-interval") + + // This should prevent multiple servers from processing schedules at the same time by + // leveraging a base duration along with a standard deviation of randomness a.k.a. + // "jitter". To create the jitter, we use the configured schedule interval duration + // along with a scale factor of 0.5. + jitter := wait.Jitter(interval, 0.5) + + logrus.Infof("sleeping for %v before scheduling builds", jitter) + // sleep for a duration of time before processing schedules + time.Sleep(jitter) + + err = processSchedules(ctx, start, compiler, database, metadata, queue, scm) + if err != nil { + logrus.WithError(err).Warn("unable to process schedules") + } else { + logrus.Trace("successfully processed schedules") + } + } + }) - return tomb.Err() + // wait for errors from server subprocesses + return g.Wait() } diff --git a/cmd/vela-server/token.go b/cmd/vela-server/token.go new file mode 100644 index 000000000..298ef283a --- /dev/null +++ b/cmd/vela-server/token.go @@ -0,0 +1,32 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package main + +import ( + "github.com/golang-jwt/jwt/v5" + + "github.com/sirupsen/logrus" + + "github.com/urfave/cli/v2" + + "github.com/go-vela/server/internal/token" +) + +// helper function to setup the tokenmanager from the CLI arguments. +func setupTokenManager(c *cli.Context) *token.Manager { + logrus.Debug("Creating token manager from CLI configuration") + + tm := &token.Manager{ + PrivateKey: c.String("vela-server-private-key"), + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: c.Duration("user-access-token-duration"), + UserRefreshTokenDuration: c.Duration("user-refresh-token-duration"), + BuildTokenBufferDuration: c.Duration("build-token-buffer-duration"), + WorkerAuthTokenDuration: c.Duration("worker-auth-token-duration"), + WorkerRegisterTokenDuration: c.Duration("worker-register-token-duration"), + } + + return tm +} diff --git a/cmd/vela-server/validate.go b/cmd/vela-server/validate.go index 4098b6a42..cac3b0424 100644 --- a/cmd/vela-server/validate.go +++ b/cmd/vela-server/validate.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -8,6 +8,8 @@ import ( "fmt" "strings" + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -31,8 +33,6 @@ func validate(c *cli.Context) error { } // helper function to validate the core CLI configuration. -// -// nolint:lll // ignoring line length check to avoid breaking up error messages func validateCore(c *cli.Context) error { logrus.Trace("Validating core CLI configuration") @@ -48,8 +48,12 @@ func validateCore(c *cli.Context) error { return fmt.Errorf("server-addr (VELA_ADDR or VELA_HOST) flag must not have trailing slash") } - if len(c.String("vela-secret")) == 0 { - return fmt.Errorf("vela-secret (VELA_SECRET) flag is not properly configured") + if len(c.String("clone-image")) == 0 { + return fmt.Errorf("clone-image (VELA_CLONE_IMAGE) flag is not properly configured") + } + + if len(c.String("vela-server-private-key")) == 0 { + return fmt.Errorf("vela-server-private-key (VELA_SERVER_PRIVATE_KEY) flag is not properly configured") } if len(c.String("webui-addr")) == 0 { @@ -68,8 +72,12 @@ func validateCore(c *cli.Context) error { } } - if c.Duration("refresh-token-duration").Seconds() <= c.Duration("access-token-duration").Seconds() { - return fmt.Errorf("refresh-token-duration (VELA_REFRESH_TOKEN_DURATION) must be larger than the access-token-duration (VELA_ACCESS_TOKEN_DURATION)") + if c.Duration("user-refresh-token-duration").Seconds() <= c.Duration("user-access-token-duration").Seconds() { + return fmt.Errorf("user-refresh-token-duration (VELA_USER_REFRESH_TOKEN_DURATION) must be larger than the user-access-token-duration (VELA_USER_ACCESS_TOKEN_DURATION)") + } + + if c.Duration("build-token-buffer-duration").Seconds() < 0 { + return fmt.Errorf("build-token-buffer-duration (VELA_BUILD_TOKEN_BUFFER_DURATION) must not be a negative time value") } if c.Int64("default-build-limit") == 0 { @@ -80,12 +88,22 @@ func validateCore(c *cli.Context) error { return fmt.Errorf("max-build-limit (VELA_MAX_BUILD_LIMIT) flag must be greater than 0") } + for _, event := range c.StringSlice("default-repo-events") { + switch event { + case constants.EventPull: + case constants.EventPush: + case constants.EventDeploy: + case constants.EventTag: + case constants.EventComment: + default: + return fmt.Errorf("default-repo-events (VELA_DEFAULT_REPO_EVENTS) has the unsupported value of %s", event) + } + } + return nil } // helper function to validate the compiler CLI configuration. -// -// nolint:lll // ignoring line length check to avoid breaking up error messages func validateCompiler(c *cli.Context) error { logrus.Trace("Validating compiler CLI configuration") @@ -99,5 +117,9 @@ func validateCompiler(c *cli.Context) error { } } + if c.Int("max-template-depth") < 1 { + return fmt.Errorf("max-template-depth (VELA_MAX_TEMPLATE_DEPTH) or (MAX_TEMPLATE_DEPTH) flag must be greater than 0") + } + return nil } diff --git a/compiler/context.go b/compiler/context.go index 41196c22a..2a5622628 100644 --- a/compiler/context.go +++ b/compiler/context.go @@ -54,7 +54,7 @@ func FromGinContext(c *gin.Context) Engine { func WithContext(c context.Context, e Engine) context.Context { // set the compiler Engine in the context.Context // - // nolint: revive,staticcheck // ignore using string with context value + //nolint:revive,staticcheck // ignore using string with context value return context.WithValue(c, key, e) } diff --git a/compiler/context_test.go b/compiler/context_test.go index 97dd383d2..e5b340de6 100644 --- a/compiler/context_test.go +++ b/compiler/context_test.go @@ -22,7 +22,7 @@ func TestCompiler_FromContext(t *testing.T) { want Engine }{ { - // nolint: staticcheck // ignore using string with context value + //nolint:staticcheck, revive // ignore using string with context value context: context.WithValue(context.Background(), key, _engine), want: _engine, }, @@ -31,7 +31,7 @@ func TestCompiler_FromContext(t *testing.T) { want: nil, }, { - // nolint: staticcheck // ignore using string with context value + //nolint:staticcheck, revive // ignore using string with context value context: context.WithValue(context.Background(), key, "foo"), want: nil, }, @@ -92,7 +92,7 @@ func TestCompiler_WithContext(t *testing.T) { // setup types var _engine Engine - // nolint: staticcheck // ignore using string with context value + //nolint:staticcheck, revive // ignore using string with context value want := context.WithValue(context.Background(), key, _engine) // run test diff --git a/compiler/doc.go b/compiler/doc.go index d86fb589d..0e6e12e57 100644 --- a/compiler/doc.go +++ b/compiler/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/compiler" +// import "github.com/go-vela/server/compiler" package compiler diff --git a/compiler/engine.go b/compiler/engine.go index 86c0c7b37..0296c99d4 100644 --- a/compiler/engine.go +++ b/compiler/engine.go @@ -20,7 +20,12 @@ type Engine interface { // Compile defines a function that produces an executable // representation of a pipeline from an object. This calls // Parse internally to convert the object to a yaml configuration. - Compile(interface{}) (*pipeline.Build, error) + Compile(interface{}) (*pipeline.Build, *library.Pipeline, error) + + // CompileLite defines a function that produces an light executable + // representation of a pipeline from an object. This calls + // Parse internally to convert the object to a yaml configuration. + CompileLite(interface{}, bool, bool) (*yaml.Build, *library.Pipeline, error) // Duplicate defines a function that // creates a clone of the Engine. @@ -28,7 +33,7 @@ type Engine interface { // Parse defines a function that converts // an object to a yaml configuration. - Parse(interface{}) (*yaml.Build, error) + Parse(interface{}, string, *yaml.Template) (*yaml.Build, []byte, error) // ParseRaw defines a function that converts // an object to a string. @@ -66,12 +71,10 @@ type Engine interface { // ExpandStages defines a function that injects the template // for each templated step in every stage in a yaml configuration. - // nolint: lll // ignore long line length due to return args - ExpandStages(*yaml.Build, map[string]*yaml.Template) (yaml.StageSlice, yaml.SecretSlice, yaml.ServiceSlice, raw.StringSliceMap, error) + ExpandStages(*yaml.Build, map[string]*yaml.Template, *pipeline.RuleData) (*yaml.Build, error) // ExpandSteps defines a function that injects the template - // for each templated step in a yaml configuration. - // nolint: lll // ignore long line length due to return args - ExpandSteps(*yaml.Build, map[string]*yaml.Template) (yaml.StepSlice, yaml.SecretSlice, yaml.ServiceSlice, raw.StringSliceMap, error) + // for each templated step in a yaml configuration with the provided template depth. + ExpandSteps(*yaml.Build, map[string]*yaml.Template, *pipeline.RuleData, int) (*yaml.Build, error) // Init Compiler Interface Functions @@ -118,12 +121,18 @@ type Engine interface { // WithComment defines a function that sets // the comment in the Engine. WithComment(string) Engine + // WithCommit defines a function that sets + // the commit in the Engine. + WithCommit(string) Engine // WithFiles defines a function that sets // the changeset files in the Engine. WithFiles([]string) Engine // WithLocal defines a function that sets // the compiler local field in the Engine. WithLocal(bool) Engine + // WithLocalTemplates defines a function that sets + // the compiler local templates field in the Engine. + WithLocalTemplates([]string) Engine // WithMetadata defines a function that sets // the compiler Metadata type in the Engine. WithMetadata(*types.Metadata) Engine diff --git a/compiler/native/clone.go b/compiler/native/clone.go index f62f2da6f..ffc14393a 100644 --- a/compiler/native/clone.go +++ b/compiler/native/clone.go @@ -10,8 +10,6 @@ import ( ) const ( - // default image for clone process. - cloneImage = "target/vela-git:v0.4.0" // default name for clone stage. cloneStageName = "clone" // default name for clone step. @@ -34,7 +32,7 @@ func (c *client) CloneStage(p *yaml.Build) (*yaml.Build, error) { Steps: yaml.StepSlice{ &yaml.Step{ Detach: false, - Image: cloneImage, + Image: c.CloneImage, Name: cloneStepName, Privileged: false, Pull: constants.PullNotPresent, @@ -67,7 +65,7 @@ func (c *client) CloneStep(p *yaml.Build) (*yaml.Build, error) { // create new clone step clone := &yaml.Step{ Detach: false, - Image: cloneImage, + Image: c.CloneImage, Name: cloneStepName, Privileged: false, Pull: constants.PullNotPresent, diff --git a/compiler/native/clone_test.go b/compiler/native/clone_test.go index fd0f137d2..f481ee4c3 100644 --- a/compiler/native/clone_test.go +++ b/compiler/native/clone_test.go @@ -13,9 +13,12 @@ import ( "github.com/urfave/cli/v2" ) +const defaultCloneImage = "target/vela-git:latest" + func TestNative_CloneStage(t *testing.T) { // setup types set := flag.NewFlagSet("test", 0) + set.String("clone-image", defaultCloneImage, "doc") c := cli.NewContext(nil, set, nil) str := "foo" @@ -53,7 +56,7 @@ func TestNative_CloneStage(t *testing.T) { Name: "clone", Steps: yaml.StepSlice{ &yaml.Step{ - Image: "target/vela-git:v0.4.0", + Image: defaultCloneImage, Name: "clone", Pull: "not_present", }, @@ -113,6 +116,7 @@ func TestNative_CloneStage(t *testing.T) { func TestNative_CloneStep(t *testing.T) { // setup types set := flag.NewFlagSet("test", 0) + set.String("clone-image", defaultCloneImage, "doc") c := cli.NewContext(nil, set, nil) str := "foo" @@ -142,7 +146,7 @@ func TestNative_CloneStep(t *testing.T) { Version: "v1", Steps: yaml.StepSlice{ &yaml.Step{ - Image: "target/vela-git:v0.4.0", + Image: defaultCloneImage, Name: "clone", Pull: "not_present", }, diff --git a/compiler/native/compile.go b/compiler/native/compile.go index 1ab2ccd8d..d106fa480 100644 --- a/compiler/native/compile.go +++ b/compiler/native/compile.go @@ -9,11 +9,13 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "strings" "time" + "github.com/go-vela/types/constants" + yml "github.com/buildkite/yaml" "github.com/go-vela/types/library" @@ -39,154 +41,265 @@ type ModifyResponse struct { } // Compile produces an executable pipeline from a yaml configuration. -// -// nolint: gocyclo,funlen // ignore function length due to comments -func (c *client) Compile(v interface{}) (*pipeline.Build, error) { - p, err := c.Parse(v) +func (c *client) Compile(v interface{}) (*pipeline.Build, *library.Pipeline, error) { + p, data, err := c.Parse(v, c.repo.GetPipelineType(), new(yaml.Template)) if err != nil { - return nil, err + return nil, nil, err } + // create the library pipeline object from the yaml configuration + _pipeline := p.ToPipelineLibrary() + _pipeline.SetData(data) + _pipeline.SetType(c.repo.GetPipelineType()) + // validate the yaml configuration err = c.Validate(p) if err != nil { - return nil, err + return nil, _pipeline, err } // create map of templates for easy lookup - tmpls := mapFromTemplates(p.Templates) + templates := mapFromTemplates(p.Templates) + + event := c.build.GetEvent() + action := c.build.GetEventAction() + + // if the build has an event action, concatenate event and event action for matching + if !strings.EqualFold(action, "") { + event = event + ":" + action + } // create the ruledata to purge steps r := &pipeline.RuleData{ Branch: c.build.GetBranch(), Comment: c.comment, - Event: c.build.GetEvent(), + Event: event, Path: c.files, Repo: c.repo.GetFullName(), Tag: strings.TrimPrefix(c.build.GetRef(), "refs/tags/"), Target: c.build.GetDeploy(), } - if len(p.Stages) > 0 { - // check if the pipeline disabled the clone - if p.Metadata.Clone == nil || *p.Metadata.Clone { - // inject the clone stage - p, err = c.CloneStage(p) - if err != nil { - return nil, err - } - } - - // inject the init stage - p, err = c.InitStage(p) + switch { + case p.Metadata.RenderInline: + newPipeline, err := c.compileInline(p, c.TemplateDepth) if err != nil { - return nil, err + return nil, _pipeline, err } - - // inject the templates into the stages - p.Stages, p.Secrets, p.Services, p.Environment, err = c.ExpandStages(p, tmpls) + // validate the yaml configuration + err = c.Validate(newPipeline) if err != nil { - return nil, err + return nil, _pipeline, err } - if c.ModificationService.Endpoint != "" { - // send config to external endpoint for modification - p, err = c.modifyConfig(p, c.build, c.repo) - if err != nil { - return nil, err - } + if len(newPipeline.Stages) > 0 { + return c.compileStages(newPipeline, _pipeline, map[string]*yaml.Template{}, r) } + return c.compileSteps(newPipeline, _pipeline, map[string]*yaml.Template{}, r) + case len(p.Stages) > 0: + return c.compileStages(p, _pipeline, templates, r) + default: + return c.compileSteps(p, _pipeline, templates, r) + } +} + +// CompileLite produces a partial of an executable pipeline from a yaml configuration. +func (c *client) CompileLite(v interface{}, template, substitute bool) (*yaml.Build, *library.Pipeline, error) { + p, data, err := c.Parse(v, c.repo.GetPipelineType(), new(yaml.Template)) + if err != nil { + return nil, nil, err + } + + // create the library pipeline object from the yaml configuration + _pipeline := p.ToPipelineLibrary() + _pipeline.SetData(data) + _pipeline.SetType(c.repo.GetPipelineType()) + + if p.Metadata.RenderInline { + newPipeline, err := c.compileInline(p, c.TemplateDepth) + if err != nil { + return nil, _pipeline, err + } // validate the yaml configuration - err = c.Validate(p) + err = c.Validate(newPipeline) if err != nil { - return nil, err + return nil, _pipeline, err } - // Create some default global environment inject vars - // these are used below to overwrite to an empty - // map if they should not be injected into a container - envGlobalServices, envGlobalSecrets, envGlobalSteps := p.Environment, p.Environment, p.Environment + p = newPipeline + } + + if template { + // create map of templates for easy lookup + templates := mapFromTemplates(p.Templates) - if !p.Metadata.HasEnvironment("services") { - envGlobalServices = make(raw.StringSliceMap) - } + switch { + case len(p.Stages) > 0: + // inject the templates into the steps + p, err = c.ExpandStages(p, templates, nil) + if err != nil { + return nil, _pipeline, err + } - if !p.Metadata.HasEnvironment("secrets") { - envGlobalSecrets = make(raw.StringSliceMap) - } + if substitute { + // inject the substituted environment variables into the steps + p.Stages, err = c.SubstituteStages(p.Stages) + if err != nil { + return nil, _pipeline, err + } + } + case len(p.Steps) > 0: + // inject the templates into the steps + p, err = c.ExpandSteps(p, templates, nil, c.TemplateDepth) + if err != nil { + return nil, _pipeline, err + } - if !p.Metadata.HasEnvironment("steps") { - envGlobalSteps = make(raw.StringSliceMap) + if substitute { + // inject the substituted environment variables into the steps + p.Steps, err = c.SubstituteSteps(p.Steps) + if err != nil { + return nil, _pipeline, err + } + } } + } - // inject the environment variables into the services - p.Services, err = c.EnvironmentServices(p.Services, envGlobalServices) + // validate the yaml configuration + err = c.Validate(p) + if err != nil { + return nil, _pipeline, err + } + + return p, _pipeline, nil +} + +// compileInline parses and expands out inline pipelines. +func (c *client) compileInline(p *yaml.Build, depth int) (*yaml.Build, error) { + newPipeline := *p + newPipeline.Templates = yaml.TemplateSlice{} + + // return if max template depth has been reached + if depth == 0 { + retErr := fmt.Errorf("max template depth of %d exceeded", c.TemplateDepth) + + return nil, retErr + } + + for _, template := range p.Templates { + bytes, err := c.getTemplate(template, template.Name) if err != nil { return nil, err } - // inject the environment variables into the secrets - p.Secrets, err = c.EnvironmentSecrets(p.Secrets, envGlobalSecrets) - if err != nil { - return nil, err + format := template.Format + + // set the default format to golang if the user did not define anything + if template.Format == "" { + format = constants.PipelineTypeGo } - // inject the environment variables into the stages - p.Stages, err = c.EnvironmentStages(p.Stages, envGlobalSteps) + parsed, _, err := c.Parse(bytes, format, template) if err != nil { return nil, err } - // inject the substituted environment variables into the stages - p.Stages, err = c.SubstituteStages(p.Stages) - if err != nil { - return nil, err + // if template parsed contains a template reference, recurse with decremented depth + if len(parsed.Templates) > 0 && parsed.Metadata.RenderInline { + parsed, err = c.compileInline(parsed, depth-1) + if err != nil { + return nil, err + } } - // inject the scripts into the stages - p.Stages, err = c.ScriptStages(p.Stages) - if err != nil { - return nil, err + switch { + case len(parsed.Environment) > 0: + for key, value := range parsed.Environment { + newPipeline.Environment[key] = value + } + + fallthrough + case len(parsed.Stages) > 0: + // ensure all templated steps inside stages have template prefix + for stgIndex, newStage := range parsed.Stages { + parsed.Stages[stgIndex].Name = fmt.Sprintf("%s_%s", template.Name, newStage.Name) + + for index, newStep := range newStage.Steps { + parsed.Stages[stgIndex].Steps[index].Name = fmt.Sprintf("%s_%s", template.Name, newStep.Name) + } + } + + newPipeline.Stages = append(newPipeline.Stages, parsed.Stages...) + + fallthrough + case len(parsed.Steps) > 0: + // ensure all templated steps have template prefix + for index, newStep := range parsed.Steps { + parsed.Steps[index].Name = fmt.Sprintf("%s_%s", template.Name, newStep.Name) + } + + newPipeline.Steps = append(newPipeline.Steps, parsed.Steps...) + + fallthrough + case len(parsed.Services) > 0: + newPipeline.Services = append(newPipeline.Services, parsed.Services...) + fallthrough + case len(parsed.Secrets) > 0: + newPipeline.Secrets = append(newPipeline.Secrets, parsed.Secrets...) + default: + //nolint:lll // ignore long line length due to error message + return nil, fmt.Errorf("empty template %s provided: template must contain secrets, services, stages or steps", template.Name) } - // return executable representation - return c.TransformStages(r, p) + if len(newPipeline.Stages) > 0 && len(newPipeline.Steps) > 0 { + //nolint:lll // ignore long line length due to error message + return nil, fmt.Errorf("invalid template %s provided: templates cannot mix stages and steps", template.Name) + } } + return &newPipeline, nil +} + +// compileSteps executes the workflow for converting a YAML pipeline into an executable struct. +// +//nolint:dupl,lll // linter thinks the steps and stages workflows are identical +func (c *client) compileSteps(p *yaml.Build, _pipeline *library.Pipeline, tmpls map[string]*yaml.Template, r *pipeline.RuleData) (*pipeline.Build, *library.Pipeline, error) { + var err error + // check if the pipeline disabled the clone if p.Metadata.Clone == nil || *p.Metadata.Clone { // inject the clone step p, err = c.CloneStep(p) if err != nil { - return nil, err + return nil, _pipeline, err } } // inject the init step p, err = c.InitStep(p) if err != nil { - return nil, err + return nil, _pipeline, err } // inject the templates into the steps - p.Steps, p.Secrets, p.Services, p.Environment, err = c.ExpandSteps(p, tmpls) + p, err = c.ExpandSteps(p, tmpls, r, c.TemplateDepth) if err != nil { - return nil, err + return nil, _pipeline, err } if c.ModificationService.Endpoint != "" { // send config to external endpoint for modification p, err = c.modifyConfig(p, c.build, c.repo) if err != nil { - return nil, err + return nil, _pipeline, err } } // validate the yaml configuration err = c.Validate(p) if err != nil { - return nil, err + return nil, _pipeline, err } // Create some default global environment inject vars @@ -209,49 +322,149 @@ func (c *client) Compile(v interface{}) (*pipeline.Build, error) { // inject the environment variables into the services p.Services, err = c.EnvironmentServices(p.Services, envGlobalServices) if err != nil { - return nil, err + return nil, _pipeline, err } // inject the environment variables into the secrets p.Secrets, err = c.EnvironmentSecrets(p.Secrets, envGlobalSecrets) if err != nil { - return nil, err + return nil, _pipeline, err } // inject the environment variables into the steps p.Steps, err = c.EnvironmentSteps(p.Steps, envGlobalSteps) if err != nil { - return nil, err + return nil, _pipeline, err } // inject the substituted environment variables into the steps p.Steps, err = c.SubstituteSteps(p.Steps) if err != nil { - return nil, err + return nil, _pipeline, err } // inject the scripts into the steps p.Steps, err = c.ScriptSteps(p.Steps) if err != nil { - return nil, err + return nil, _pipeline, err + } + + // create executable representation + build, err := c.TransformSteps(r, p) + if err != nil { + return nil, _pipeline, err + } + + return build, _pipeline, nil +} + +// compileStages executes the workflow for converting a YAML pipeline into an executable struct. +// +//nolint:dupl,lll // linter thinks the steps and stages workflows are identical +func (c *client) compileStages(p *yaml.Build, _pipeline *library.Pipeline, tmpls map[string]*yaml.Template, r *pipeline.RuleData) (*pipeline.Build, *library.Pipeline, error) { + var err error + + // check if the pipeline disabled the clone + if p.Metadata.Clone == nil || *p.Metadata.Clone { + // inject the clone stage + p, err = c.CloneStage(p) + if err != nil { + return nil, _pipeline, err + } + } + + // inject the init stage + p, err = c.InitStage(p) + if err != nil { + return nil, _pipeline, err + } + + // inject the templates into the stages + p, err = c.ExpandStages(p, tmpls, r) + if err != nil { + return nil, _pipeline, err + } + + if c.ModificationService.Endpoint != "" { + // send config to external endpoint for modification + p, err = c.modifyConfig(p, c.build, c.repo) + if err != nil { + return nil, _pipeline, err + } + } + + // validate the yaml configuration + err = c.Validate(p) + if err != nil { + return nil, _pipeline, err + } + + // Create some default global environment inject vars + // these are used below to overwrite to an empty + // map if they should not be injected into a container + envGlobalServices, envGlobalSecrets, envGlobalSteps := p.Environment, p.Environment, p.Environment + + if !p.Metadata.HasEnvironment("services") { + envGlobalServices = make(raw.StringSliceMap) + } + + if !p.Metadata.HasEnvironment("secrets") { + envGlobalSecrets = make(raw.StringSliceMap) + } + + if !p.Metadata.HasEnvironment("steps") { + envGlobalSteps = make(raw.StringSliceMap) + } + + // inject the environment variables into the services + p.Services, err = c.EnvironmentServices(p.Services, envGlobalServices) + if err != nil { + return nil, _pipeline, err + } + + // inject the environment variables into the secrets + p.Secrets, err = c.EnvironmentSecrets(p.Secrets, envGlobalSecrets) + if err != nil { + return nil, _pipeline, err + } + + // inject the environment variables into the stages + p.Stages, err = c.EnvironmentStages(p.Stages, envGlobalSteps) + if err != nil { + return nil, _pipeline, err + } + + // inject the substituted environment variables into the stages + p.Stages, err = c.SubstituteStages(p.Stages) + if err != nil { + return nil, _pipeline, err + } + + // inject the scripts into the stages + p.Stages, err = c.ScriptStages(p.Stages) + if err != nil { + return nil, _pipeline, err } - // return executable representation - return c.TransformSteps(r, p) + // create executable representation + build, err := c.TransformStages(r, p) + if err != nil { + return nil, _pipeline, err + } + + return build, _pipeline, nil } // errorHandler ensures the error contains the number of request attempts. func errorHandler(resp *http.Response, err error, attempts int) (*http.Response, error) { if err != nil { - // nolint:lll // detailed error message - err = fmt.Errorf("giving up connecting to modification endpoint after %d attempts due to: %v", attempts, err) + err = fmt.Errorf("giving up connecting to modification endpoint after %d attempts due to: %w", attempts, err) } return resp, err } // modifyConfig sends the configuration to external http endpoint for modification. -// nolint:lll // parameter struct references push line limit func (c *client) modifyConfig(build *yaml.Build, libraryBuild *library.Build, repo *library.Repo) (*yaml.Build, error) { // create request to send to endpoint data, err := yml.Marshal(build) @@ -284,17 +497,16 @@ func (c *client) modifyConfig(build *yaml.Build, libraryBuild *library.Build, re Backoff: retryablehttp.DefaultBackoff, } + // ensure the overall request(s) do not take over the defined timeout + ctx, cancel := context.WithTimeout(context.Background(), c.ModificationService.Timeout) + defer cancel() + // create POST request - req, err := retryablehttp.NewRequest("POST", c.ModificationService.Endpoint, bytes.NewBuffer(b)) + req, err := retryablehttp.NewRequestWithContext(ctx, "POST", c.ModificationService.Endpoint, bytes.NewBuffer(b)) if err != nil { return nil, err } - // ensure the overall request(s) do not take over the defined timeout - ctx, cancel := context.WithTimeout(req.Request.Context(), c.ModificationService.Timeout) - defer cancel() - req.WithContext(ctx) - // add content-type and auth headers req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.ModificationService.Secret)) @@ -311,7 +523,7 @@ func (c *client) modifyConfig(build *yaml.Build, libraryBuild *library.Build, re return nil, fmt.Errorf("modification endpoint returned status code %v", resp.StatusCode) } - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read payload: %w", err) } diff --git a/compiler/native/compile_test.go b/compiler/native/compile_test.go index f7a925c97..3a37703d6 100644 --- a/compiler/native/compile_test.go +++ b/compiler/native/compile_test.go @@ -7,10 +7,16 @@ package native import ( "flag" "fmt" - "io/ioutil" "net/http" "net/http/httptest" - "reflect" + "os" + "path/filepath" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/raw" + + "github.com/google/go-github/v53/github" + "testing" "time" @@ -31,6 +37,8 @@ import ( func TestNative_Compile_StagesPipeline(t *testing.T) { // setup types set := flag.NewFlagSet("test", 0) + set.String("clone-image", defaultCloneImage, "doc") + set.Int("max-template-depth", 5, "doc") c := cli.NewContext(nil, set, nil) m := &types.Metadata{ @@ -129,7 +137,7 @@ func TestNative_Compile_StagesPipeline(t *testing.T) { ID: "__0_clone_clone", Directory: "/vela/src/foo//", Environment: cloneEnv, - Image: "target/vela-git:v0.4.0", + Image: defaultCloneImage, Name: "clone", Number: 2, Pull: "not_present", @@ -236,7 +244,7 @@ func TestNative_Compile_StagesPipeline(t *testing.T) { } // run test - yaml, err := ioutil.ReadFile("testdata/stages_pipeline.yml") + yaml, err := os.ReadFile("testdata/stages_pipeline.yml") if err != nil { t.Errorf("Reading yaml file return err: %v", err) } @@ -248,7 +256,7 @@ func TestNative_Compile_StagesPipeline(t *testing.T) { compiler.WithMetadata(m) - got, err := compiler.Compile(yaml) + got, _, err := compiler.Compile(yaml) if err != nil { t.Errorf("Compile returned err: %v", err) } @@ -291,7 +299,7 @@ func TestNative_Compile_StagesPipeline_Modification(t *testing.T) { number := 1 // run test - yaml, err := ioutil.ReadFile("testdata/stages_pipeline.yml") + yaml, err := os.ReadFile("testdata/stages_pipeline.yml") if err != nil { t.Errorf("Reading yaml file return err: %v", err) } @@ -301,6 +309,7 @@ func TestNative_Compile_StagesPipeline_Modification(t *testing.T) { libraryBuild *library.Build repo *library.Repo } + tests := []struct { name string args args @@ -317,6 +326,7 @@ func TestNative_Compile_StagesPipeline_Modification(t *testing.T) { endpoint: fmt.Sprintf("%s/%s", s.URL, "config/bad"), }, true}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { compiler := client{ @@ -327,7 +337,7 @@ func TestNative_Compile_StagesPipeline_Modification(t *testing.T) { repo: &library.Repo{Name: &author}, build: &library.Build{Author: &name, Number: &number}, } - _, err := compiler.Compile(yaml) + _, _, err := compiler.Compile(yaml) if (err != nil) != tt.wantErr { t.Errorf("Compile() error = %v, wantErr %v", err, tt.wantErr) return @@ -357,7 +367,7 @@ func TestNative_Compile_StepsPipeline_Modification(t *testing.T) { number := 1 // run test - yaml, err := ioutil.ReadFile("testdata/steps_pipeline.yml") + yaml, err := os.ReadFile("testdata/steps_pipeline.yml") if err != nil { t.Errorf("Reading yaml file return err: %v", err) } @@ -367,6 +377,7 @@ func TestNative_Compile_StepsPipeline_Modification(t *testing.T) { libraryBuild *library.Build repo *library.Repo } + tests := []struct { name string args args @@ -383,6 +394,7 @@ func TestNative_Compile_StepsPipeline_Modification(t *testing.T) { endpoint: fmt.Sprintf("%s/%s", s.URL, "config/bad"), }, true}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { compiler := client{ @@ -393,7 +405,7 @@ func TestNative_Compile_StepsPipeline_Modification(t *testing.T) { repo: tt.args.repo, build: tt.args.libraryBuild, } - _, err := compiler.Compile(yaml) + _, _, err := compiler.Compile(yaml) if (err != nil) != tt.wantErr { t.Errorf("Compile() error = %v, wantErr %v", err, tt.wantErr) return @@ -405,6 +417,8 @@ func TestNative_Compile_StepsPipeline_Modification(t *testing.T) { func TestNative_Compile_StepsPipeline(t *testing.T) { // setup types set := flag.NewFlagSet("test", 0) + set.String("clone-image", defaultCloneImage, "doc") + set.Int("max-template-depth", 5, "doc") c := cli.NewContext(nil, set, nil) m := &types.Metadata{ @@ -485,7 +499,7 @@ func TestNative_Compile_StepsPipeline(t *testing.T) { ID: "step___0_clone", Directory: "/vela/src/foo//", Environment: cloneEnv, - Image: "target/vela-git:v0.4.0", + Image: defaultCloneImage, Name: "clone", Number: 2, Pull: "not_present", @@ -562,7 +576,7 @@ func TestNative_Compile_StepsPipeline(t *testing.T) { } // run test - yaml, err := ioutil.ReadFile("testdata/steps_pipeline.yml") + yaml, err := os.ReadFile("testdata/steps_pipeline.yml") if err != nil { t.Errorf("Reading yaml file return err: %v", err) } @@ -574,7 +588,7 @@ func TestNative_Compile_StepsPipeline(t *testing.T) { compiler.WithMetadata(m) - got, err := compiler.Compile(yaml) + got, _, err := compiler.Compile(yaml) if err != nil { t.Errorf("Compile returned err: %v", err) } @@ -592,10 +606,12 @@ func TestNative_Compile_StagesPipelineTemplate(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/api/v3/repos/:org/:name/contents/:path", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/template.json") + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) }) s := httptest.NewServer(engine) @@ -606,6 +622,8 @@ func TestNative_Compile_StagesPipelineTemplate(t *testing.T) { set.Bool("github-driver", true, "doc") set.String("github-url", s.URL, "doc") set.String("github-token", "", "doc") + set.String("clone-image", defaultCloneImage, "doc") + set.Int("max-template-depth", 5, "doc") c := cli.NewContext(nil, set, nil) m := &types.Metadata{ @@ -702,7 +720,7 @@ func TestNative_Compile_StagesPipelineTemplate(t *testing.T) { ID: "__0_clone_clone", Directory: "/vela/src/foo//", Environment: setupEnv, - Image: "target/vela-git:v0.4.0", + Image: defaultCloneImage, Name: "clone", Number: 2, Pull: "not_present", @@ -813,7 +831,7 @@ func TestNative_Compile_StagesPipelineTemplate(t *testing.T) { } // run test - yaml, err := ioutil.ReadFile("testdata/stages_pipeline_template.yml") + yaml, err := os.ReadFile("testdata/stages_pipeline_template.yml") if err != nil { t.Errorf("Reading yaml file return err: %v", err) } @@ -825,7 +843,7 @@ func TestNative_Compile_StagesPipelineTemplate(t *testing.T) { compiler.WithMetadata(m) - got, err := compiler.Compile(yaml) + got, _, err := compiler.Compile(yaml) if err != nil { t.Errorf("Compile returned err: %v", err) } @@ -855,10 +873,12 @@ func TestNative_Compile_StepsPipelineTemplate(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/api/v3/repos/foo/bar/contents/:path", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/template.json") + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) }) s := httptest.NewServer(engine) @@ -869,6 +889,8 @@ func TestNative_Compile_StepsPipelineTemplate(t *testing.T) { set.Bool("github-driver", true, "doc") set.String("github-url", s.URL, "doc") set.String("github-token", "", "doc") + set.String("clone-image", defaultCloneImage, "doc") + set.Int("max-template-depth", 5, "doc") c := cli.NewContext(nil, set, nil) m := &types.Metadata{ @@ -955,7 +977,7 @@ func TestNative_Compile_StepsPipelineTemplate(t *testing.T) { ID: "step___0_clone", Directory: "/vela/src/foo//", Environment: setupEnv, - Image: "target/vela-git:v0.4.0", + Image: defaultCloneImage, Name: "clone", Number: 2, Pull: "not_present", @@ -1050,7 +1072,7 @@ func TestNative_Compile_StepsPipelineTemplate(t *testing.T) { } // run test - yaml, err := ioutil.ReadFile("testdata/steps_pipeline_template.yml") + yaml, err := os.ReadFile("testdata/steps_pipeline_template.yml") if err != nil { t.Errorf("Reading yaml file return err: %v", err) } @@ -1062,7 +1084,7 @@ func TestNative_Compile_StepsPipelineTemplate(t *testing.T) { compiler.WithMetadata(m) - got, err := compiler.Compile(yaml) + got, _, err := compiler.Compile(yaml) if err != nil { t.Errorf("Compile returned err: %v", err) } @@ -1072,7 +1094,8 @@ func TestNative_Compile_StepsPipelineTemplate(t *testing.T) { } } -func TestNative_Compile_InvalidType(t *testing.T) { +// Test evaluation of `vela "tempalate_name"` function. +func TestNative_Compile_StepsPipelineTemplate_VelaFunction_TemplateName(t *testing.T) { // setup context gin.SetMode(gin.TestMode) @@ -1080,10 +1103,12 @@ func TestNative_Compile_InvalidType(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/api/v3/repos/foo/bar/contents/:path", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/template.json") + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) }) s := httptest.NewServer(engine) @@ -1094,6 +1119,8 @@ func TestNative_Compile_InvalidType(t *testing.T) { set.Bool("github-driver", true, "doc") set.String("github-url", s.URL, "doc") set.String("github-token", "", "doc") + set.String("clone-image", defaultCloneImage, "doc") + set.Int("max-template-depth", 5, "doc") c := cli.NewContext(nil, set, nil) m := &types.Metadata{ @@ -1116,14 +1143,12 @@ func TestNative_Compile_InvalidType(t *testing.T) { }, } - gradleEnv := environment(nil, m, nil, nil) - gradleEnv["GRADLE_OPTS"] = "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=1 -Dorg.gradle.parallel=false" - gradleEnv["GRADLE_USER_HOME"] = ".gradle" + setupEnv := environment(nil, m, nil, nil) - dockerEnv := environment(nil, m, nil, nil) - dockerEnv["PARAMETER_REGISTRY"] = "index.docker.io" - dockerEnv["PARAMETER_REPO"] = "github/octocat" - dockerEnv["PARAMETER_TAGS"] = "latest,dev" + helloEnv := environment(nil, m, nil, nil) + helloEnv["HOME"] = "/root" + helloEnv["SHELL"] = "/bin/sh" + helloEnv["VELA_BUILD_SCRIPT"] = generateScriptPosix([]string{"echo sample"}) want := &pipeline.Build{ Version: "1", @@ -1137,7 +1162,7 @@ func TestNative_Compile_InvalidType(t *testing.T) { &pipeline.Container{ ID: "step___0_init", Directory: "/vela/src/foo//", - Environment: environment(nil, m, nil, nil), + Environment: setupEnv, Image: "#init", Name: "init", Number: 1, @@ -1146,52 +1171,148 @@ func TestNative_Compile_InvalidType(t *testing.T) { &pipeline.Container{ ID: "step___0_clone", Directory: "/vela/src/foo//", - Environment: environment(nil, m, nil, nil), - Image: "target/vela-git:v0.4.0", + Environment: setupEnv, + Image: defaultCloneImage, Name: "clone", Number: 2, Pull: "not_present", }, &pipeline.Container{ - ID: "step___0_docker", + ID: "step___0_sample_hello", Directory: "/vela/src/foo//", - Image: "plugins/docker:18.09", - Environment: dockerEnv, - Name: "docker", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: helloEnv, + Image: "sample", + Name: "sample_hello", Number: 3, - Pull: "always", - Secrets: pipeline.StepSecretSlice{ - &pipeline.StepSecret{ - Source: "docker_username", - Target: "registry_username", - }, - &pipeline.StepSecret{ - Source: "docker_password", - Target: "registry_password", - }, - }, + Pull: "not_present", }, }, - Secrets: pipeline.SecretSlice{ - &pipeline.Secret{ - Name: "docker_username", - Key: "org/repo/docker/username", - Engine: "native", - Type: "repo", - Origin: &pipeline.Container{}, + } + + // run test + yaml, err := os.ReadFile("testdata/template_name.yml") + if err != nil { + t.Errorf("Reading yaml file return err: %v", err) + } + + compiler, err := New(c) + if err != nil { + t.Errorf("Creating compiler returned err: %v", err) + } + + compiler.WithMetadata(m) + + got, _, err := compiler.Compile(yaml) + if err != nil { + t.Errorf("Compile returned err: %v", err) + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Compile() mismatch (-want +got):\n%s", diff) + } +} + +// Test evaluation of `vela "tempalate_name"` function on a inline template. +func TestNative_Compile_StepsPipelineTemplate_VelaFunction_TemplateName_Inline(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + set := flag.NewFlagSet("test", 0) + set.Bool("github-driver", true, "doc") + set.String("github-url", s.URL, "doc") + set.String("github-token", "", "doc") + set.String("clone-image", defaultCloneImage, "doc") + set.Int("max-template-depth", 5, "doc") + c := cli.NewContext(nil, set, nil) + + m := &types.Metadata{ + Database: &types.Database{ + Driver: "foo", + Host: "foo", + }, + Queue: &types.Queue{ + Channel: "foo", + Driver: "foo", + Host: "foo", + }, + Source: &types.Source{ + Driver: "foo", + Host: "foo", + }, + Vela: &types.Vela{ + Address: "foo", + WebAddress: "foo", + }, + } + + setupEnv := environment(nil, m, nil, nil) + + helloEnv := environment(nil, m, nil, nil) + helloEnv["HOME"] = "/root" + helloEnv["SHELL"] = "/bin/sh" + helloEnv["VELA_BUILD_SCRIPT"] = generateScriptPosix([]string{"echo inline_templatename"}) + + want := &pipeline.Build{ + Version: "1", + ID: "__0", + Metadata: pipeline.Metadata{ + Clone: true, + Template: false, + Environment: []string{"steps", "services", "secrets"}, + }, + Steps: pipeline.ContainerSlice{ + &pipeline.Container{ + ID: "step___0_init", + Directory: "/vela/src/foo//", + Environment: setupEnv, + Image: "#init", + Name: "init", + Number: 1, + Pull: "not_present", }, - &pipeline.Secret{ - Name: "docker_password", - Key: "org/repo/docker/password", - Engine: "vault", - Type: "repo", - Origin: &pipeline.Container{}, + &pipeline.Container{ + ID: "step___0_clone", + Directory: "/vela/src/foo//", + Environment: setupEnv, + Image: defaultCloneImage, + Name: "clone", + Number: 2, + Pull: "not_present", + }, + &pipeline.Container{ + ID: "step___0_inline_templatename_hello", + Directory: "/vela/src/foo//", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: helloEnv, + Image: "inline_templatename", + Name: "inline_templatename_hello", + Number: 3, + Pull: "not_present", }, }, } // run test - yaml, err := ioutil.ReadFile("testdata/invalid_type.yml") + yaml, err := os.ReadFile("testdata/template_name_inline.yml") if err != nil { t.Errorf("Reading yaml file return err: %v", err) } @@ -1203,13 +1324,88 @@ func TestNative_Compile_InvalidType(t *testing.T) { compiler.WithMetadata(m) - got, err := compiler.Compile(yaml) + got, _, err := compiler.Compile(yaml) if err != nil { t.Errorf("Compile returned err: %v", err) } - if !reflect.DeepEqual(got, want) { - t.Errorf("Compile is %v, want %v", got, want) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("Compile() mismatch (-want +got):\n%s", diff) + } +} + +func TestNative_Compile_InvalidType(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + set := flag.NewFlagSet("test", 0) + set.Bool("github-driver", true, "doc") + set.String("github-url", s.URL, "doc") + set.String("github-token", "", "doc") + set.Int("max-template-depth", 5, "doc") + c := cli.NewContext(nil, set, nil) + + m := &types.Metadata{ + Database: &types.Database{ + Driver: "foo", + Host: "foo", + }, + Queue: &types.Queue{ + Channel: "foo", + Driver: "foo", + Host: "foo", + }, + Source: &types.Source{ + Driver: "foo", + Host: "foo", + }, + Vela: &types.Vela{ + Address: "foo", + WebAddress: "foo", + }, + } + + gradleEnv := environment(nil, m, nil, nil) + gradleEnv["GRADLE_OPTS"] = "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=1 -Dorg.gradle.parallel=false" + gradleEnv["GRADLE_USER_HOME"] = ".gradle" + + dockerEnv := environment(nil, m, nil, nil) + dockerEnv["PARAMETER_REGISTRY"] = "index.docker.io" + dockerEnv["PARAMETER_REPO"] = "github/octocat" + dockerEnv["PARAMETER_TAGS"] = "latest,dev" + + // run test + invalidYaml, err := os.ReadFile("testdata/invalid_type.yml") + if err != nil { + t.Errorf("Reading yaml file return err: %v", err) + } + + compiler, err := New(c) + if err != nil { + t.Errorf("Creating compiler returned err: %v", err) + } + + compiler.WithMetadata(m) + + _, _, err = compiler.Compile(invalidYaml) + if err == nil { + t.Error("Compile should have returned an err") } } @@ -1218,6 +1414,8 @@ func TestNative_Compile_Clone(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("github-driver", true, "doc") set.String("github-token", "", "doc") + set.String("clone-image", defaultCloneImage, "doc") + set.Int("max-template-depth", 5, "doc") c := cli.NewContext(nil, set, nil) m := &types.Metadata{ @@ -1298,7 +1496,7 @@ func TestNative_Compile_Clone(t *testing.T) { ID: "step___0_clone", Directory: "/vela/src/foo//", Environment: environment(nil, m, nil, nil), - Image: "target/vela-git:v0.4.0", + Image: defaultCloneImage, Name: "clone", Number: 2, Pull: "not_present", @@ -1337,7 +1535,7 @@ func TestNative_Compile_Clone(t *testing.T) { ID: "step___0_clone", Directory: "/vela/src/foo//", Environment: cloneEnv, - Image: "target/vela-git:v0.4.0", + Image: "target/vela-git:v0.5.1", Name: "clone", Number: 2, Pull: "always", @@ -1357,6 +1555,7 @@ func TestNative_Compile_Clone(t *testing.T) { type args struct { file string } + tests := []struct { name string args args @@ -1373,10 +1572,11 @@ func TestNative_Compile_Clone(t *testing.T) { file: "testdata/clone_replace.yml", }, wantReplace, false}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // run test - yaml, err := ioutil.ReadFile(tt.args.file) + yaml, err := os.ReadFile(tt.args.file) if err != nil { t.Errorf("Reading yaml file return err: %v", err) } @@ -1388,7 +1588,7 @@ func TestNative_Compile_Clone(t *testing.T) { compiler.WithMetadata(m) - got, err := compiler.Compile(yaml) + got, _, err := compiler.Compile(yaml) if err != nil { t.Errorf("Compile returned err: %v", err) } @@ -1405,6 +1605,8 @@ func TestNative_Compile_Pipeline_Type(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("github-driver", true, "doc") set.String("github-token", "", "doc") + set.String("clone-image", defaultCloneImage, "doc") + set.Int("max-template-depth", 5, "doc") c := cli.NewContext(nil, set, nil) m := &types.Metadata{ @@ -1435,8 +1637,9 @@ func TestNative_Compile_Pipeline_Type(t *testing.T) { Version: "1", ID: "__0", Metadata: pipeline.Metadata{ - Clone: true, - Template: false, + Clone: true, + Template: false, + Environment: []string{"steps", "services", "secrets"}, }, Steps: pipeline.ContainerSlice{ &pipeline.Container{ @@ -1452,7 +1655,7 @@ func TestNative_Compile_Pipeline_Type(t *testing.T) { ID: "step___0_clone", Directory: "/vela/src/foo//", Environment: defaultEnv, - Image: "target/vela-git:v0.4.0", + Image: defaultCloneImage, Name: "clone", Number: 2, Pull: "not_present", @@ -1479,8 +1682,9 @@ func TestNative_Compile_Pipeline_Type(t *testing.T) { Version: "1", ID: "__0", Metadata: pipeline.Metadata{ - Clone: true, - Template: false, + Clone: true, + Template: false, + Environment: []string{"steps", "services", "secrets"}, }, Steps: pipeline.ContainerSlice{ &pipeline.Container{ @@ -1496,7 +1700,7 @@ func TestNative_Compile_Pipeline_Type(t *testing.T) { ID: "step___0_clone", Directory: "/vela/src/foo//", Environment: defaultGoEnv, - Image: "target/vela-git:v0.4.0", + Image: defaultCloneImage, Name: "clone", Number: 2, Pull: "not_present", @@ -1523,8 +1727,9 @@ func TestNative_Compile_Pipeline_Type(t *testing.T) { Version: "1", ID: "__0", Metadata: pipeline.Metadata{ - Clone: true, - Template: false, + Clone: true, + Template: false, + Environment: []string{"steps", "services", "secrets"}, }, Steps: pipeline.ContainerSlice{ &pipeline.Container{ @@ -1540,7 +1745,7 @@ func TestNative_Compile_Pipeline_Type(t *testing.T) { ID: "step___0_clone", Directory: "/vela/src/foo//", Environment: defaultStarlarkEnv, - Image: "target/vela-git:v0.4.0", + Image: defaultCloneImage, Name: "clone", Number: 2, Pull: "not_present", @@ -1561,6 +1766,7 @@ func TestNative_Compile_Pipeline_Type(t *testing.T) { file string pipelineType string } + tests := []struct { name string args args @@ -1571,10 +1777,11 @@ func TestNative_Compile_Pipeline_Type(t *testing.T) { {"golang", args{file: "testdata/pipeline_type_go.yml", pipelineType: "go"}, wantGo, false}, {"starlark", args{file: "testdata/pipeline_type.star", pipelineType: "starlark"}, wantStarlark, false}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // run test - yaml, err := ioutil.ReadFile(tt.args.file) + yaml, err := os.ReadFile(tt.args.file) if err != nil { t.Errorf("Reading yaml file return err: %v", err) } @@ -1587,7 +1794,7 @@ func TestNative_Compile_Pipeline_Type(t *testing.T) { compiler.WithMetadata(m) compiler.WithRepo(&library.Repo{PipelineType: &tt.args.pipelineType}) - got, err := compiler.Compile(yaml) + got, _, err := compiler.Compile(yaml) if err != nil { t.Errorf("Compile returned err: %v", err) } @@ -1608,7 +1815,7 @@ func TestNative_Compile_NoStepsorStages(t *testing.T) { number := 1 // run test - yaml, err := ioutil.ReadFile("testdata/metadata.yml") + yaml, err := os.ReadFile("testdata/metadata.yml") if err != nil { t.Errorf("Reading yaml file return err: %v", err) } @@ -1617,10 +1824,11 @@ func TestNative_Compile_NoStepsorStages(t *testing.T) { if err != nil { t.Errorf("Creating compiler returned err: %v", err) } + compiler.repo = &library.Repo{Name: &author} compiler.build = &library.Build{Author: &name, Number: &number} - got, err := compiler.Compile(yaml) + got, _, err := compiler.Compile(yaml) if err == nil { t.Errorf("Compile should have returned err") } @@ -1639,7 +1847,7 @@ func TestNative_Compile_StepsandStages(t *testing.T) { number := 1 // run test - yaml, err := ioutil.ReadFile("testdata/steps_and_stages.yml") + yaml, err := os.ReadFile("testdata/steps_and_stages.yml") if err != nil { t.Errorf("Reading yaml file return err: %v", err) } @@ -1648,10 +1856,11 @@ func TestNative_Compile_StepsandStages(t *testing.T) { if err != nil { t.Errorf("Creating compiler returned err: %v", err) } + compiler.repo = &library.Repo{Name: &author} compiler.build = &library.Build{Author: &name, Number: &number} - got, err := compiler.Compile(yaml) + got, _, err := compiler.Compile(yaml) if err == nil { t.Errorf("Compile should have returned err") } @@ -1683,10 +1892,12 @@ func Test_client_modifyConfig(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/api/v3/repos/foo/bar/contents/:path", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/template.json") + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) }) m := &types.Metadata{ @@ -1724,7 +1935,7 @@ func Test_client_modifyConfig(t *testing.T) { }, &yaml.Step{ Environment: environment(nil, m, nil, nil), - Image: "target/vela-git:v0.3.0", + Image: defaultCloneImage, Name: "clone", Pull: "not_present", }, @@ -1757,7 +1968,7 @@ func Test_client_modifyConfig(t *testing.T) { }, &yaml.Step{ Environment: environment(nil, m, nil, nil), - Image: "target/vela-git:v0.3.0", + Image: defaultCloneImage, Name: "clone", Pull: "not_present", }, @@ -1851,6 +2062,7 @@ func Test_client_modifyConfig(t *testing.T) { libraryBuild *library.Build repo *library.Repo } + tests := []struct { name string args args @@ -1894,6 +2106,7 @@ func Test_client_modifyConfig(t *testing.T) { endpoint: fmt.Sprintf("%s/%s", s.URL, "config/empty"), }, nil, true}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { compiler := client{ @@ -1915,3 +2128,1308 @@ func Test_client_modifyConfig(t *testing.T) { }) } } + +func convertFileToGithubResponse(file string) (github.RepositoryContent, error) { + body, err := os.ReadFile(filepath.Join("testdata", file)) + if err != nil { + return github.RepositoryContent{}, err + } + + content := github.RepositoryContent{ + Encoding: github.String(""), + Content: github.String(string(body)), + } + + return content, nil +} + +func generateTestEnv(command string, m *types.Metadata, pipelineType string) map[string]string { + output := environment(nil, m, nil, nil) + output["VELA_BUILD_SCRIPT"] = generateScriptPosix([]string{command}) + output["HOME"] = "/root" + output["SHELL"] = "/bin/sh" + output["VELA_REPO_PIPELINE_TYPE"] = pipelineType + + return output +} + +func Test_Compile_Inline(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + set := flag.NewFlagSet("test", 0) + set.Bool("github-driver", true, "doc") + set.String("github-url", s.URL, "doc") + set.String("github-token", "", "doc") + set.String("clone-image", defaultCloneImage, "doc") + set.Int("max-template-depth", 5, "doc") + c := cli.NewContext(nil, set, nil) + + m := &types.Metadata{ + Database: &types.Database{ + Driver: "foo", + Host: "foo", + }, + Queue: &types.Queue{ + Channel: "foo", + Driver: "foo", + Host: "foo", + }, + Source: &types.Source{ + Driver: "foo", + Host: "foo", + }, + Vela: &types.Vela{ + Address: "foo", + WebAddress: "foo", + }, + } + + initEnv := environment(nil, m, nil, nil) + testEnv := environment(nil, m, nil, nil) + testEnv["FOO"] = "Hello, foo!" + testEnv["HELLO"] = "Hello, Vela!" + stepEnv := environment(nil, m, nil, nil) + stepEnv["FOO"] = "Hello, foo!" + stepEnv["HELLO"] = "Hello, Vela!" + stepEnv["PARAMETER_FIRST"] = "foo" + golangEnv := environment(nil, m, nil, nil) + golangEnv["VELA_REPO_PIPELINE_TYPE"] = "go" + + type args struct { + file string + pipelineType string + } + + tests := []struct { + name string + args args + want *pipeline.Build + wantErr bool + }{ + { + name: "root stages", + args: args{ + file: "testdata/inline_with_stages.yml", + }, + want: &pipeline.Build{ + Version: "1", + ID: "__0", + Metadata: pipeline.Metadata{ + Clone: true, + Environment: []string{"steps", "services", "secrets"}, + }, + Stages: []*pipeline.Stage{ + { + Name: "init", + Environment: initEnv, + Steps: pipeline.ContainerSlice{ + &pipeline.Container{ + ID: "__0_init_init", + Directory: "/vela/src/foo//", + Environment: initEnv, + Image: "#init", + Name: "init", + Number: 1, + Pull: "not_present", + }, + }, + }, + { + Name: "clone", + Environment: initEnv, + Steps: pipeline.ContainerSlice{ + &pipeline.Container{ + ID: "__0_clone_clone", + Directory: "/vela/src/foo//", + Environment: initEnv, + Image: defaultCloneImage, + Name: "clone", + Number: 2, + Pull: "not_present", + }, + }, + }, + { + Name: "test", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_test_test", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo from inline", m, ""), + Image: "alpine", + Name: "test", + Pull: "not_present", + Number: 3, + }, + }, + }, + { + Name: "golang_foo", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_golang_foo_golang_foo", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from foo", m, ""), + Image: "golang:latest", + Name: "golang_foo", + Pull: "not_present", + Number: 4, + }, + }, + }, + { + Name: "golang_bar", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_golang_bar_golang_bar", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from bar", m, ""), + Image: "golang:latest", + Name: "golang_bar", + Pull: "not_present", + Number: 5, + }, + }, + }, + { + Name: "golang_star", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_golang_star_golang_star", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from star", m, ""), + Image: "golang:latest", + Name: "golang_star", + Pull: "not_present", + Number: 6, + }, + }, + }, + { + Name: "starlark_foo", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_starlark_foo_starlark_build_foo", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from foo", m, ""), + Image: "alpine", + Name: "starlark_build_foo", + Pull: "not_present", + Number: 7, + }, + }, + }, + { + Name: "starlark_bar", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_starlark_bar_starlark_build_bar", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from bar", m, ""), + Image: "alpine", + Name: "starlark_build_bar", + Pull: "not_present", + Number: 8, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "nested templates", + args: args{ + file: "testdata/inline_nested_template.yml", + }, + want: &pipeline.Build{ + Version: "1", + ID: "__0", + Metadata: pipeline.Metadata{ + Clone: true, + Environment: []string{"steps", "services", "secrets"}, + }, + Stages: []*pipeline.Stage{ + { + Name: "init", + Environment: initEnv, + Steps: pipeline.ContainerSlice{ + &pipeline.Container{ + ID: "__0_init_init", + Directory: "/vela/src/foo//", + Environment: initEnv, + Image: "#init", + Name: "init", + Number: 1, + Pull: "not_present", + }, + }, + }, + { + Name: "clone", + Environment: initEnv, + Steps: pipeline.ContainerSlice{ + &pipeline.Container{ + ID: "__0_clone_clone", + Directory: "/vela/src/foo//", + Environment: initEnv, + Image: defaultCloneImage, + Name: "clone", + Number: 2, + Pull: "not_present", + }, + }, + }, + { + Name: "test", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_test_test", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo from inline", m, ""), + Image: "alpine", + Name: "test", + Pull: "not_present", + Number: 3, + }, + }, + }, + { + Name: "nested_test", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_nested_test_nested_test", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo from inline", m, ""), + Image: "alpine", + Name: "nested_test", + Pull: "not_present", + Number: 4, + }, + }, + }, + { + Name: "nested_golang_foo", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_nested_golang_foo_nested_golang_foo", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from foo", m, ""), + Image: "golang:latest", + Name: "nested_golang_foo", + Pull: "not_present", + Number: 5, + }, + }, + }, + { + Name: "nested_golang_bar", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_nested_golang_bar_nested_golang_bar", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from bar", m, ""), + Image: "golang:latest", + Name: "nested_golang_bar", + Pull: "not_present", + Number: 6, + }, + }, + }, + { + Name: "nested_golang_star", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_nested_golang_star_nested_golang_star", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from star", m, ""), + Image: "golang:latest", + Name: "nested_golang_star", + Pull: "not_present", + Number: 7, + }, + }, + }, + { + Name: "nested_starlark_foo", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_nested_starlark_foo_nested_starlark_build_foo", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from foo", m, ""), + Image: "alpine", + Name: "nested_starlark_build_foo", + Pull: "not_present", + Number: 8, + }, + }, + }, + { + Name: "nested_starlark_bar", + Needs: []string{"clone"}, + Environment: initEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_nested_starlark_bar_nested_starlark_build_bar", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from bar", m, ""), + Image: "alpine", + Name: "nested_starlark_build_bar", + Pull: "not_present", + Number: 9, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "root steps", + args: args{ + file: "testdata/inline_with_steps.yml", + }, + want: &pipeline.Build{ + Version: "1", + ID: "__0", + Metadata: pipeline.Metadata{ + Clone: true, + Environment: []string{"steps", "services", "secrets"}, + }, + Steps: []*pipeline.Container{ + { + ID: "step___0_init", + Directory: "/vela/src/foo//", + Environment: initEnv, + Name: "init", + Image: "#init", + Number: 1, + Pull: "not_present", + }, + { + ID: "step___0_clone", + Directory: "/vela/src/foo//", + Environment: initEnv, + Name: "clone", + Image: defaultCloneImage, + Number: 2, + Pull: "not_present", + }, + { + ID: "step___0_test", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Directory: "/vela/src/foo//", + Environment: generateTestEnv("echo from inline", m, ""), + Name: "test", + Image: "alpine", + Number: 3, + Pull: "not_present", + }, + { + ID: "step___0_golang_foo", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Directory: "/vela/src/foo//", + Environment: generateTestEnv("echo hello from foo", m, ""), + Name: "golang_foo", + Image: "alpine", + Number: 4, + Pull: "not_present", + }, + { + ID: "step___0_golang_bar", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Directory: "/vela/src/foo//", + Environment: generateTestEnv("echo hello from bar", m, ""), + Name: "golang_bar", + Image: "alpine", + Number: 5, + Pull: "not_present", + }, + { + ID: "step___0_golang_star", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Directory: "/vela/src/foo//", + Environment: generateTestEnv("echo hello from star", m, ""), + Name: "golang_star", + Image: "alpine", + Number: 6, + Pull: "not_present", + }, + { + ID: "step___0_starlark_build_foo", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Directory: "/vela/src/foo//", + Environment: generateTestEnv("echo hello from foo", m, ""), + Name: "starlark_build_foo", + Image: "alpine", + Number: 7, + Pull: "not_present", + }, + { + ID: "step___0_starlark_build_bar", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Directory: "/vela/src/foo//", + Environment: generateTestEnv("echo hello from bar", m, ""), + Name: "starlark_build_bar", + Image: "alpine", + Number: 8, + Pull: "not_present", + }, + }, + }, + }, + { + name: "stages and steps", + args: args{ + file: "testdata/inline_with_stages_and_steps.yml", + }, + want: nil, + wantErr: true, + }, + { + name: "circular template call", + args: args{ + file: "testdata/inline_circular_template.yml", + }, + want: nil, + wantErr: true, + }, + { + name: "secrets", + args: args{ + file: "testdata/inline_with_secrets.yml", + }, + want: &pipeline.Build{ + Version: "1", + ID: "__0", + Metadata: pipeline.Metadata{ + Clone: true, + Environment: []string{"steps", "services", "secrets"}, + }, + Steps: []*pipeline.Container{ + { + ID: "step___0_init", + Directory: "/vela/src/foo//", + Environment: initEnv, + Name: "init", + Image: "#init", + Number: 1, + Pull: "not_present", + }, + { + ID: "step___0_clone", + Directory: "/vela/src/foo//", + Environment: initEnv, + Name: "clone", + Image: defaultCloneImage, + Number: 2, + Pull: "not_present", + }, + { + ID: "step___0_test", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Directory: "/vela/src/foo//", + Environment: generateTestEnv("echo from inline", m, ""), + Name: "test", + Image: "alpine", + Number: 3, + Pull: "not_present", + }, + }, + Secrets: pipeline.SecretSlice{ + &pipeline.Secret{ + Name: "foo_username", + Key: "org/repo/foo/username", + Engine: "native", + Type: "repo", + Origin: &pipeline.Container{}, + }, + &pipeline.Secret{ + Name: "docker_username", + Key: "org/repo/docker/username", + Engine: "native", + Type: "repo", + Origin: &pipeline.Container{}, + }, + &pipeline.Secret{ + Name: "docker_password", + Key: "org/repo/docker/password", + Engine: "vault", + Type: "repo", + Origin: &pipeline.Container{}, + }, + &pipeline.Secret{ + Name: "docker_username", + Key: "org/docker/username", + Engine: "native", + Type: "org", + Origin: &pipeline.Container{}, + }, + &pipeline.Secret{ + Name: "docker_password", + Key: "org/docker/password", + Engine: "vault", + Type: "org", + Origin: &pipeline.Container{}, + }, + &pipeline.Secret{ + Name: "docker_username", + Key: "org/team/docker/username", + Engine: "native", + Type: "shared", + Origin: &pipeline.Container{}, + }, + &pipeline.Secret{ + Name: "docker_password", + Key: "org/team/docker/password", + Engine: "vault", + Type: "shared", + Origin: &pipeline.Container{}, + }, + }, + }, + wantErr: false, + }, + { + name: "services", + args: args{ + file: "testdata/inline_with_services.yml", + }, + want: &pipeline.Build{ + Version: "1", + ID: "__0", + Metadata: pipeline.Metadata{ + Clone: true, + Environment: []string{"steps", "services", "secrets"}, + }, + Steps: []*pipeline.Container{ + { + ID: "step___0_init", + Directory: "/vela/src/foo//", + Environment: initEnv, + Name: "init", + Image: "#init", + Number: 1, + Pull: "not_present", + }, + { + ID: "step___0_clone", + Directory: "/vela/src/foo//", + Environment: initEnv, + Name: "clone", + Image: defaultCloneImage, + Number: 2, + Pull: "not_present", + }, + { + ID: "step___0_test", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Entrypoint: []string{"/bin/sh", "-c"}, + Directory: "/vela/src/foo//", + Environment: generateTestEnv("echo from inline", m, ""), + Name: "test", + Image: "alpine", + Number: 3, + Pull: "not_present", + }, + }, + Services: []*pipeline.Container{ + { + ID: "service___0_postgres", + Detach: true, + Environment: initEnv, + Image: "postgres:latest", + Name: "postgres", + Number: 1, + Pull: "not_present", + }, + { + ID: "service___0_cache", + Detach: true, + Environment: initEnv, + Image: "redis", + Name: "cache", + Number: 2, + Pull: "not_present", + }, + { + ID: "service___0_database", + Detach: true, + Environment: initEnv, + Image: "mongo", + Name: "database", + Number: 3, + Pull: "not_present", + }, + }, + }, + wantErr: false, + }, + { + name: "environment", + args: args{ + file: "testdata/inline_with_environment.yml", + }, + want: &pipeline.Build{ + Version: "1", + ID: "__0", + Metadata: pipeline.Metadata{ + Clone: true, + Environment: []string{"steps", "services", "secrets"}, + }, + Steps: []*pipeline.Container{ + { + ID: "step___0_init", + Directory: "/vela/src/foo//", + Environment: testEnv, + Name: "init", + Image: "#init", + Number: 1, + Pull: "not_present", + }, + { + ID: "step___0_clone", + Directory: "/vela/src/foo//", + Environment: testEnv, + Name: "clone", + Image: defaultCloneImage, + Number: 2, + Pull: "not_present", + }, + { + ID: "step___0_test", + Directory: "/vela/src/foo//", + Environment: stepEnv, + Name: "test", + Image: "alpine", + Number: 3, + Pull: "not_present", + }, + }, + }, + }, + { + name: "golang base", + args: args{ + file: "testdata/inline_with_golang.yml", + pipelineType: constants.PipelineTypeGo, + }, + want: &pipeline.Build{ + Version: "1", + ID: "__0", + Metadata: pipeline.Metadata{ + Clone: true, + Environment: []string{"steps", "services", "secrets"}, + }, + Stages: []*pipeline.Stage{ + { + Name: "init", + Environment: golangEnv, + Steps: pipeline.ContainerSlice{ + &pipeline.Container{ + ID: "__0_init_init", + Directory: "/vela/src/foo//", + Environment: golangEnv, + Image: "#init", + Name: "init", + Number: 1, + Pull: "not_present", + }, + }, + }, + { + Name: "clone", + Environment: golangEnv, + Steps: pipeline.ContainerSlice{ + &pipeline.Container{ + ID: "__0_clone_clone", + Directory: "/vela/src/foo//", + Environment: golangEnv, + Image: defaultCloneImage, + Name: "clone", + Number: 2, + Pull: "not_present", + }, + }, + }, + { + Name: "foo", + Needs: []string{"clone"}, + Environment: golangEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_foo_foo", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo from inline foo", m, constants.PipelineTypeGo), + Image: "alpine", + Name: "foo", + Pull: "not_present", + Number: 3, + }, + }, + }, + { + Name: "bar", + Needs: []string{"clone"}, + Environment: golangEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_bar_bar", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo from inline bar", m, constants.PipelineTypeGo), + Image: "alpine", + Name: "bar", + Pull: "not_present", + Number: 4, + }, + }, + }, + { + Name: "star", + Needs: []string{"clone"}, + Environment: golangEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_star_star", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo from inline star", m, constants.PipelineTypeGo), + Image: "alpine", + Name: "star", + Pull: "not_present", + Number: 5, + }, + }, + }, + { + Name: "golang_foo", + Needs: []string{"clone"}, + Environment: golangEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_golang_foo_golang_foo", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from foo", m, constants.PipelineTypeGo), + Image: "golang:latest", + Name: "golang_foo", + Pull: "not_present", + Number: 6, + }, + }, + }, + { + Name: "golang_bar", + Needs: []string{"clone"}, + Environment: golangEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_golang_bar_golang_bar", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from bar", m, constants.PipelineTypeGo), + Image: "golang:latest", + Name: "golang_bar", + Pull: "not_present", + Number: 7, + }, + }, + }, + { + Name: "golang_star", + Needs: []string{"clone"}, + Environment: golangEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_golang_star_golang_star", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from star", m, constants.PipelineTypeGo), + Image: "golang:latest", + Name: "golang_star", + Pull: "not_present", + Number: 8, + }, + }, + }, + { + Name: "starlark_foo", + Needs: []string{"clone"}, + Environment: golangEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_starlark_foo_starlark_build_foo", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from foo", m, constants.PipelineTypeGo), + Image: "alpine", + Name: "starlark_build_foo", + Pull: "not_present", + Number: 9, + }, + }, + }, + { + Name: "starlark_bar", + Needs: []string{"clone"}, + Environment: golangEnv, + Steps: []*pipeline.Container{ + { + ID: "__0_starlark_bar_starlark_build_bar", + Commands: []string{"echo $VELA_BUILD_SCRIPT | base64 -d | /bin/sh -e"}, + Directory: "/vela/src/foo//", + Entrypoint: []string{"/bin/sh", "-c"}, + Environment: generateTestEnv("echo hello from bar", m, constants.PipelineTypeGo), + Image: "alpine", + Name: "starlark_build_bar", + Pull: "not_present", + Number: 10, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + yaml, err := os.ReadFile(tt.args.file) + if err != nil { + t.Errorf("Reading yaml file return err: %v", err) + } + compiler, err := New(c) + if err != nil { + t.Errorf("Creating compiler returned err: %v", err) + } + + compiler.WithMetadata(m) + + if tt.args.pipelineType != "" { + compiler.WithRepo(&library.Repo{PipelineType: &tt.args.pipelineType}) + } + + got, _, err := compiler.Compile(yaml) + if (err != nil) != tt.wantErr { + t.Errorf("Compile() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // WARNING: hack to compare stages + // + // Channel values can only be compared for equality. + // Two channel values are considered equal if they + // originated from the same make call meaning they + // refer to the same channel value in memory. + if got != nil { + for i, stage := range got.Stages { + tmp := tt.want.Stages + + tmp[i].Done = stage.Done + } + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("Compile() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func Test_CompileLite(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + set := flag.NewFlagSet("test", 0) + set.Bool("github-driver", true, "doc") + set.String("github-url", s.URL, "doc") + set.String("github-token", "", "doc") + set.Int("max-template-depth", 5, "doc") + c := cli.NewContext(nil, set, nil) + + m := &types.Metadata{ + Database: &types.Database{ + Driver: "foo", + Host: "foo", + }, + Queue: &types.Queue{ + Channel: "foo", + Driver: "foo", + Host: "foo", + }, + Source: &types.Source{ + Driver: "foo", + Host: "foo", + }, + Vela: &types.Vela{ + Address: "foo", + WebAddress: "foo", + }, + } + + type args struct { + file string + pipelineType string + template bool + substitute bool + } + + tests := []struct { + name string + args args + want *yaml.Build + wantErr bool + }{ + { + name: "render_inline with stages", + args: args{ + file: "testdata/inline_with_stages.yml", + pipelineType: "", + template: true, + substitute: true, + }, + want: &yaml.Build{ + Version: "1", + Metadata: yaml.Metadata{ + RenderInline: true, + Environment: []string{"steps", "services", "secrets"}, + }, + Templates: []*yaml.Template{}, + Stages: []*yaml.Stage{ + { + Name: "test", + Needs: []string{"clone"}, + Steps: []*yaml.Step{ + { + Commands: raw.StringSlice{"echo from inline"}, + Image: "alpine", + Name: "test", + Pull: "not_present", + }, + }, + }, + { + Name: "golang_foo", + Needs: []string{"clone"}, + Steps: []*yaml.Step{ + { + Commands: raw.StringSlice{"echo hello from foo"}, + Image: "golang:latest", + Name: "golang_foo", + Pull: "not_present", + }, + }, + }, + { + Name: "golang_bar", + Needs: []string{"clone"}, + Steps: []*yaml.Step{ + { + Commands: raw.StringSlice{"echo hello from bar"}, + Image: "golang:latest", + Name: "golang_bar", + Pull: "not_present", + }, + }, + }, + { + Name: "golang_star", + Needs: []string{"clone"}, + Steps: []*yaml.Step{ + { + Commands: raw.StringSlice{"echo hello from star"}, + Image: "golang:latest", + Name: "golang_star", + Pull: "not_present", + }, + }, + }, + { + Name: "starlark_foo", + Needs: []string{"clone"}, + Steps: []*yaml.Step{ + { + Commands: raw.StringSlice{"echo hello from foo"}, + Image: "alpine", + Name: "starlark_build_foo", + Pull: "not_present", + }, + }, + }, + { + Name: "starlark_bar", + Needs: []string{"clone"}, + Steps: []*yaml.Step{ + { + Commands: raw.StringSlice{"echo hello from bar"}, + Image: "alpine", + Name: "starlark_build_bar", + Pull: "not_present", + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "render_inline with steps", + args: args{ + file: "testdata/inline_with_steps.yml", + pipelineType: "", + template: true, + substitute: true, + }, + want: &yaml.Build{ + Version: "1", + Metadata: yaml.Metadata{ + RenderInline: true, + Environment: []string{"steps", "services", "secrets"}, + }, + Steps: yaml.StepSlice{ + { + Commands: raw.StringSlice{"echo from inline"}, + Image: "alpine", + Name: "test", + Pull: "not_present", + }, + { + Commands: raw.StringSlice{"echo hello from foo"}, + Image: "alpine", + Name: "golang_foo", + Pull: "not_present", + }, + { + Commands: raw.StringSlice{"echo hello from bar"}, + Image: "alpine", + Name: "golang_bar", + Pull: "not_present", + }, + { + Commands: raw.StringSlice{"echo hello from star"}, + Image: "alpine", + Name: "golang_star", + Pull: "not_present", + }, + { + Commands: raw.StringSlice{"echo hello from foo"}, + Image: "alpine", + Name: "starlark_build_foo", + Pull: "not_present", + }, + { + Commands: raw.StringSlice{"echo hello from bar"}, + Image: "alpine", + Name: "starlark_build_bar", + Pull: "not_present", + }, + }, + Templates: yaml.TemplateSlice{}, + }, + wantErr: false, + }, + { + name: "golang", + args: args{ + file: "testdata/golang_inline_stages.yml", + pipelineType: "golang", + template: false, + substitute: false, + }, + want: &yaml.Build{ + Version: "1", + Metadata: yaml.Metadata{ + Environment: []string{"steps", "services", "secrets"}, + }, + Stages: []*yaml.Stage{ + { + Name: "foo", + Needs: []string{"clone"}, + Steps: []*yaml.Step{ + { + Commands: raw.StringSlice{"echo hello from foo"}, + Image: "alpine", + Name: "foo", + Pull: "not_present", + }, + }, + }, + { + Name: "bar", + Needs: []string{"clone"}, + Steps: []*yaml.Step{ + { + Commands: raw.StringSlice{"echo hello from bar"}, + Image: "alpine", + Name: "bar", + Pull: "not_present", + }, + }, + }, + { + Name: "star", + Needs: []string{"clone"}, + Steps: []*yaml.Step{ + { + Commands: raw.StringSlice{"echo hello from star"}, + Image: "alpine", + Name: "star", + Pull: "not_present", + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "step with template", + args: args{ + file: "testdata/step_inline_template.yml", + pipelineType: "", + template: false, + substitute: false, + }, + want: nil, + wantErr: true, + }, + { + name: "stage with template", + args: args{ + file: "testdata/stage_inline_template.yml", + pipelineType: "", + template: false, + substitute: false, + }, + want: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compiler, err := New(c) + if err != nil { + t.Errorf("Creating compiler returned err: %v", err) + } + + compiler.WithMetadata(m) + if tt.args.pipelineType != "" { + compiler.WithRepo(&library.Repo{PipelineType: &tt.args.pipelineType}) + } + + yaml, err := os.ReadFile(tt.args.file) + if err != nil { + t.Errorf("Reading yaml file return err: %v", err) + } + + got, _, err := compiler.CompileLite(yaml, tt.args.template, tt.args.substitute) + if (err != nil) != tt.wantErr { + t.Errorf("CompileLite() error = %v, wantErr %v", err, tt.wantErr) + return + } + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("CompileLite() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/compiler/native/doc.go b/compiler/native/doc.go index 3de2c7a3a..81877d3da 100644 --- a/compiler/native/doc.go +++ b/compiler/native/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/compiler/native" +// import "github.com/go-vela/server/compiler/native" package native diff --git a/compiler/native/environment.go b/compiler/native/environment.go index bfe26262a..7536731a6 100644 --- a/compiler/native/environment.go +++ b/compiler/native/environment.go @@ -18,7 +18,6 @@ import ( // EnvironmentStages injects environment variables // for each stage in a yaml configuration. -// nolint:lll // ignore function line length func (c *client) EnvironmentStages(s yaml.StageSlice, globalEnv raw.StringSliceMap) (yaml.StageSlice, error) { // iterate through all stages for _, stage := range s { @@ -33,7 +32,6 @@ func (c *client) EnvironmentStages(s yaml.StageSlice, globalEnv raw.StringSliceM // EnvironmentStage injects environment variables // for each stage in a yaml configuration. -// nolint:lll // ignore function line length func (c *client) EnvironmentStage(s *yaml.Stage, globalEnv raw.StringSliceMap) (*yaml.Stage, error) { // make empty map of environment variables env := make(map[string]string) @@ -74,7 +72,6 @@ func (c *client) EnvironmentStage(s *yaml.Stage, globalEnv raw.StringSliceMap) ( // EnvironmentSteps injects environment variables // for each step in a stage for the yaml configuration. -// nolint:lll // ignore function line length func (c *client) EnvironmentSteps(s yaml.StepSlice, stageEnv raw.StringSliceMap) (yaml.StepSlice, error) { // iterate through all steps for _, step := range s { @@ -101,7 +98,6 @@ func (c *client) EnvironmentStep(s *yaml.Step, stageEnv raw.StringSliceMap) (*ya // capture all environment variables from the local environment for _, e := range os.Environ() { // split the environment variable on = into a key value pair - // nolint: gomnd // ignore magic number parts := strings.SplitN(e, "=", 2) env[parts[0]] = parts[1] @@ -150,7 +146,6 @@ func (c *client) EnvironmentStep(s *yaml.Step, stageEnv raw.StringSliceMap) (*ya // EnvironmentServices injects environment variables // for each service in a yaml configuration. -// nolint:lll // ignore function line length func (c *client) EnvironmentServices(s yaml.ServiceSlice, globalEnv raw.StringSliceMap) (yaml.ServiceSlice, error) { // iterate through all services for _, service := range s { @@ -186,7 +181,6 @@ func (c *client) EnvironmentServices(s yaml.ServiceSlice, globalEnv raw.StringSl // EnvironmentSecrets injects environment variables // for each secret plugin in a yaml configuration. -// nolint:lll // ignore function line length func (c *client) EnvironmentSecrets(s yaml.SecretSlice, globalEnv raw.StringSliceMap) (yaml.SecretSlice, error) { // iterate through all secrets for _, secret := range s { @@ -205,7 +199,6 @@ func (c *client) EnvironmentSecrets(s yaml.SecretSlice, globalEnv raw.StringSlic // capture all environment variables from the local environment for _, e := range os.Environ() { // split the environment variable on = into a key value pair - // nolint: gomnd // ignore magic number parts := strings.SplitN(e, "=", 2) env[parts[0]] = parts[1] @@ -264,7 +257,6 @@ func (c *client) EnvironmentBuild() map[string]string { // capture all environment variables from the local environment for _, e := range os.Environ() { // split the environment variable on = into a key value pair - // nolint: gomnd // ignore magic number parts := strings.SplitN(e, "=", 2) env[parts[0]] = parts[1] @@ -292,8 +284,6 @@ func appendMap(originalMap, otherMap map[string]string) map[string]string { } // helper function that creates the standard set of environment variables for a pipeline. -// -// nolint: lll // ignore line length due to number of parameters provided func environment(b *library.Build, m *types.Metadata, r *library.Repo, u *library.User) map[string]string { // set default workspace workspace := constants.WorkspaceDefault @@ -316,7 +306,7 @@ func environment(b *library.Build, m *types.Metadata, r *library.Repo, u *librar env["VELA_RUNTIME"] = notImplemented env["VELA_SOURCE"] = notImplemented env["VELA_VERSION"] = notImplemented - env["CI"] = "vela" + env["CI"] = "true" // populate environment variables from metadata if m != nil { diff --git a/compiler/native/environment_test.go b/compiler/native/environment_test.go index 8ad6c7ccd..cba5e9242 100644 --- a/compiler/native/environment_test.go +++ b/compiler/native/environment_test.go @@ -126,7 +126,7 @@ func TestNative_EnvironmentSteps(t *testing.T) { "BUILD_STATUS": "", "BUILD_TITLE": "", "BUILD_WORKSPACE": "/vela/src", - "CI": "vela", + "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", @@ -156,6 +156,7 @@ func TestNative_EnvironmentSteps(t *testing.T) { "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "0", "VELA_BUILD_EVENT": "", + "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "", @@ -184,6 +185,7 @@ func TestNative_EnvironmentSteps(t *testing.T) { "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "", + "VELA_REPO_TOPICS": "", "VELA_REPO_BUILD_LIMIT": "0", "VELA_REPO_CLONE": "", "VELA_REPO_FULL_NAME": "", @@ -219,8 +221,8 @@ func TestNative_EnvironmentSteps(t *testing.T) { t.Errorf("EnvironmentSteps returned err: %v", err) } - if !reflect.DeepEqual(got, want) { - t.Errorf("EnvironmentSteps is %v, want %v", got, want) + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("EnvironmentSteps mismatch (-want +got):\n%s", diff) } } @@ -273,7 +275,7 @@ func TestNative_EnvironmentServices(t *testing.T) { "BUILD_STATUS": "", "BUILD_TITLE": "", "BUILD_WORKSPACE": "/vela/src", - "CI": "vela", + "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", @@ -303,6 +305,7 @@ func TestNative_EnvironmentServices(t *testing.T) { "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "0", "VELA_BUILD_EVENT": "", + "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "", @@ -331,6 +334,7 @@ func TestNative_EnvironmentServices(t *testing.T) { "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "", + "VELA_REPO_TOPICS": "", "VELA_REPO_BUILD_LIMIT": "0", "VELA_REPO_CLONE": "", "VELA_REPO_FULL_NAME": "", @@ -366,8 +370,8 @@ func TestNative_EnvironmentServices(t *testing.T) { t.Errorf("EnvironmentServices returned err: %v", err) } - if !reflect.DeepEqual(got, want) { - t.Errorf("EnvironmentServices is %v, want %v", got, want) + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("EnvironmentServices mismatch (-want +got):\n%s", diff) } } @@ -431,7 +435,7 @@ func TestNative_EnvironmentSecrets(t *testing.T) { "BUILD_STATUS": "", "BUILD_TITLE": "", "BUILD_WORKSPACE": "/vela/src", - "CI": "vela", + "CI": "true", "PARAMETER_FOO": "bar", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", @@ -462,6 +466,7 @@ func TestNative_EnvironmentSecrets(t *testing.T) { "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "0", "VELA_BUILD_EVENT": "", + "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "", @@ -490,6 +495,7 @@ func TestNative_EnvironmentSecrets(t *testing.T) { "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "", + "VELA_REPO_TOPICS": "", "VELA_REPO_BUILD_LIMIT": "0", "VELA_REPO_CLONE": "", "VELA_REPO_FULL_NAME": "", @@ -526,8 +532,8 @@ func TestNative_EnvironmentSecrets(t *testing.T) { t.Errorf("EnvironmentSecrets returned err: %v", err) } - if !reflect.DeepEqual(got, want) { - t.Errorf("EnvironmentSecrets is %v, want %v", got, want) + if diff := cmp.Diff(got, want); diff != "" { + t.Errorf("EnvironmentSecrets mismatch (-want +got):\n%s", diff) } } @@ -538,6 +544,7 @@ func TestNative_environment(t *testing.T) { num64 := int64(num) str := "foo" workspace := "/vela/src/foo/foo/foo" + topics := []string{"cloud", "security"} // push push := "push" // tag @@ -545,6 +552,7 @@ func TestNative_environment(t *testing.T) { tagref := "refs/tags/1" // pull_request pull := "pull_request" + pullact := "opened" pullref := "refs/pull/1/head" // deployment deploy := "deployment" @@ -563,36 +571,36 @@ func TestNative_environment(t *testing.T) { w: workspace, b: &library.Build{ID: &num64, RepoID: &num64, Number: &num, Parent: &num, Event: &push, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &str, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, Author: &str, Branch: &str, Ref: &str, BaseRef: &str}, m: &types.Metadata{Database: &types.Database{Driver: str, Host: str}, Queue: &types.Queue{Channel: str, Driver: str, Host: str}, Source: &types.Source{Driver: str, Host: str}, Vela: &types.Vela{Address: str, WebAddress: str}}, - r: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, + r: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, u: &library.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "push", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "foo", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "vela", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "push", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "foo", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "push", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "foo", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "push", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "foo", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, }, // tag { w: workspace, b: &library.Build{ID: &num64, RepoID: &num64, Number: &num, Parent: &num, Event: &tag, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &str, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, Author: &str, Branch: &str, Ref: &tagref, BaseRef: &str}, m: &types.Metadata{Database: &types.Database{Driver: str, Host: str}, Queue: &types.Queue{Channel: str, Driver: str, Host: str}, Source: &types.Source{Driver: str, Host: str}, Vela: &types.Vela{Address: str, WebAddress: str}}, - r: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, + r: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, u: &library.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "tag", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/tags/1", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TAG": "1", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "vela", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "tag", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/tags/1", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TAG": "1", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "tag", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/tags/1", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TAG": "1", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "tag", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/tags/1", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TAG": "1", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, }, // pull_request { w: workspace, - b: &library.Build{ID: &num64, RepoID: &num64, Number: &num, Parent: &num, Event: &pull, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &str, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, Author: &str, Branch: &str, Ref: &pullref, BaseRef: &str}, + b: &library.Build{ID: &num64, RepoID: &num64, Number: &num, Parent: &num, Event: &pull, EventAction: &pullact, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &str, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, Author: &str, Branch: &str, Ref: &pullref, BaseRef: &str}, m: &types.Metadata{Database: &types.Database{Driver: str, Host: str}, Queue: &types.Queue{Channel: str, Driver: str, Host: str}, Source: &types.Source{Driver: str, Host: str}, Vela: &types.Vela{Address: str, WebAddress: str}}, - r: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, + r: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, u: &library.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "pull_request", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_PULL_REQUEST_NUMBER": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "vela", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "pull_request", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_PULL_REQUEST": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_PULL_REQUEST": "1", "VELA_PULL_REQUEST_SOURCE": "", "VELA_PULL_REQUEST_TARGET": "foo", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "pull_request", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_PULL_REQUEST_NUMBER": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "pull_request", "VELA_BUILD_EVENT_ACTION": "opened", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_PULL_REQUEST": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_PULL_REQUEST": "1", "VELA_PULL_REQUEST_SOURCE": "", "VELA_PULL_REQUEST_TARGET": "foo", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, }, // deployment { w: workspace, b: &library.Build{ID: &num64, RepoID: &num64, Number: &num, Parent: &num, Event: &deploy, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &target, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, Author: &str, Branch: &str, Ref: &pullref, BaseRef: &str}, m: &types.Metadata{Database: &types.Database{Driver: str, Host: str}, Queue: &types.Queue{Channel: str, Driver: str, Host: str}, Source: &types.Source{Driver: str, Host: str}, Vela: &types.Vela{Address: str, WebAddress: str}}, - r: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, + r: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, u: &library.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "deployment", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TARGET": "production", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "vela", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "deployment", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TARGET": "production", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DEPLOYMENT": "production", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + want: map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "deployment", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TARGET": "production", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "deployment", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TARGET": "production", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DEPLOYMENT": "production", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, }, } @@ -600,8 +608,8 @@ func TestNative_environment(t *testing.T) { for _, test := range tests { got := environment(test.b, test.m, test.r, test.u) - if !reflect.DeepEqual(got, test.want) { - t.Errorf("environment is %v, want %v", got, test.want) + if diff := cmp.Diff(got, test.want); diff != "" { + t.Errorf("environment mismatch (-want +got):\n%s", diff) } } } @@ -611,6 +619,7 @@ func Test_mergeMap(t *testing.T) { combinedMap map[string]string loopMap map[string]string } + tests := []struct { name string args args @@ -632,6 +641,7 @@ func Test_mergeMap(t *testing.T) { "VELA_TEST": "foo", }}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := appendMap(tt.args.combinedMap, tt.args.loopMap); !reflect.DeepEqual(got, tt.want) { @@ -647,6 +657,7 @@ func Test_client_EnvironmentBuild(t *testing.T) { num := 1 num64 := int64(num) str := "foo" + topics := []string{"cloud", "security"} //workspace := "/vela/src/foo/foo/foo" // push push := "push" @@ -655,16 +666,19 @@ func Test_client_EnvironmentBuild(t *testing.T) { tagref := "refs/tags/1" // pull_request pull := "pull_request" + pullact := "opened" pullref := "refs/pull/1/head" // deployment deploy := "deployment" target := "production" + type fields struct { build *library.Build metadata *types.Metadata repo *library.Repo user *library.User } + tests := []struct { name string fields fields @@ -673,29 +687,29 @@ func Test_client_EnvironmentBuild(t *testing.T) { {"push", fields{ build: &library.Build{ID: &num64, RepoID: &num64, Number: &num, Parent: &num, Event: &push, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &str, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, Author: &str, Branch: &str, Ref: &str, BaseRef: &str}, metadata: &types.Metadata{Database: &types.Database{Driver: str, Host: str}, Queue: &types.Queue{Channel: str, Driver: str, Host: str}, Source: &types.Source{Driver: str, Host: str}, Vela: &types.Vela{Address: str, WebAddress: str}}, - repo: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, + repo: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, user: &library.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "push", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "foo", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "vela", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "push", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "foo", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}}, + }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "push", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "foo", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "push", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "foo", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}}, {"tag", fields{ build: &library.Build{ID: &num64, RepoID: &num64, Number: &num, Parent: &num, Event: &tag, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &str, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, Author: &str, Branch: &str, Ref: &tagref, BaseRef: &str}, metadata: &types.Metadata{Database: &types.Database{Driver: str, Host: str}, Queue: &types.Queue{Channel: str, Driver: str, Host: str}, Source: &types.Source{Driver: str, Host: str}, Vela: &types.Vela{Address: str, WebAddress: str}}, - repo: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, + repo: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, user: &library.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "tag", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/tags/1", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TAG": "1", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "vela", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "tag", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/tags/1", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TAG": "1", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "tag", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/tags/1", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TAG": "1", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "tag", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/tags/1", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TAG": "1", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, }, {"pull_request", fields{ - build: &library.Build{ID: &num64, RepoID: &num64, Number: &num, Parent: &num, Event: &pull, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &str, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, Author: &str, Branch: &str, Ref: &pullref, BaseRef: &str}, + build: &library.Build{ID: &num64, RepoID: &num64, Number: &num, Parent: &num, Event: &pull, EventAction: &pullact, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &str, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, Author: &str, Branch: &str, Ref: &pullref, BaseRef: &str}, metadata: &types.Metadata{Database: &types.Database{Driver: str, Host: str}, Queue: &types.Queue{Channel: str, Driver: str, Host: str}, Source: &types.Source{Driver: str, Host: str}, Vela: &types.Vela{Address: str, WebAddress: str}}, - repo: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, + repo: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, user: &library.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "pull_request", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_PULL_REQUEST_NUMBER": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "vela", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "pull_request", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_PULL_REQUEST": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_PULL_REQUEST": "1", "VELA_PULL_REQUEST_SOURCE": "", "VELA_PULL_REQUEST_TARGET": "foo", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "pull_request", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_PULL_REQUEST_NUMBER": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "pull_request", "VELA_BUILD_EVENT_ACTION": "opened", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_PULL_REQUEST": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_PULL_REQUEST": "1", "VELA_PULL_REQUEST_SOURCE": "", "VELA_PULL_REQUEST_TARGET": "foo", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, }, {"deployment", fields{ build: &library.Build{ID: &num64, RepoID: &num64, Number: &num, Parent: &num, Event: &deploy, Status: &str, Error: &str, Enqueued: &num64, Created: &num64, Started: &num64, Finished: &num64, Deploy: &target, Clone: &str, Source: &str, Title: &str, Message: &str, Commit: &str, Sender: &str, Author: &str, Branch: &str, Ref: &pullref, BaseRef: &str}, metadata: &types.Metadata{Database: &types.Database{Driver: str, Host: str}, Queue: &types.Queue{Channel: str, Driver: str, Host: str}, Source: &types.Source{Driver: str, Host: str}, Vela: &types.Vela{Address: str, WebAddress: str}}, - repo: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, + repo: &library.Repo{ID: &num64, UserID: &num64, Org: &str, Name: &str, FullName: &str, Link: &str, Clone: &str, Branch: &str, Topics: &topics, BuildLimit: &num64, Timeout: &num64, Visibility: &str, Private: &booL, Trusted: &booL, Active: &booL, AllowPull: &booL, AllowPush: &booL, AllowDeploy: &booL, AllowTag: &booL, AllowComment: &booL}, user: &library.User{ID: &num64, Name: &str, Token: &str, Active: &booL, Admin: &booL}, - }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "deployment", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TARGET": "production", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "vela", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "deployment", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TARGET": "production", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DEPLOYMENT": "production", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, + }, map[string]string{"BUILD_AUTHOR": "foo", "BUILD_AUTHOR_EMAIL": "", "BUILD_BASE_REF": "foo", "BUILD_BRANCH": "foo", "BUILD_CHANNEL": "foo", "BUILD_CLONE": "foo", "BUILD_COMMIT": "foo", "BUILD_CREATED": "1", "BUILD_ENQUEUED": "1", "BUILD_EVENT": "deployment", "BUILD_HOST": "", "BUILD_LINK": "", "BUILD_MESSAGE": "foo", "BUILD_NUMBER": "1", "BUILD_PARENT": "1", "BUILD_REF": "refs/pull/1/head", "BUILD_SENDER": "foo", "BUILD_SOURCE": "foo", "BUILD_STARTED": "1", "BUILD_STATUS": "foo", "BUILD_TARGET": "production", "BUILD_TITLE": "foo", "BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "CI": "true", "REPOSITORY_ACTIVE": "false", "REPOSITORY_ALLOW_COMMENT": "false", "REPOSITORY_ALLOW_DEPLOY": "false", "REPOSITORY_ALLOW_PULL": "false", "REPOSITORY_ALLOW_PUSH": "false", "REPOSITORY_ALLOW_TAG": "false", "REPOSITORY_BRANCH": "foo", "REPOSITORY_CLONE": "foo", "REPOSITORY_FULL_NAME": "foo", "REPOSITORY_LINK": "foo", "REPOSITORY_NAME": "foo", "REPOSITORY_ORG": "foo", "REPOSITORY_PRIVATE": "false", "REPOSITORY_TIMEOUT": "1", "REPOSITORY_TRUSTED": "false", "REPOSITORY_VISIBILITY": "foo", "VELA": "true", "VELA_ADDR": "foo", "VELA_BUILD_AUTHOR": "foo", "VELA_BUILD_AUTHOR_EMAIL": "", "VELA_BUILD_BASE_REF": "foo", "VELA_BUILD_BRANCH": "foo", "VELA_BUILD_CHANNEL": "foo", "VELA_BUILD_CLONE": "foo", "VELA_BUILD_COMMIT": "foo", "VELA_BUILD_CREATED": "1", "VELA_BUILD_DISTRIBUTION": "", "VELA_BUILD_ENQUEUED": "1", "VELA_BUILD_EVENT": "deployment", "VELA_BUILD_EVENT_ACTION": "", "VELA_BUILD_HOST": "", "VELA_BUILD_LINK": "", "VELA_BUILD_MESSAGE": "foo", "VELA_BUILD_NUMBER": "1", "VELA_BUILD_PARENT": "1", "VELA_BUILD_REF": "refs/pull/1/head", "VELA_BUILD_RUNTIME": "", "VELA_BUILD_SENDER": "foo", "VELA_BUILD_SOURCE": "foo", "VELA_BUILD_STARTED": "1", "VELA_BUILD_STATUS": "foo", "VELA_BUILD_TARGET": "production", "VELA_BUILD_TITLE": "foo", "VELA_BUILD_WORKSPACE": "/vela/src/foo/foo/foo", "VELA_CHANNEL": "foo", "VELA_DATABASE": "foo", "VELA_DEPLOYMENT": "production", "VELA_DISTRIBUTION": "TODO", "VELA_HOST": "foo", "VELA_NETRC_MACHINE": "foo", "VELA_NETRC_PASSWORD": "foo", "VELA_NETRC_USERNAME": "x-oauth-basic", "VELA_QUEUE": "foo", "VELA_REPO_ACTIVE": "false", "VELA_REPO_ALLOW_COMMENT": "false", "VELA_REPO_ALLOW_DEPLOY": "false", "VELA_REPO_ALLOW_PULL": "false", "VELA_REPO_ALLOW_PUSH": "false", "VELA_REPO_ALLOW_TAG": "false", "VELA_REPO_BRANCH": "foo", "VELA_REPO_BUILD_LIMIT": "1", "VELA_REPO_CLONE": "foo", "VELA_REPO_FULL_NAME": "foo", "VELA_REPO_LINK": "foo", "VELA_REPO_NAME": "foo", "VELA_REPO_ORG": "foo", "VELA_REPO_PIPELINE_TYPE": "", "VELA_REPO_PRIVATE": "false", "VELA_REPO_TIMEOUT": "1", "VELA_REPO_TOPICS": "cloud,security", "VELA_REPO_TRUSTED": "false", "VELA_REPO_VISIBILITY": "foo", "VELA_RUNTIME": "TODO", "VELA_SOURCE": "foo", "VELA_USER_ACTIVE": "false", "VELA_USER_ADMIN": "false", "VELA_USER_FAVORITES": "[]", "VELA_USER_NAME": "foo", "VELA_VERSION": "TODO", "VELA_WORKSPACE": "/vela/src/foo/foo/foo"}, }, } for _, tt := range tests { @@ -706,8 +720,9 @@ func Test_client_EnvironmentBuild(t *testing.T) { repo: tt.fields.repo, user: tt.fields.user, } - if got := c.EnvironmentBuild(); !reflect.DeepEqual(got, tt.want) { - t.Errorf("EnvironmentBuild() = %v, want %v", got, tt.want) + got := c.EnvironmentBuild() + if diff := cmp.Diff(got, tt.want); diff != "" { + t.Errorf("EnvironmentBuild mismatch (-want +got):\n%s", diff) } }) } diff --git a/compiler/native/expand.go b/compiler/native/expand.go index 0b68c5f02..d593bc26e 100644 --- a/compiler/native/expand.go +++ b/compiler/native/expand.go @@ -8,6 +8,10 @@ import ( "fmt" "strings" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/pipeline" + + "github.com/go-vela/server/compiler/registry" "github.com/go-vela/server/compiler/template/native" "github.com/go-vela/server/compiler/template/starlark" "github.com/spf13/afero" @@ -19,44 +23,53 @@ import ( // ExpandStages injects the template for each // templated step in every stage in a yaml configuration. -// -// nolint: lll // ignore long line length due to variable names -func (c *client) ExpandStages(s *yaml.Build, tmpls map[string]*yaml.Template) (yaml.StageSlice, yaml.SecretSlice, yaml.ServiceSlice, raw.StringSliceMap, error) { +func (c *client) ExpandStages(s *yaml.Build, tmpls map[string]*yaml.Template, r *pipeline.RuleData) (*yaml.Build, error) { + if len(tmpls) == 0 { + return s, nil + } + // iterate through all stages for _, stage := range s.Stages { // inject the templates into the steps for the stage - steps, secrets, services, environment, err := c.ExpandSteps(&yaml.Build{Steps: stage.Steps, Secrets: s.Secrets, Services: s.Services, Environment: s.Environment}, tmpls) + p, err := c.ExpandSteps(&yaml.Build{Steps: stage.Steps, Secrets: s.Secrets, Services: s.Services, Environment: s.Environment}, tmpls, r, c.TemplateDepth) if err != nil { - return nil, nil, nil, nil, err + return nil, err } - stage.Steps = steps - s.Secrets = secrets - s.Services = services - s.Environment = environment + stage.Steps = p.Steps + s.Secrets = p.Secrets + s.Services = p.Services + s.Environment = p.Environment } - return s.Stages, s.Secrets, s.Services, s.Environment, nil + return s, nil } // ExpandSteps injects the template for each // templated step in a yaml configuration. -// -// nolint: lll,funlen,gocyclo // ignore long line length due to variable names -func (c *client) ExpandSteps(s *yaml.Build, tmpls map[string]*yaml.Template) (yaml.StepSlice, yaml.SecretSlice, yaml.ServiceSlice, raw.StringSliceMap, error) { +func (c *client) ExpandSteps(s *yaml.Build, tmpls map[string]*yaml.Template, r *pipeline.RuleData, depth int) (*yaml.Build, error) { + if len(tmpls) == 0 { + return s, nil + } + + // return if max template depth has been reached + if depth == 0 { + retErr := fmt.Errorf("max template depth of %d exceeded", c.TemplateDepth) + + return s, retErr + } + steps := yaml.StepSlice{} secrets := s.Secrets services := s.Services environment := s.Environment + if len(environment) == 0 { environment = make(raw.StringSliceMap) } // iterate through each step for _, step := range s.Steps { - // nolint: ineffassign,staticcheck // ignore ineffectual assignment - bytes := []byte{} - // skip if no template is provided for the step if len(step.Template.Name) == 0 { // add existing step if no template @@ -67,7 +80,23 @@ func (c *client) ExpandSteps(s *yaml.Build, tmpls map[string]*yaml.Template) (ya // lookup step template name tmpl, ok := tmpls[step.Template.Name] if !ok { - return yaml.StepSlice{}, yaml.SecretSlice{}, yaml.ServiceSlice{}, raw.StringSliceMap{}, fmt.Errorf("missing template source for template %s in pipeline for step %s", step.Template.Name, step.Name) + return s, fmt.Errorf("missing template source for template %s in pipeline for step %s", step.Template.Name, step.Name) + } + + // if ruledata is nil (CompileLite), continue with expansion + if r != nil { + // form a one-step pipeline to prep for purge check + check := &yaml.StepSlice{step} + pipeline := &pipeline.Build{ + Steps: *check.ToPipeline(), + } + + pipeline = pipeline.Purge(r) + + // if step purged, do not proceed with expansion + if len(pipeline.Steps) == 0 { + continue + } } // Create some default global environment inject vars @@ -82,83 +111,34 @@ func (c *client) ExpandSteps(s *yaml.Build, tmpls map[string]*yaml.Template) (ya // inject environment information for template step, err := c.EnvironmentStep(step, envGlobalSteps) if err != nil { - return yaml.StepSlice{}, yaml.SecretSlice{}, yaml.ServiceSlice{}, raw.StringSliceMap{}, err + return s, err } - switch { - case c.local: - a := &afero.Afero{ - Fs: afero.NewOsFs(), - } - - bytes, err = a.ReadFile(tmpl.Source) - if err != nil { - return yaml.StepSlice{}, yaml.SecretSlice{}, yaml.ServiceSlice{}, raw.StringSliceMap{}, err - } - - case strings.EqualFold(tmpl.Type, "github"): - // parse source from template - src, err := c.Github.Parse(tmpl.Source) - if err != nil { - return yaml.StepSlice{}, yaml.SecretSlice{}, yaml.ServiceSlice{}, raw.StringSliceMap{}, fmt.Errorf("invalid template source provided for %s: %v", step.Template.Name, err) - } - - // pull from github without auth when the host isn't provided or is set to github.com - if !c.UsePrivateGithub && (len(src.Host) == 0 || strings.Contains(src.Host, "github.com")) { - logrus.WithFields(logrus.Fields{ - "org": src.Org, - "repo": src.Repo, - "path": src.Name, - "host": src.Host, - }).Tracef("Using GitHub client to pull template") - bytes, err = c.Github.Template(nil, src) - if err != nil { - return yaml.StepSlice{}, yaml.SecretSlice{}, yaml.ServiceSlice{}, raw.StringSliceMap{}, err - } - } else { - logrus.WithFields(logrus.Fields{ - "org": src.Org, - "repo": src.Repo, - "path": src.Name, - "host": src.Host, - }).Tracef("Using authenticated GitHub client to pull template") - // use private (authenticated) github instance to pull from - bytes, err = c.PrivateGithub.Template(c.user, src) - if err != nil { - return yaml.StepSlice{}, yaml.SecretSlice{}, yaml.ServiceSlice{}, raw.StringSliceMap{}, err - } - } - - default: - logrus.Errorf("Unsupported template type: %v", tmpl.Type) - continue + bytes, err := c.getTemplate(tmpl, step.Template.Name) + if err != nil { + return s, err } - var tmplSteps yaml.StepSlice - var tmplSecrets yaml.SecretSlice - var tmplServices yaml.ServiceSlice - var tmplEnvironment raw.StringSliceMap + tmplBuild, err := c.mergeTemplate(bytes, tmpl, step) + if err != nil { + return s, err + } - // TODO: provide friendlier error messages with file type mismatches - switch tmpl.Format { - case "go", "golang", "": - // render template for steps - tmplSteps, tmplSecrets, tmplServices, tmplEnvironment, err = native.RenderStep(string(bytes), step) - if err != nil { - return yaml.StepSlice{}, yaml.SecretSlice{}, yaml.ServiceSlice{}, raw.StringSliceMap{}, err + // if template references other templates, expand again + if len(tmplBuild.Templates) != 0 { + // if the tmplBuild has render_inline but the parent build does not, abort + if tmplBuild.Metadata.RenderInline && !s.Metadata.RenderInline { + return s, fmt.Errorf("cannot use render_inline inside a called template (%s)", step.Template.Name) } - case "starlark": - // render template for steps - tmplSteps, tmplSecrets, tmplServices, tmplEnvironment, err = starlark.RenderStep(string(bytes), step) + + tmplBuild, err = c.ExpandSteps(tmplBuild, mapFromTemplates(tmplBuild.Templates), r, depth-1) if err != nil { - return yaml.StepSlice{}, yaml.SecretSlice{}, yaml.ServiceSlice{}, raw.StringSliceMap{}, err + return s, err } - default: - return yaml.StepSlice{}, yaml.SecretSlice{}, yaml.ServiceSlice{}, raw.StringSliceMap{}, fmt.Errorf("format of %s is unsupported", tmpl.Format) } // loop over secrets within template - for _, secret := range tmplSecrets { + for _, secret := range tmplBuild.Secrets { found := false // loop over secrets within base configuration for _, sec := range secrets { @@ -175,8 +155,9 @@ func (c *client) ExpandSteps(s *yaml.Build, tmpls map[string]*yaml.Template) (ya } // loop over services within template - for _, service := range tmplServices { + for _, service := range tmplBuild.Services { found := false + for _, serv := range services { if serv.Name == service.Name { found = true @@ -190,8 +171,9 @@ func (c *client) ExpandSteps(s *yaml.Build, tmpls map[string]*yaml.Template) (ya } // loop over environment within template - for key, value := range tmplEnvironment { + for key, value := range tmplBuild.Environment { found := false + for env := range environment { if key == env { found = true @@ -205,10 +187,137 @@ func (c *client) ExpandSteps(s *yaml.Build, tmpls map[string]*yaml.Template) (ya } // add templated steps - steps = append(steps, tmplSteps...) + steps = append(steps, tmplBuild.Steps...) } - return steps, secrets, services, environment, nil + s.Steps = steps + s.Secrets = secrets + s.Services = services + s.Environment = environment + + return s, nil +} + +func (c *client) getTemplate(tmpl *yaml.Template, name string) ([]byte, error) { + var ( + bytes []byte + err error + ) + + switch { + case c.local: + // iterate over locally provided templates + for _, t := range c.localTemplates { + parts := strings.Split(t, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("local templates must be provided in the form :, got %s", t) + } + + if strings.EqualFold(tmpl.Name, parts[0]) { + a := &afero.Afero{ + Fs: afero.NewOsFs(), + } + + bytes, err = a.ReadFile(parts[1]) + if err != nil { + return bytes, err + } + + return bytes, nil + } + } + + // no template found in provided templates, exit with error + return nil, fmt.Errorf("unable to find template %s: not supplied in list %s", tmpl.Name, c.localTemplates) + + case strings.EqualFold(tmpl.Type, "github"): + // parse source from template + src, err := c.Github.Parse(tmpl.Source) + if err != nil { + return bytes, fmt.Errorf("invalid template source provided for %s: %w", name, err) + } + + // pull from github without auth when the host isn't provided or is set to github.com + if !c.UsePrivateGithub && (len(src.Host) == 0 || strings.Contains(src.Host, "github.com")) { + logrus.WithFields(logrus.Fields{ + "org": src.Org, + "repo": src.Repo, + "path": src.Name, + "host": src.Host, + }).Tracef("Using GitHub client to pull template") + + bytes, err = c.Github.Template(nil, src) + if err != nil { + return bytes, err + } + } else { + logrus.WithFields(logrus.Fields{ + "org": src.Org, + "repo": src.Repo, + "path": src.Name, + "host": src.Host, + }).Tracef("Using authenticated GitHub client to pull template") + + // use private (authenticated) github instance to pull from + bytes, err = c.PrivateGithub.Template(c.user, src) + if err != nil { + return bytes, err + } + } + + case strings.EqualFold(tmpl.Type, "file"): + src := ®istry.Source{ + Org: c.repo.GetOrg(), + Repo: c.repo.GetName(), + Name: tmpl.Source, + Ref: c.commit, + } + + if !c.UsePrivateGithub { + logrus.WithFields(logrus.Fields{ + "org": src.Org, + "repo": src.Repo, + "path": src.Name, + }).Tracef("Using GitHub client to pull template") + + bytes, err = c.Github.Template(nil, src) + if err != nil { + return bytes, err + } + } else { + logrus.WithFields(logrus.Fields{ + "org": src.Org, + "repo": src.Repo, + "path": src.Name, + }).Tracef("Using authenticated GitHub client to pull template") + + // use private (authenticated) github instance to pull from + bytes, err = c.PrivateGithub.Template(c.user, src) + if err != nil { + return bytes, err + } + } + + default: + return bytes, fmt.Errorf("unsupported template type: %v", tmpl.Type) + } + + return bytes, nil +} + +//nolint:lll // ignore long line length due to input arguments +func (c *client) mergeTemplate(bytes []byte, tmpl *yaml.Template, step *yaml.Step) (*yaml.Build, error) { + switch tmpl.Format { + case constants.PipelineTypeGo, "golang", "": + //nolint:lll // ignore long line length due to return + return native.Render(string(bytes), step.Name, step.Template.Name, step.Environment, step.Template.Variables) + case constants.PipelineTypeStarlark: + //nolint:lll // ignore long line length due to return + return starlark.Render(string(bytes), step.Name, step.Template.Name, step.Environment, step.Template.Variables) + default: + //nolint:lll // ignore long line length due to return + return &yaml.Build{}, fmt.Errorf("format of %s is unsupported", tmpl.Format) + } } // helper function that creates a map of templates from a yaml configuration. diff --git a/compiler/native/expand_test.go b/compiler/native/expand_test.go index 5dae3f99a..d2a8a92b6 100644 --- a/compiler/native/expand_test.go +++ b/compiler/native/expand_test.go @@ -11,6 +11,8 @@ import ( "reflect" "testing" + "github.com/go-vela/types/library" + "github.com/go-vela/types/pipeline" "github.com/go-vela/types/raw" "github.com/go-vela/types/yaml" "github.com/google/go-cmp/cmp" @@ -27,10 +29,12 @@ func TestNative_ExpandStages(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/api/v3/repos/foo/bar/contents/:path", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/template.json") + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) }) s := httptest.NewServer(engine) @@ -41,12 +45,13 @@ func TestNative_ExpandStages(t *testing.T) { set.Bool("github-driver", true, "doc") set.String("github-url", s.URL, "doc") set.String("github-token", "", "doc") + set.Int("max-template-depth", 5, "doc") c := cli.NewContext(nil, set, nil) tmpls := map[string]*yaml.Template{ "gradle": { Name: "gradle", - Source: "github.example.com/foo/bar/template.yml", + Source: "github.example.com/foo/bar/long_template.yml", Type: "github", }, } @@ -144,23 +149,24 @@ func TestNative_ExpandStages(t *testing.T) { t.Errorf("Creating new compiler returned err: %v", err) } - stages, secrets, services, environment, err := compiler.ExpandStages(&yaml.Build{Stages: stages, Services: yaml.ServiceSlice{}, Environment: raw.StringSliceMap{}}, tmpls) + build, err := compiler.ExpandStages(&yaml.Build{Stages: stages, Services: yaml.ServiceSlice{}, Environment: raw.StringSliceMap{}}, tmpls, new(pipeline.RuleData)) if err != nil { t.Errorf("ExpandStages returned err: %v", err) } - if diff := cmp.Diff(stages, wantStages); diff != "" { + if diff := cmp.Diff(build.Stages, wantStages); diff != "" { t.Errorf("ExpandStages() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(secrets, wantSecrets); diff != "" { + if diff := cmp.Diff(build.Secrets, wantSecrets); diff != "" { t.Errorf("ExpandStages() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(services, wantServices); diff != "" { + if diff := cmp.Diff(build.Services, wantServices); diff != "" { t.Errorf("ExpandStages() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(environment, wantEnvironment); diff != "" { + + if diff := cmp.Diff(build.Environment, wantEnvironment); diff != "" { t.Errorf("ExpandStages() mismatch (-want +got):\n%s", diff) } } @@ -173,10 +179,12 @@ func TestNative_ExpandSteps(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/api/v3/repos/foo/bar/contents/:path", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/template.json") + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) }) s := httptest.NewServer(engine) @@ -187,13 +195,38 @@ func TestNative_ExpandSteps(t *testing.T) { set.Bool("github-driver", true, "doc") set.String("github-url", s.URL, "doc") set.String("github-token", "", "doc") + set.Int("max-template-depth", 5, "doc") c := cli.NewContext(nil, set, nil) - tmpls := map[string]*yaml.Template{ - "gradle": { - Name: "gradle", - Source: "github.example.com/foo/bar/template.yml", - Type: "github", + testRepo := new(library.Repo) + + testRepo.SetID(1) + testRepo.SetOrg("foo") + testRepo.SetName("bar") + + tests := []struct { + name string + tmpls map[string]*yaml.Template + }{ + { + name: "GitHub", + tmpls: map[string]*yaml.Template{ + "gradle": { + Name: "gradle", + Source: "github.example.com/foo/bar/long_template.yml", + Type: "github", + }, + }, + }, + { + name: "File", + tmpls: map[string]*yaml.Template{ + "gradle": { + Name: "gradle", + Source: "long_template.yml", + Type: "file", + }, + }, }, } @@ -286,25 +319,31 @@ func TestNative_ExpandSteps(t *testing.T) { t.Errorf("Creating new compiler returned err: %v", err) } - steps, secrets, services, environment, err := compiler.ExpandSteps(&yaml.Build{Steps: steps, Services: yaml.ServiceSlice{}, Environment: globalEnvironment}, tmpls) - if err != nil { - t.Errorf("ExpandSteps returned err: %v", err) - } + compiler.WithCommit("123abc456def").WithRepo(testRepo) - if diff := cmp.Diff(steps, wantSteps); diff != "" { - t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + build, err := compiler.ExpandSteps(&yaml.Build{Steps: steps, Services: yaml.ServiceSlice{}, Environment: globalEnvironment}, test.tmpls, new(pipeline.RuleData), compiler.TemplateDepth) + if err != nil { + t.Errorf("ExpandSteps_Type%s returned err: %v", test.name, err) + } - if diff := cmp.Diff(secrets, wantSecrets); diff != "" { - t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) - } + if diff := cmp.Diff(build.Steps, wantSteps); diff != "" { + t.Errorf("ExpandSteps()_Type%s mismatch (-want +got):\n%s", test.name, diff) + } - if diff := cmp.Diff(services, wantServices); diff != "" { - t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) - } + if diff := cmp.Diff(build.Secrets, wantSecrets); diff != "" { + t.Errorf("ExpandSteps()_Type%s mismatch (-want +got):\n%s", test.name, diff) + } - if diff := cmp.Diff(environment, wantEnvironment); diff != "" { - t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(build.Services, wantServices); diff != "" { + t.Errorf("ExpandSteps()_Type%s mismatch (-want +got):\n%s", test.name, diff) + } + + if diff := cmp.Diff(build.Environment, wantEnvironment); diff != "" { + t.Errorf("ExpandSteps()_Type%s mismatch (-want +got):\n%s", test.name, diff) + } + }) } } @@ -316,15 +355,12 @@ func TestNative_ExpandStepsMulti(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/api/v3/repos/foo/bar/contents/:path", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/template-gradle.json") - }) - engine.GET("/api/v3/repos/bar/foo/contents/:path", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/template-maven.json") + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) }) s := httptest.NewServer(engine) @@ -335,6 +371,7 @@ func TestNative_ExpandStepsMulti(t *testing.T) { set.Bool("github-driver", true, "doc") set.String("github-url", s.URL, "doc") set.String("github-token", "", "doc") + set.Int("max-template-depth", 5, "doc") c := cli.NewContext(nil, set, nil) tmpls := map[string]*yaml.Template{ @@ -348,6 +385,11 @@ func TestNative_ExpandStepsMulti(t *testing.T) { Source: "github.example.com/bar/foo/maven.yml", Type: "github", }, + "npm": { + Name: "npm", + Source: "github.example.com/foo/bar/gradle.yml", + Type: "github", + }, } steps := yaml.StepSlice{ @@ -372,6 +414,27 @@ func TestNative_ExpandStepsMulti(t *testing.T) { "pull_policy": "pull: true", }, }, + Ruleset: yaml.Ruleset{ + If: yaml.Rules{ + Branch: []string{"main"}, + }, + }, + }, + &yaml.Step{ + Name: "sample", + Template: yaml.StepTemplate{ + Name: "npm", + Variables: map[string]interface{}{ + "image": "openjdk:latest", + "environment": "{ GRADLE_USER_HOME: .gradle, GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=1 -Dorg.gradle.parallel=false }", + "pull_policy": "pull: true", + }, + }, + Ruleset: yaml.Ruleset{ + If: yaml.Rules{ + Branch: []string{"dev"}, + }, + }, }, } @@ -476,7 +539,7 @@ func TestNative_ExpandStepsMulti(t *testing.T) { "auth_method": "token", "username": "octocat", "items": []interface{}{ - map[interface{}]interface{}{string("path"): string("docker"), string("source"): string("secret/docker")}, + map[interface{}]interface{}{"path": "docker", "source": "secret/docker"}, }, }, }, @@ -497,7 +560,7 @@ func TestNative_ExpandStepsMulti(t *testing.T) { "auth_method": "token", "username": "octocat", "items": []interface{}{ - map[interface{}]interface{}{string("path"): string("docker"), string("source"): string("secret/docker")}, + map[interface{}]interface{}{"path": "docker", "source": "secret/docker"}, }, }, }, @@ -520,24 +583,27 @@ func TestNative_ExpandStepsMulti(t *testing.T) { t.Errorf("Creating new compiler returned err: %v", err) } - steps, secrets, services, environment, err := compiler.ExpandSteps(&yaml.Build{Steps: steps, Services: yaml.ServiceSlice{}, Environment: raw.StringSliceMap{}}, tmpls) + ruledata := new(pipeline.RuleData) + ruledata.Branch = "main" + + build, err := compiler.ExpandSteps(&yaml.Build{Steps: steps, Services: yaml.ServiceSlice{}, Environment: raw.StringSliceMap{}}, tmpls, ruledata, compiler.TemplateDepth) if err != nil { t.Errorf("ExpandSteps returned err: %v", err) } - if diff := cmp.Diff(steps, wantSteps); diff != "" { + if diff := cmp.Diff(build.Steps, wantSteps); diff != "" { t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(secrets, wantSecrets); diff != "" { + if diff := cmp.Diff(build.Secrets, wantSecrets); diff != "" { t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(services, wantServices); diff != "" { + if diff := cmp.Diff(build.Services, wantServices); diff != "" { t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(environment, wantEnvironment); diff != "" { + if diff := cmp.Diff(build.Environment, wantEnvironment); diff != "" { t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) } } @@ -550,10 +616,12 @@ func TestNative_ExpandStepsStarlark(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/api/v3/repos/foo/bar/contents/:path", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/template-starlark.json") + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) }) s := httptest.NewServer(engine) @@ -564,6 +632,7 @@ func TestNative_ExpandStepsStarlark(t *testing.T) { set.Bool("github-driver", true, "doc") set.String("github-url", s.URL, "doc") set.String("github-token", "", "doc") + set.Int("max-template-depth", 5, "doc") c := cli.NewContext(nil, set, nil) tmpls := map[string]*yaml.Template{ @@ -607,28 +676,366 @@ func TestNative_ExpandStepsStarlark(t *testing.T) { t.Errorf("Creating new compiler returned err: %v", err) } - steps, secrets, services, environment, err := compiler.ExpandSteps(&yaml.Build{Steps: steps, Secrets: yaml.SecretSlice{}, Services: yaml.ServiceSlice{}, Environment: raw.StringSliceMap{}}, tmpls) + build, err := compiler.ExpandSteps(&yaml.Build{Steps: steps, Secrets: yaml.SecretSlice{}, Services: yaml.ServiceSlice{}, Environment: raw.StringSliceMap{}}, tmpls, new(pipeline.RuleData), compiler.TemplateDepth) if err != nil { t.Errorf("ExpandSteps returned err: %v", err) } - if diff := cmp.Diff(steps, wantSteps); diff != "" { + if diff := cmp.Diff(build.Steps, wantSteps); diff != "" { t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(secrets, wantSecrets); diff != "" { + if diff := cmp.Diff(build.Secrets, wantSecrets); diff != "" { t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(services, wantServices); diff != "" { + if diff := cmp.Diff(build.Services, wantServices); diff != "" { t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(environment, wantEnvironment); diff != "" { + if diff := cmp.Diff(build.Environment, wantEnvironment); diff != "" { t.Errorf("ExpandSteps() mismatch (-want +got):\n%s", diff) } } +func TestNative_ExpandSteps_TemplateCallTemplate(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + set := flag.NewFlagSet("test", 0) + set.Bool("github-driver", true, "doc") + set.String("github-url", s.URL, "doc") + set.String("github-token", "", "doc") + set.Int("max-template-depth", 5, "doc") + c := cli.NewContext(nil, set, nil) + + testBuild := new(library.Build) + + testBuild.SetID(1) + testBuild.SetCommit("123abc456def") + + testRepo := new(library.Repo) + + testRepo.SetID(1) + testRepo.SetOrg("foo") + testRepo.SetName("bar") + + tests := []struct { + name string + tmpls map[string]*yaml.Template + }{ + { + name: "Test 1", + tmpls: map[string]*yaml.Template{ + "chain": { + Name: "chain", + Source: "github.example.com/faz/baz/template_calls_template.yml", + Type: "github", + }, + }, + }, + } + + steps := yaml.StepSlice{ + &yaml.Step{ + Name: "sample", + Template: yaml.StepTemplate{ + Name: "chain", + }, + }, + } + + globalEnvironment := raw.StringSliceMap{ + "foo": "test1", + "bar": "test2", + } + + wantSteps := yaml.StepSlice{ + &yaml.Step{ + Commands: []string{"./gradlew downloadDependencies"}, + Environment: raw.StringSliceMap{ + "GRADLE_OPTS": "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=1 -Dorg.gradle.parallel=false", + "GRADLE_USER_HOME": ".gradle", + }, + Image: "openjdk:latest", + Name: "sample_call template_install", + Pull: "always", + }, + &yaml.Step{ + Commands: []string{"./gradlew check"}, + Environment: raw.StringSliceMap{ + "GRADLE_OPTS": "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=1 -Dorg.gradle.parallel=false", + "GRADLE_USER_HOME": ".gradle", + }, + Image: "openjdk:latest", + Name: "sample_call template_test", + Pull: "always", + }, + &yaml.Step{ + Commands: []string{"./gradlew build"}, + Environment: raw.StringSliceMap{ + "GRADLE_OPTS": "-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=1 -Dorg.gradle.parallel=false", + "GRADLE_USER_HOME": ".gradle", + }, + Image: "openjdk:latest", + Name: "sample_call template_build", + Pull: "always", + }, + } + + wantSecrets := yaml.SecretSlice{ + &yaml.Secret{ + Name: "docker_username", + Key: "org/repo/foo/bar", + Engine: "native", + Type: "repo", + Origin: yaml.Origin{}, + }, + &yaml.Secret{ + Name: "foo_password", + Key: "org/repo/foo/password", + Engine: "vault", + Type: "repo", + Origin: yaml.Origin{}, + }, + } + + wantServices := yaml.ServiceSlice{ + &yaml.Service{ + Image: "postgres:12", + Name: "postgres", + Pull: "not_present", + }, + } + + wantEnvironment := raw.StringSliceMap{ + "foo": "test1", + "bar": "test2", + "star": "test3", + } + + // run test + compiler, err := New(c) + if err != nil { + t.Errorf("Creating new compiler returned err: %v", err) + } + + compiler.WithBuild(testBuild).WithRepo(testRepo) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + build, err := compiler.ExpandSteps(&yaml.Build{Steps: steps, Services: yaml.ServiceSlice{}, Environment: globalEnvironment}, test.tmpls, new(pipeline.RuleData), compiler.TemplateDepth) + if err != nil { + t.Errorf("ExpandSteps_Type%s returned err: %v", test.name, err) + } + + if diff := cmp.Diff(build.Steps, wantSteps); diff != "" { + t.Errorf("ExpandSteps()_Type%s mismatch (-want +got):\n%s", test.name, diff) + } + + if diff := cmp.Diff(build.Secrets, wantSecrets); diff != "" { + t.Errorf("ExpandSteps()_Type%s mismatch (-want +got):\n%s", test.name, diff) + } + + if diff := cmp.Diff(build.Services, wantServices); diff != "" { + t.Errorf("ExpandSteps()_Type%s mismatch (-want +got):\n%s", test.name, diff) + } + + if diff := cmp.Diff(build.Environment, wantEnvironment); diff != "" { + t.Errorf("ExpandSteps()_Type%s mismatch (-want +got):\n%s", test.name, diff) + } + }) + } +} + +func TestNative_ExpandSteps_TemplateCallTemplate_CircularFail(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + set := flag.NewFlagSet("test", 0) + set.Bool("github-driver", true, "doc") + set.String("github-url", s.URL, "doc") + set.String("github-token", "", "doc") + set.Int("max-template-depth", 5, "doc") + c := cli.NewContext(nil, set, nil) + + testBuild := new(library.Build) + + testBuild.SetID(1) + testBuild.SetCommit("123abc456def") + + testRepo := new(library.Repo) + + testRepo.SetID(1) + testRepo.SetOrg("foo") + testRepo.SetName("bar") + + tests := []struct { + name string + tmpls map[string]*yaml.Template + }{ + { + name: "Test 1", + tmpls: map[string]*yaml.Template{ + "circle": { + Name: "circle", + Source: "github.example.com/bad/design/template_calls_itself.yml", + Type: "github", + }, + }, + }, + } + + steps := yaml.StepSlice{ + &yaml.Step{ + Name: "sample", + Template: yaml.StepTemplate{ + Name: "circle", + }, + }, + } + + globalEnvironment := raw.StringSliceMap{ + "foo": "test1", + "bar": "test2", + } + + // run test + compiler, err := New(c) + if err != nil { + t.Errorf("Creating new compiler returned err: %v", err) + } + + compiler.WithBuild(testBuild).WithRepo(testRepo) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := compiler.ExpandSteps(&yaml.Build{Steps: steps, Services: yaml.ServiceSlice{}, Environment: globalEnvironment}, test.tmpls, new(pipeline.RuleData), compiler.TemplateDepth) + if err == nil { + t.Errorf("ExpandSteps_Type%s should have returned an error", test.name) + } + }) + } +} + +func TestNative_ExpandSteps_CallTemplateWithRenderInline(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + body, err := convertFileToGithubResponse(c.Param("path")) + if err != nil { + t.Error(err) + } + c.JSON(http.StatusOK, body) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + set := flag.NewFlagSet("test", 0) + set.Bool("github-driver", true, "doc") + set.String("github-url", s.URL, "doc") + set.String("github-token", "", "doc") + set.Int("max-template-depth", 5, "doc") + c := cli.NewContext(nil, set, nil) + + testBuild := new(library.Build) + + testBuild.SetID(1) + testBuild.SetCommit("123abc456def") + + testRepo := new(library.Repo) + + testRepo.SetID(1) + testRepo.SetOrg("foo") + testRepo.SetName("bar") + + tests := []struct { + name string + tmpls map[string]*yaml.Template + }{ + { + name: "Test 1", + tmpls: map[string]*yaml.Template{ + "render_inline": { + Name: "render_inline", + Source: "github.example.com/github/octocat/nested.yml", + Type: "github", + }, + }, + }, + } + + steps := yaml.StepSlice{ + &yaml.Step{ + Name: "sample", + Template: yaml.StepTemplate{ + Name: "render_inline", + }, + }, + } + + globalEnvironment := raw.StringSliceMap{ + "foo": "test1", + "bar": "test2", + } + + // run test + compiler, err := New(c) + if err != nil { + t.Errorf("Creating new compiler returned err: %v", err) + } + + compiler.WithBuild(testBuild).WithRepo(testRepo) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := compiler.ExpandSteps(&yaml.Build{Steps: steps, Services: yaml.ServiceSlice{}, Environment: globalEnvironment}, test.tmpls, new(pipeline.RuleData), compiler.TemplateDepth) + if err == nil { + t.Errorf("ExpandSteps_Type%s should have returned an error", test.name) + } + }) + } +} + func TestNative_mapFromTemplates(t *testing.T) { // setup types str := "foo" diff --git a/compiler/native/native.go b/compiler/native/native.go index 5a231dd68..c394af933 100644 --- a/compiler/native/native.go +++ b/compiler/native/native.go @@ -31,21 +31,26 @@ type client struct { PrivateGithub registry.Service UsePrivateGithub bool ModificationService ModificationConfig - - build *library.Build - comment string - files []string - local bool - metadata *types.Metadata - repo *library.Repo - user *library.User + CloneImage string + TemplateDepth int + + build *library.Build + comment string + commit string + files []string + local bool + localTemplates []string + metadata *types.Metadata + repo *library.Repo + user *library.User } // New returns a Pipeline implementation that integrates with the supported registries. // -// nolint: revive // ignore returning unexported client +//nolint:revive // ignore returning unexported client func New(ctx *cli.Context) (*client, error) { logrus.Debug("Creating registry clients from CLI configuration") + c := new(client) if ctx.String("modification-addr") != "" { @@ -65,6 +70,11 @@ func New(ctx *cli.Context) (*client, error) { c.Github = github + // set the clone image to use for the injected clone step + c.CloneImage = ctx.String("clone-image") + + c.TemplateDepth = ctx.Int("max-template-depth") + if ctx.Bool("github-driver") { logrus.Tracef("setting up Private GitHub Client for %s", ctx.String("github-url")) // setup private github service @@ -103,6 +113,8 @@ func (c *client) Duplicate() compiler.Engine { cc.PrivateGithub = c.PrivateGithub cc.UsePrivateGithub = c.UsePrivateGithub cc.ModificationService = c.ModificationService + cc.CloneImage = c.CloneImage + cc.TemplateDepth = c.TemplateDepth return cc } @@ -125,6 +137,15 @@ func (c *client) WithComment(cmt string) compiler.Engine { return c } +// WithCommit sets the comment in the Engine. +func (c *client) WithCommit(cmt string) compiler.Engine { + if cmt != "" { + c.commit = cmt + } + + return c +} + // WithFiles sets the changeset files in the Engine. func (c *client) WithFiles(f []string) compiler.Engine { if f != nil { @@ -141,6 +162,13 @@ func (c *client) WithLocal(local bool) compiler.Engine { return c } +// WithLocalTemplates sets the compiler local templates in the Engine. +func (c *client) WithLocalTemplates(templates []string) compiler.Engine { + c.localTemplates = templates + + return c +} + // WithMetadata sets the compiler metadata type in the Engine. func (c *client) WithMetadata(m *types.Metadata) compiler.Engine { if m != nil { diff --git a/compiler/native/native_test.go b/compiler/native/native_test.go index 62c6fb7ed..de1f60631 100644 --- a/compiler/native/native_test.go +++ b/compiler/native/native_test.go @@ -202,6 +202,26 @@ func TestNative_WithLocal(t *testing.T) { } } +func TestNative_WithLocalTemplates(t *testing.T) { + // setup types + set := flag.NewFlagSet("test", 0) + c := cli.NewContext(nil, set, nil) + + localTemplates := []string{"example:tmpl.yml", "exmpl:template.yml"} + want, _ := New(c) + want.localTemplates = []string{"example:tmpl.yml", "exmpl:template.yml"} + + // run test + got, err := New(c) + if err != nil { + t.Errorf("Unable to create new compiler: %v", err) + } + + if !reflect.DeepEqual(got.WithLocalTemplates(localTemplates), want) { + t.Errorf("WithLocalTemplates is %v, want %v", got, want) + } +} + func TestNative_WithMetadata(t *testing.T) { // setup types set := flag.NewFlagSet("test", 0) diff --git a/compiler/native/parse.go b/compiler/native/parse.go index 71b449e59..b3ff020f4 100644 --- a/compiler/native/parse.go +++ b/compiler/native/parse.go @@ -7,7 +7,6 @@ package native import ( "fmt" "io" - "io/ioutil" "os" "github.com/go-vela/server/compiler/template/native" @@ -43,29 +42,40 @@ func (c *client) ParseRaw(v interface{}) (string, error) { } // Parse converts an object to a yaml configuration. -func (c *client) Parse(v interface{}) (*types.Build, error) { - var p *types.Build - - switch c.repo.GetPipelineType() { - case constants.PipelineTypeGo: +func (c *client) Parse(v interface{}, pipelineType string, template *types.Template) (*types.Build, []byte, error) { + var ( + p *types.Build + raw []byte + ) + + switch pipelineType { + case constants.PipelineTypeGo, "golang": // expand the base configuration parsedRaw, err := c.ParseRaw(v) if err != nil { - return nil, err + return nil, nil, err } - p, err = native.RenderBuild(parsedRaw, c.EnvironmentBuild()) + + // capture the raw pipeline configuration + raw = []byte(parsedRaw) + + p, err = native.RenderBuild(template.Name, parsedRaw, c.EnvironmentBuild(), template.Variables) if err != nil { - return nil, err + return nil, raw, err } case constants.PipelineTypeStarlark: // expand the base configuration parsedRaw, err := c.ParseRaw(v) if err != nil { - return nil, err + return nil, nil, err } - p, err = starlark.RenderBuild(parsedRaw, c.EnvironmentBuild()) + + // capture the raw pipeline configuration + raw = []byte(parsedRaw) + + p, err = starlark.RenderBuild(template.Name, parsedRaw, c.EnvironmentBuild(), template.Variables) if err != nil { - return nil, err + return nil, raw, err } case constants.PipelineTypeYAML, "": switch v := v.(type) { @@ -86,31 +96,30 @@ func (c *client) Parse(v interface{}) (*types.Build, error) { // parse string as yaml configuration return ParseString(v) default: - return nil, fmt.Errorf("unable to parse yaml: unrecognized type %T", v) + return nil, nil, fmt.Errorf("unable to parse yaml: unrecognized type %T", v) } default: - // nolint:lll // detailed error message - return nil, fmt.Errorf("unable to parse config: unrecognized pipeline_type of %s", c.repo.GetPipelineType()) + return nil, nil, fmt.Errorf("unable to parse config: unrecognized pipeline_type of %s", c.repo.GetPipelineType()) } - return p, nil + return p, raw, nil } // ParseBytes converts a byte slice to a yaml configuration. -func ParseBytes(b []byte) (*types.Build, error) { +func ParseBytes(data []byte) (*types.Build, []byte, error) { config := new(types.Build) // unmarshal the bytes into the yaml configuration - err := yaml.Unmarshal(b, config) + err := yaml.Unmarshal(data, config) if err != nil { - return nil, fmt.Errorf("unable to unmarshal yaml: %v", err) + return nil, data, fmt.Errorf("unable to unmarshal yaml: %w", err) } - return config, nil + return config, data, nil } // ParseFile converts an os.File into a yaml configuration. -func ParseFile(f *os.File) (*types.Build, error) { +func ParseFile(f *os.File) (*types.Build, []byte, error) { return ParseReader(f) } @@ -120,11 +129,11 @@ func ParseFileRaw(f *os.File) (string, error) { } // ParsePath converts a file path into a yaml configuration. -func ParsePath(p string) (*types.Build, error) { +func ParsePath(p string) (*types.Build, []byte, error) { // open the file for reading f, err := os.Open(p) if err != nil { - return nil, fmt.Errorf("unable to open yaml file %s: %v", p, err) + return nil, nil, fmt.Errorf("unable to open yaml file %s: %w", p, err) } defer f.Close() @@ -137,7 +146,7 @@ func ParsePathRaw(p string) (string, error) { // open the file for reading f, err := os.Open(p) if err != nil { - return "", fmt.Errorf("unable to open yaml file %s: %v", p, err) + return "", fmt.Errorf("unable to open yaml file %s: %w", p, err) } defer f.Close() @@ -146,28 +155,28 @@ func ParsePathRaw(p string) (string, error) { } // ParseReader converts an io.Reader into a yaml configuration. -func ParseReader(r io.Reader) (*types.Build, error) { +func ParseReader(r io.Reader) (*types.Build, []byte, error) { // read all the bytes from the reader - b, err := ioutil.ReadAll(r) + data, err := io.ReadAll(r) if err != nil { - return nil, fmt.Errorf("unable to read bytes for yaml: %v", err) + return nil, nil, fmt.Errorf("unable to read bytes for yaml: %w", err) } - return ParseBytes(b) + return ParseBytes(data) } // ParseReaderRaw converts an io.Reader into a yaml configuration. func ParseReaderRaw(r io.Reader) (string, error) { // read all the bytes from the reader - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err != nil { - return "", fmt.Errorf("unable to read bytes for yaml: %v", err) + return "", fmt.Errorf("unable to read bytes for yaml: %w", err) } return string(b), nil } // ParseString converts a string into a yaml configuration. -func ParseString(s string) (*types.Build, error) { +func ParseString(s string) (*types.Build, []byte, error) { return ParseBytes([]byte(s)) } diff --git a/compiler/native/parse_test.go b/compiler/native/parse_test.go index d1dcb3c39..0bb5a0195 100644 --- a/compiler/native/parse_test.go +++ b/compiler/native/parse_test.go @@ -8,7 +8,6 @@ import ( "bytes" "errors" "flag" - "io/ioutil" "os" "reflect" "testing" @@ -36,12 +35,12 @@ func TestNative_Parse_Metadata_Bytes(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/metadata.yml") + b, err := os.ReadFile("testdata/metadata.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := client.Parse(b) + got, _, err := client.Parse(b, "", new(yaml.Template)) if err != nil { t.Errorf("Parse returned err: %v", err) } @@ -71,7 +70,7 @@ func TestNative_Parse_Metadata_File(t *testing.T) { defer f.Close() - got, err := client.Parse(f) + got, _, err := client.Parse(f, "", new(yaml.Template)) if err != nil { t.Errorf("Parse returned err: %v", err) } @@ -86,7 +85,7 @@ func TestNative_Parse_Metadata_Invalid(t *testing.T) { client, _ := New(cli.NewContext(nil, flag.NewFlagSet("test", 0), nil)) // run test - got, err := client.Parse(nil) + got, _, err := client.Parse(nil, "", new(yaml.Template)) if err == nil { t.Error("Parse should have returned err") @@ -110,7 +109,7 @@ func TestNative_Parse_Metadata_Path(t *testing.T) { } // run test - got, err := client.Parse("testdata/metadata.yml") + got, _, err := client.Parse("testdata/metadata.yml", "", new(yaml.Template)) if err != nil { t.Errorf("Parse returned err: %v", err) } @@ -133,12 +132,12 @@ func TestNative_Parse_Metadata_Reader(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/metadata.yml") + b, err := os.ReadFile("testdata/metadata.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := client.Parse(bytes.NewReader(b)) + got, _, err := client.Parse(bytes.NewReader(b), "", new(yaml.Template)) if err != nil { t.Errorf("Parse returned err: %v", err) } @@ -161,12 +160,12 @@ func TestNative_Parse_Metadata_String(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/metadata.yml") + b, err := os.ReadFile("testdata/metadata.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := client.Parse(string(b)) + got, _, err := client.Parse(string(b), "", new(yaml.Template)) if err != nil { t.Errorf("Parse returned err: %v", err) } @@ -180,6 +179,9 @@ func TestNative_Parse_Parameters(t *testing.T) { // setup types client, _ := New(cli.NewContext(nil, flag.NewFlagSet("test", 0), nil)) want := &yaml.Build{ + Metadata: yaml.Metadata{ + Environment: []string{"steps", "services", "secrets"}, + }, Steps: yaml.StepSlice{ &yaml.Step{ Image: "plugins/docker:18.09", @@ -205,12 +207,12 @@ func TestNative_Parse_Parameters(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/parameters.yml") + b, err := os.ReadFile("testdata/parameters.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := client.Parse(b) + got, _, err := client.Parse(b, "", new(yaml.Template)) if err != nil { t.Errorf("Parse returned err: %v", err) } @@ -331,12 +333,12 @@ func TestNative_Parse_StagesPipeline(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/stages_pipeline.yml") + b, err := os.ReadFile("testdata/stages_pipeline.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := client.Parse(b) + got, _, err := client.Parse(b, "", new(yaml.Template)) if err != nil { t.Errorf("Parse returned err: %v", err) } @@ -428,12 +430,12 @@ func TestNative_Parse_StepsPipeline(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/steps_pipeline.yml") + b, err := os.ReadFile("testdata/steps_pipeline.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := client.Parse(b) + got, _, err := client.Parse(b, "", new(yaml.Template)) if err != nil { t.Errorf("Parse returned err: %v", err) } @@ -447,6 +449,9 @@ func TestNative_Parse_Secrets(t *testing.T) { // setup types client, _ := New(cli.NewContext(nil, flag.NewFlagSet("test", 0), nil)) want := &yaml.Build{ + Metadata: yaml.Metadata{ + Environment: []string{"steps", "services", "secrets"}, + }, Secrets: yaml.SecretSlice{ &yaml.Secret{ Name: "docker_username", @@ -488,12 +493,12 @@ func TestNative_Parse_Secrets(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/secrets.yml") + b, err := os.ReadFile("testdata/secrets.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := client.Parse(b) + got, _, err := client.Parse(b, "", new(yaml.Template)) if err != nil { t.Errorf("Parse returned err: %v", err) @@ -508,6 +513,9 @@ func TestNative_Parse_Stages(t *testing.T) { // setup types client, _ := New(cli.NewContext(nil, flag.NewFlagSet("test", 0), nil)) want := &yaml.Build{ + Metadata: yaml.Metadata{ + Environment: []string{"steps", "services", "secrets"}, + }, Stages: yaml.StageSlice{ &yaml.Stage{ Name: "install", @@ -561,12 +569,12 @@ func TestNative_Parse_Stages(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/stages.yml") + b, err := os.ReadFile("testdata/stages.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := client.Parse(b) + got, _, err := client.Parse(b, "", new(yaml.Template)) if err != nil { t.Errorf("Parse returned err: %v", err) @@ -581,6 +589,9 @@ func TestNative_Parse_Steps(t *testing.T) { // setup types client, _ := New(cli.NewContext(nil, flag.NewFlagSet("test", 0), nil)) want := &yaml.Build{ + Metadata: yaml.Metadata{ + Environment: []string{"steps", "services", "secrets"}, + }, Steps: yaml.StepSlice{ &yaml.Step{ Commands: []string{"./gradlew downloadDependencies"}, @@ -616,12 +627,12 @@ func TestNative_Parse_Steps(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/steps.yml") + b, err := os.ReadFile("testdata/steps.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := client.Parse(b) + got, _, err := client.Parse(b, "", new(yaml.Template)) if err != nil { t.Errorf("Parse returned err: %v", err) @@ -644,12 +655,12 @@ func TestNative_ParseBytes_Metadata(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/metadata.yml") + b, err := os.ReadFile("testdata/metadata.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := ParseBytes(b) + got, _, err := ParseBytes(b) if err != nil { t.Errorf("ParseBytes returned err: %v", err) @@ -662,12 +673,12 @@ func TestNative_ParseBytes_Metadata(t *testing.T) { func TestNative_ParseBytes_Invalid(t *testing.T) { // run test - b, err := ioutil.ReadFile("testdata/invalid.yml") + b, err := os.ReadFile("testdata/invalid.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := ParseBytes(b) + got, _, err := ParseBytes(b) if err == nil { t.Error("ParseBytes should have returned err") @@ -697,7 +708,7 @@ func TestNative_ParseFile_Metadata(t *testing.T) { defer f.Close() - got, err := ParseFile(f) + got, _, err := ParseFile(f) if err != nil { t.Errorf("ParseFile returned err: %v", err) @@ -717,7 +728,7 @@ func TestNative_ParseFile_Invalid(t *testing.T) { f.Close() - got, err := ParseFile(f) + got, _, err := ParseFile(f) if err == nil { t.Error("ParseFile should have returned err") @@ -740,7 +751,7 @@ func TestNative_ParsePath_Metadata(t *testing.T) { } // run test - got, err := ParsePath("testdata/metadata.yml") + got, _, err := ParsePath("testdata/metadata.yml") if err != nil { t.Errorf("ParsePath returned err: %v", err) @@ -753,7 +764,7 @@ func TestNative_ParsePath_Metadata(t *testing.T) { func TestNative_ParsePath_Invalid(t *testing.T) { // run test - got, err := ParsePath("testdata/foobar.yml") + got, _, err := ParsePath("testdata/foobar.yml") if err == nil { t.Error("ParsePath should have returned err") @@ -776,12 +787,12 @@ func TestNative_ParseReader_Metadata(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/metadata.yml") + b, err := os.ReadFile("testdata/metadata.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := ParseReader(bytes.NewReader(b)) + got, _, err := ParseReader(bytes.NewReader(b)) if err != nil { t.Errorf("ParseReader returned err: %v", err) @@ -794,7 +805,7 @@ func TestNative_ParseReader_Metadata(t *testing.T) { func TestNative_ParseReader_Invalid(t *testing.T) { // run test - got, err := ParseReader(FailReader{}) + got, _, err := ParseReader(FailReader{}) if err == nil { t.Error("ParseFile should have returned err") @@ -817,12 +828,12 @@ func TestNative_ParseString_Metadata(t *testing.T) { } // run test - b, err := ioutil.ReadFile("testdata/metadata.yml") + b, err := os.ReadFile("testdata/metadata.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } - got, err := ParseString(string(b)) + got, _, err := ParseString(string(b)) if err != nil { t.Errorf("ParseString returned err: %v", err) @@ -846,7 +857,7 @@ func Test_client_Parse(t *testing.T) { Metadata: yaml.Metadata{ Template: false, Clone: nil, - Environment: nil, + Environment: []string{"steps", "services", "secrets"}, }, Steps: yaml.StepSlice{ { @@ -859,10 +870,12 @@ func Test_client_Parse(t *testing.T) { }, }, } + type args struct { pipelineType string file string } + tests := []struct { name string args args @@ -873,12 +886,13 @@ func Test_client_Parse(t *testing.T) { {"starlark", args{pipelineType: constants.PipelineTypeStarlark, file: "testdata/pipeline_type.star"}, want, false}, {"go", args{pipelineType: constants.PipelineTypeGo, file: "testdata/pipeline_type_go.yml"}, want, false}, {"empty", args{pipelineType: "", file: "testdata/pipeline_type_default.yml"}, want, false}, - {"nil", args{pipelineType: "nil", file: "testdata/pipeline_type_default.yml"}, want, false}, + {"nil", args{pipelineType: "nil", file: "testdata/pipeline_type_default.yml"}, nil, true}, {"invalid", args{pipelineType: "foo", file: "testdata/pipeline_type_default.yml"}, nil, true}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - content, err := ioutil.ReadFile(tt.args.file) + content, err := os.ReadFile(tt.args.file) if err != nil { t.Errorf("Reading file returned err: %v", err) } @@ -892,7 +906,7 @@ func Test_client_Parse(t *testing.T) { } } - got, err := c.Parse(content) + got, _, err := c.Parse(content, tt.args.pipelineType, new(yaml.Template)) if (err != nil) != tt.wantErr { t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) return @@ -905,13 +919,15 @@ func Test_client_Parse(t *testing.T) { } func Test_client_ParseRaw(t *testing.T) { - expected, err := ioutil.ReadFile("testdata/metadata.yml") + expected, err := os.ReadFile("testdata/metadata.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } + type args struct { kind string } + tests := []struct { name string args args @@ -925,13 +941,14 @@ func Test_client_ParseRaw(t *testing.T) { {"path", args{kind: "path"}, string(expected), false}, {"unexpected", args{kind: "foo"}, "", true}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var content interface{} var err error switch tt.args.kind { case "byte": - content, err = ioutil.ReadFile("testdata/metadata.yml") + content, err = os.ReadFile("testdata/metadata.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } @@ -941,7 +958,7 @@ func Test_client_ParseRaw(t *testing.T) { t.Errorf("Reading file returned err: %v", err) } case "ioreader": - b, err := ioutil.ReadFile("testdata/metadata.yml") + b, err := os.ReadFile("testdata/metadata.yml") if err != nil { t.Errorf("ParseReader returned err: %v", err) } diff --git a/compiler/native/script.go b/compiler/native/script.go index 83ff32f84..8256ee5a1 100644 --- a/compiler/native/script.go +++ b/compiler/native/script.go @@ -39,7 +39,7 @@ func (c *client) ScriptSteps(s yaml.StepSlice) (yaml.StepSlice, error) { } // set the default home - // nolint: goconst // ignore making this a constant for now + //nolint:goconst // ignore making this a constant for now home := "/root" // override the home value if user is defined // TODO: @@ -60,7 +60,7 @@ func (c *client) ScriptSteps(s yaml.StepSlice) (yaml.StepSlice, error) { // set the environment variables for the step step.Environment["VELA_BUILD_SCRIPT"] = script step.Environment["HOME"] = home - // nolint: goconst // ignore making this a constant for now + //nolint:goconst // ignore making this a constant for now step.Environment["SHELL"] = "/bin/sh" } diff --git a/compiler/native/script_test.go b/compiler/native/script_test.go index a7f9517a2..bde8adcda 100644 --- a/compiler/native/script_test.go +++ b/compiler/native/script_test.go @@ -120,6 +120,7 @@ func TestNative_ScriptSteps(t *testing.T) { type args struct { s yaml.StepSlice } + tests := []struct { name string args args @@ -234,6 +235,7 @@ func TestNative_ScriptSteps(t *testing.T) { }, }, false}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { compiler, err := New(c) diff --git a/compiler/native/substitute.go b/compiler/native/substitute.go index c0a4be6de..fcfd2b263 100644 --- a/compiler/native/substitute.go +++ b/compiler/native/substitute.go @@ -16,7 +16,7 @@ import ( ) // SubstituteStages replaces every declared environment -// variable with it's corresponding value for each step +// variable with its corresponding value for each step // in every stage in a yaml configuration. func (c *client) SubstituteStages(s types.StageSlice) (types.StageSlice, error) { // iterate through all stages @@ -34,7 +34,7 @@ func (c *client) SubstituteStages(s types.StageSlice) (types.StageSlice, error) } // SubstituteSteps replaces every declared environment -// variable with it's corresponding value for each step +// variable with its corresponding value for each step // in a yaml configuration. func (c *client) SubstituteSteps(s types.StepSlice) (types.StepSlice, error) { // iterate through all steps @@ -42,7 +42,7 @@ func (c *client) SubstituteSteps(s types.StepSlice) (types.StepSlice, error) { // marshal step configuration body, err := yaml.Marshal(step) if err != nil { - return nil, fmt.Errorf("unable to marshal configuration: %v", err) + return nil, fmt.Errorf("unable to marshal configuration: %w", err) } // create substitute function @@ -67,13 +67,13 @@ func (c *client) SubstituteSteps(s types.StepSlice) (types.StepSlice, error) { // substitute the environment variables subStep, err := envsubst.Eval(string(body), subFunc) if err != nil { - return nil, fmt.Errorf("unable to substitute environment variables: %v", err) + return nil, fmt.Errorf("unable to substitute environment variables: %w", err) } // unmarshal step configuration err = yaml.Unmarshal([]byte(subStep), step) if err != nil { - return nil, fmt.Errorf("unable to unmarshal configuration: %v", err) + return nil, fmt.Errorf("unable to unmarshal configuration: %w", err) } } diff --git a/compiler/native/substitute_test.go b/compiler/native/substitute_test.go index feff0cf4f..da27378ac 100644 --- a/compiler/native/substitute_test.go +++ b/compiler/native/substitute_test.go @@ -6,62 +6,175 @@ package native import ( "flag" - "reflect" "testing" - "github.com/go-vela/types/yaml" - "github.com/urfave/cli/v2" + + "github.com/go-vela/types/yaml" + "github.com/google/go-cmp/cmp" ) -func TestNative_SubstituteStages(t *testing.T) { +func Test_client_SubstituteStages(t *testing.T) { + type args struct { + stages yaml.StageSlice + } + // setup types set := flag.NewFlagSet("test", 0) c := cli.NewContext(nil, set, nil) - s := yaml.StageSlice{ + tests := []struct { + name string + args args + want yaml.StageSlice + wantErr bool + }{ { - Name: "simple", - Steps: yaml.StepSlice{ - { - Commands: []string{"echo ${FOO}", "echo $${BAR}"}, - Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, - Image: "alpine:latest", - Name: "simple", - Pull: "always", + name: "normal", + args: args{ + stages: yaml.StageSlice{ + { + Name: "simple", + Steps: yaml.StepSlice{ + { + Commands: []string{"echo ${FOO}", "echo $${BAR}"}, + Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, + Image: "alpine:latest", + Name: "simple", + Pull: "always", + }, + }, + }, + { + Name: "advanced", + Steps: yaml.StepSlice{ + { + Commands: []string{"echo ${COMPLEX}"}, + Environment: map[string]string{"COMPLEX": "{\"hello\":\n \"world\"}"}, + Image: "alpine:latest", + Name: "advanced", + Pull: "always", + }, + }, + }, + { + Name: "not_found", + Steps: yaml.StepSlice{ + { + Commands: []string{"echo $NOT_FOUND", "echo ${NOT_FOUND}", "echo $${NOT_FOUND}"}, + Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, + Image: "alpine:latest", + Name: "not_found", + Pull: "always", + }, + }, + }, }, }, - }, - { - Name: "advanced", - Steps: yaml.StepSlice{ + want: yaml.StageSlice{ { - Commands: []string{"echo ${COMPLEX}"}, - Environment: map[string]string{"COMPLEX": "{\"hello\":\n \"world\"}"}, - Image: "alpine:latest", - Name: "advanced", - Pull: "always", + Name: "simple", + Steps: yaml.StepSlice{ + { + Commands: []string{"echo baz", "echo ${BAR}"}, + Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, + Image: "alpine:latest", + Name: "simple", + Pull: "always", + }, + }, }, - }, - }, - { - Name: "not_found", - Steps: yaml.StepSlice{ { - Commands: []string{"echo $NOT_FOUND", "echo ${NOT_FOUND}", "echo $${NOT_FOUND}"}, - Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, - Image: "alpine:latest", - Name: "not_found", - Pull: "always", + Name: "advanced", + Steps: yaml.StepSlice{ + { + Commands: []string{"echo \"{\\\"hello\\\":\\n \\\"world\\\"}\""}, + Environment: map[string]string{"COMPLEX": "{\"hello\":\n \"world\"}"}, + Image: "alpine:latest", + Name: "advanced", + Pull: "always", + }, + }, + }, + { + Name: "not_found", + Steps: yaml.StepSlice{ + { + Commands: []string{"echo $NOT_FOUND", "echo ${NOT_FOUND}", "echo ${NOT_FOUND}"}, + Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, + Image: "alpine:latest", + Name: "not_found", + Pull: "always", + }, + }, }, }, + wantErr: false, }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compiler, err := New(c) + if err != nil { + t.Errorf("Creating compiler returned err: %v", err) + } + + got, err := compiler.SubstituteStages(tt.args.stages) + if (err != nil) != tt.wantErr { + t.Errorf("SubstituteStages() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("SubstituteStages() mismatch (-want +got):\n%s", diff) + } + }) + } +} - want := yaml.StageSlice{ +func Test_client_SubstituteSteps(t *testing.T) { + type args struct { + steps yaml.StepSlice + } + + // setup types + set := flag.NewFlagSet("test", 0) + c := cli.NewContext(nil, set, nil) + + tests := []struct { + name string + args args + want yaml.StepSlice + wantErr bool + }{ { - Name: "simple", - Steps: yaml.StepSlice{ + name: "steps", + args: args{ + steps: yaml.StepSlice{ + { + Commands: []string{"echo ${FOO}", "echo $${BAR}"}, + Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, + Image: "alpine:latest", + Name: "simple", + Pull: "always", + }, + { + Commands: []string{"echo ${COMPLEX}"}, + Environment: map[string]string{"COMPLEX": "{\"hello\":\n \"world\"}"}, + Image: "alpine:latest", + Name: "advanced", + Pull: "always", + }, + { + Commands: []string{"echo $NOT_FOUND", "echo ${NOT_FOUND}", "echo $${NOT_FOUND}"}, + Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, + Image: "alpine:latest", + Name: "not_found", + Pull: "always", + }, + }, + }, + want: yaml.StepSlice{ { Commands: []string{"echo baz", "echo ${BAR}"}, Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, @@ -69,11 +182,6 @@ func TestNative_SubstituteStages(t *testing.T) { Name: "simple", Pull: "always", }, - }, - }, - { - Name: "advanced", - Steps: yaml.StepSlice{ { Commands: []string{"echo \"{\\\"hello\\\":\\n \\\"world\\\"}\""}, Environment: map[string]string{"COMPLEX": "{\"hello\":\n \"world\"}"}, @@ -81,11 +189,6 @@ func TestNative_SubstituteStages(t *testing.T) { Name: "advanced", Pull: "always", }, - }, - }, - { - Name: "not_found", - Steps: yaml.StepSlice{ { Commands: []string{"echo $NOT_FOUND", "echo ${NOT_FOUND}", "echo ${NOT_FOUND}"}, Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, @@ -94,91 +197,61 @@ func TestNative_SubstituteStages(t *testing.T) { Pull: "always", }, }, + wantErr: false, }, - } - - // run test - compiler, err := New(c) - if err != nil { - t.Errorf("Creating compiler returned err: %v", err) - } - - got, err := compiler.SubstituteStages(s) - - if err != nil { - t.Errorf("SubstituteStages returned err: %v", err) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("SubstituteStages is %v, want %v", got, want) - } -} - -func TestNative_SubstituteSteps(t *testing.T) { - // setup types - set := flag.NewFlagSet("test", 0) - c := cli.NewContext(nil, set, nil) - - p := yaml.StepSlice{ { - Commands: []string{"echo ${FOO}", "echo $${BAR}"}, - Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, - Image: "alpine:latest", - Name: "simple", - Pull: "always", - }, - { - Commands: []string{"echo ${COMPLEX}"}, - Environment: map[string]string{"COMPLEX": "{\"hello\":\n \"world\"}"}, - Image: "alpine:latest", - Name: "advanced", - Pull: "always", - }, - { - Commands: []string{"echo $NOT_FOUND", "echo ${NOT_FOUND}", "echo $${NOT_FOUND}"}, - Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, - Image: "alpine:latest", - Name: "not_found", - Pull: "always", - }, - } - - want := yaml.StepSlice{ - { - Commands: []string{"echo baz", "echo ${BAR}"}, - Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, - Image: "alpine:latest", - Name: "simple", - Pull: "always", - }, - { - Commands: []string{"echo \"{\\\"hello\\\":\\n \\\"world\\\"}\""}, - Environment: map[string]string{"COMPLEX": "{\"hello\":\n \"world\"}"}, - Image: "alpine:latest", - Name: "advanced", - Pull: "always", - }, - { - Commands: []string{"echo $NOT_FOUND", "echo ${NOT_FOUND}", "echo ${NOT_FOUND}"}, - Environment: map[string]string{"FOO": "baz", "BAR": "baz"}, - Image: "alpine:latest", - Name: "not_found", - Pull: "always", + name: "template", + args: args{ + steps: yaml.StepSlice{ + { + Name: "sample", + Template: yaml.StepTemplate{ + Name: "go", + Variables: map[string]interface{}{ + "build_author": "${BUILD_AUTHOR}", + "unknown": "${DEPLOYMENT_PARAMETER_API_IMAGE}", + }, + }, + Environment: map[string]string{ + "BUILD_AUTHOR": "testauthor", + }, + }, + }, + }, + want: yaml.StepSlice{ + { + Name: "sample", + Template: yaml.StepTemplate{ + Name: "go", + Variables: map[string]interface{}{ + "build_author": "testauthor", + "unknown": "${DEPLOYMENT_PARAMETER_API_IMAGE}", + }, + }, + Environment: map[string]string{ + "BUILD_AUTHOR": "testauthor", + }, + }, + }, + wantErr: false, }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + compiler, err := New(c) + if err != nil { + t.Errorf("Creating compiler returned err: %v", err) + } - // run test - compiler, err := New(c) - if err != nil { - t.Errorf("Creating compiler returned err: %v", err) - } - - got, err := compiler.SubstituteSteps(p) - if err != nil { - t.Errorf("SubstituteSteps returned err: %v", err) - } + got, err := compiler.SubstituteSteps(tt.args.steps) + if (err != nil) != tt.wantErr { + t.Errorf("SubstituteSteps() error = %v, wantErr %v", err, tt.wantErr) + return + } - if !reflect.DeepEqual(got, want) { - t.Errorf("SubstituteSteps is %v, want %v", got, want) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Errorf("SubstituteSteps() mismatch (-want +got):\n%s", diff) + } + }) } } diff --git a/compiler/native/testdata/circular.yml b/compiler/native/testdata/circular.yml new file mode 100644 index 000000000..bd8327df3 --- /dev/null +++ b/compiler/native/testdata/circular.yml @@ -0,0 +1,16 @@ +metadata: + render_inline: true + template: true + +templates: + - name: bad + source: github.example.com/github/octocat/inline_circular_template.yml + type: github + +stages: + test: + steps: + - name: test + image: alpine + commands: + - echo from inline \ No newline at end of file diff --git a/compiler/native/testdata/clone_replace.yml b/compiler/native/testdata/clone_replace.yml index 69ab9a369..e258910b6 100644 --- a/compiler/native/testdata/clone_replace.yml +++ b/compiler/native/testdata/clone_replace.yml @@ -5,7 +5,7 @@ metadata: steps: - name: clone - image: target/vela-git:v0.4.0 + image: target/vela-git:v0.5.1 parameters: depth: 5 pull: always diff --git a/compiler/native/testdata/environment.yml b/compiler/native/testdata/environment.yml new file mode 100644 index 000000000..fe6e584e0 --- /dev/null +++ b/compiler/native/testdata/environment.yml @@ -0,0 +1,3 @@ +--- +environment: + FOO: "Hello, foo!" \ No newline at end of file diff --git a/compiler/native/testdata/golang_inline_stages.yml b/compiler/native/testdata/golang_inline_stages.yml new file mode 100644 index 000000000..59b390d47 --- /dev/null +++ b/compiler/native/testdata/golang_inline_stages.yml @@ -0,0 +1,13 @@ +version: "1" + +{{$stageList := list "foo" "bar" "star" -}} + +stages: + {{range $stage := $stageList -}} + {{ $stage }}: + steps: + - name: {{ $stage }} + image: {{ default "alpine" $.image }} + commands: + - echo hello from {{ $stage }} + {{ end }} \ No newline at end of file diff --git a/compiler/native/testdata/golang_inline_steps.yml b/compiler/native/testdata/golang_inline_steps.yml new file mode 100644 index 000000000..1a69da41b --- /dev/null +++ b/compiler/native/testdata/golang_inline_steps.yml @@ -0,0 +1,11 @@ +version: "1" + +{{$stepList := list "foo" "bar" "star" -}} + +steps: + {{range $step := $stepList -}} + - name: {{ $step }} + image: alpine + commands: + - echo hello from {{ $step }} + {{ end }} \ No newline at end of file diff --git a/compiler/native/testdata/gradle.yml b/compiler/native/testdata/gradle.yml new file mode 100644 index 000000000..ed1e8e0d1 --- /dev/null +++ b/compiler/native/testdata/gradle.yml @@ -0,0 +1,70 @@ +metadata: + template: true + +steps: + - name: install + commands: + - ./gradlew downloadDependencies + environment: {{ .environment }} + image: {{ .image }} + {{ .pull_policy }} + + - name: test + commands: + - ./gradlew check + environment: {{ .environment }} + image: {{ .image }} + {{ .pull_policy }} + + - name: build + commands: + - ./gradlew build + environment: {{ .environment }} + image: {{ .image }} + {{ .pull_policy }} + +secrets: + - name: docker_username + key: org/repo/foo/bar + engine: native + type: repo + + - name: foo_password + key: org/repo/foo/password + engine: vault + type: repo + + - name: vault_token + + - origin: + name: private vault + image: target/secret-vault:latest + pull: always + secrets: [ vault_token ] + parameters: + addr: vault.example.com + auth_method: token + username: octocat + items: + - source: secret/docker + path: docker + + {{ if .secret }} + +- name: bar_password + key: org/repo/bar/password + engine: vault + type: repo + + {{ end }} + +services: + - name: postgres + image: postgres:12 + + {{ if .service }} + + - name: redis + key: redis:6 + + {{ end }} diff --git a/compiler/native/testdata/inline_circular_template.yml b/compiler/native/testdata/inline_circular_template.yml new file mode 100644 index 000000000..4b0469c58 --- /dev/null +++ b/compiler/native/testdata/inline_circular_template.yml @@ -0,0 +1,19 @@ +version: "1" + +metadata: + render_inline: true + +templates: + - name: nested + source: github.example.com/github/octocat/circular.yml + type: github + vars: + image: golang:latest + +stages: + test: + steps: + - name: test + image: alpine + commands: + - echo from inline \ No newline at end of file diff --git a/compiler/native/testdata/inline_nested_template.yml b/compiler/native/testdata/inline_nested_template.yml new file mode 100644 index 000000000..0faaeb0c8 --- /dev/null +++ b/compiler/native/testdata/inline_nested_template.yml @@ -0,0 +1,19 @@ +version: "1" + +metadata: + render_inline: true + +templates: + - name: nested + source: github.example.com/github/octocat/nested.yml + type: github + vars: + image: golang:latest + +stages: + test: + steps: + - name: test + image: alpine + commands: + - echo from inline \ No newline at end of file diff --git a/compiler/native/testdata/inline_with_environment.yml b/compiler/native/testdata/inline_with_environment.yml new file mode 100644 index 000000000..ffdb844fc --- /dev/null +++ b/compiler/native/testdata/inline_with_environment.yml @@ -0,0 +1,19 @@ +version: "1" + +environment: + HELLO: "Hello, Vela!" + +metadata: + render_inline: true + +templates: + - name: golang + source: github.example.com/github/octocat/environment.yml + format: golang + type: github + +steps: + - name: test + image: alpine + parameters: + first: "foo" \ No newline at end of file diff --git a/compiler/native/testdata/inline_with_golang.yml b/compiler/native/testdata/inline_with_golang.yml new file mode 100644 index 000000000..12767cfab --- /dev/null +++ b/compiler/native/testdata/inline_with_golang.yml @@ -0,0 +1,28 @@ +version: "1" + +metadata: + render_inline: true + +templates: + - name: golang + source: github.example.com/github/octocat/golang_inline_stages.yml + format: golang + type: github + vars: + image: golang:latest + - name: starlark + source: github.example.com/github/octocat/starlark_inline_stages.star + format: starlark + type: github + +{{$stageList := list "foo" "bar" "star" -}} + +stages: + {{range $stage := $stageList -}} + {{ $stage }}: + steps: + - name: {{ $stage }} + image: alpine + commands: + - echo from inline {{ $stage }} + {{ end }} \ No newline at end of file diff --git a/compiler/native/testdata/inline_with_secrets.yml b/compiler/native/testdata/inline_with_secrets.yml new file mode 100644 index 000000000..42ec50464 --- /dev/null +++ b/compiler/native/testdata/inline_with_secrets.yml @@ -0,0 +1,22 @@ +version: "1" + +secrets: + - name: foo_username + key: org/repo/foo/username + engine: native + type: repo + +metadata: + render_inline: true + +templates: + - name: golang + source: github.example.com/github/octocat/secrets.yml + format: golang + type: github + +steps: + - name: test + image: alpine + commands: + - echo from inline \ No newline at end of file diff --git a/compiler/native/testdata/inline_with_services.yml b/compiler/native/testdata/inline_with_services.yml new file mode 100644 index 000000000..5ecc71c2b --- /dev/null +++ b/compiler/native/testdata/inline_with_services.yml @@ -0,0 +1,20 @@ +version: "1" + +services: + - name: postgres + image: postgres:latest + +metadata: + render_inline: true + +templates: + - name: golang + source: github.example.com/github/octocat/services.yml + format: golang + type: github + +steps: + - name: test + image: alpine + commands: + - echo from inline \ No newline at end of file diff --git a/compiler/native/testdata/inline_with_stages.yml b/compiler/native/testdata/inline_with_stages.yml new file mode 100644 index 000000000..89d79b5e3 --- /dev/null +++ b/compiler/native/testdata/inline_with_stages.yml @@ -0,0 +1,24 @@ +version: "1" + +metadata: + render_inline: true + +templates: + - name: golang + source: github.example.com/github/octocat/golang_inline_stages.yml + format: golang + type: github + vars: + image: golang:latest + - name: starlark + source: github.example.com/github/octocat/starlark_inline_stages.star + format: starlark + type: github + +stages: + test: + steps: + - name: test + image: alpine + commands: + - echo from inline \ No newline at end of file diff --git a/compiler/native/testdata/inline_with_stages_and_steps.yml b/compiler/native/testdata/inline_with_stages_and_steps.yml new file mode 100644 index 000000000..8a70e12a7 --- /dev/null +++ b/compiler/native/testdata/inline_with_stages_and_steps.yml @@ -0,0 +1,22 @@ +version: "1" + +metadata: + render_inline: true + +templates: + - name: golang + source: github.example.com/github/octocat/golang_inline_stages.yml + format: golang + type: github + - name: starlark + source: github.example.com/github/octocat/starlark_inline_steps.star + format: starlark + type: github + +stages: + test: + steps: + - name: test + image: alpine + commands: + - echo from inline \ No newline at end of file diff --git a/compiler/native/testdata/inline_with_steps.yml b/compiler/native/testdata/inline_with_steps.yml new file mode 100644 index 000000000..28348cf8d --- /dev/null +++ b/compiler/native/testdata/inline_with_steps.yml @@ -0,0 +1,20 @@ +version: "1" + +metadata: + render_inline: true + +templates: + - name: golang + source: github.example.com/github/octocat/golang_inline_steps.yml + format: golang + type: github + - name: starlark + source: github.example.com/github/octocat/starlark_inline_steps.star + format: starlark + type: github + +steps: + - name: test + image: alpine + commands: + - echo from inline \ No newline at end of file diff --git a/compiler/native/testdata/long_template.yml b/compiler/native/testdata/long_template.yml new file mode 100644 index 000000000..c9f92ee44 --- /dev/null +++ b/compiler/native/testdata/long_template.yml @@ -0,0 +1,59 @@ +environment: + star: "test3" + bar: "test4" + +metadata: + template: true + +steps: + - name: install + commands: + - ./gradlew downloadDependencies + environment: {{ .environment }} + image: {{ .image }} + {{ .pull_policy }} + + - name: test + commands: + - ./gradlew check + environment: {{ .environment }} + image: {{ .image }} + {{ .pull_policy }} + + - name: build + commands: + - ./gradlew build + environment: {{ .environment }} + image: {{ .image }} + {{ .pull_policy }} + +secrets: + - name: docker_username + key: org/repo/foo/bar + engine: native + type: repo + + - name: foo_password + key: org/repo/foo/password + engine: vault + type: repo + + {{ if .secret }} + + - name: bar_password + key: org/repo/bar/password + engine: vault + type: repo + + {{ end }} + +services: + - name: postgres + image: postgres:12 + + {{ if .service }} + + - name: redis + key: redis:6 + + {{ end }} diff --git a/compiler/native/testdata/maven.yml b/compiler/native/testdata/maven.yml new file mode 100644 index 000000000..fd6ff73b1 --- /dev/null +++ b/compiler/native/testdata/maven.yml @@ -0,0 +1,70 @@ +metadata: + template: true + +steps: + - name: install + commands: + - mvn downloadDependencies + environment: {{ .environment }} + image: {{ .image }} + {{ .pull_policy }} + + - name: test + commands: + - mvn check + environment: {{ .environment }} + image: {{ .image }} + {{ .pull_policy }} + + - name: build + commands: + - mvn build + environment: {{ .environment }} + image: {{ .image }} + {{ .pull_policy }} + +secrets: + - name: docker_username + key: org/repo/foo/bar + engine: native + type: repo + + - name: foo_password + key: org/repo/foo/password + engine: vault + type: repo + + - name: vault_token + + - origin: + name: private vault + image: target/secret-vault:latest + pull: always + secrets: [ vault_token ] + parameters: + addr: vault.example.com + auth_method: token + username: octocat + items: + - source: secret/docker + path: docker + + {{ if .secret }} + +- name: bar_password + key: org/repo/bar/password + engine: vault + type: repo + + {{ end }} + +services: + - name: postgres + image: postgres:12 + + {{ if .service }} + + - name: redis + key: redis:6 + + {{ end }} diff --git a/compiler/native/testdata/nested.yml b/compiler/native/testdata/nested.yml new file mode 100644 index 000000000..2d44729d7 --- /dev/null +++ b/compiler/native/testdata/nested.yml @@ -0,0 +1,23 @@ +metadata: + render_inline: true + template: true + +templates: + - name: golang + source: github.example.com/github/octocat/golang_inline_stages.yml + format: golang + type: github + vars: + image: golang:latest + - name: starlark + source: github.example.com/github/octocat/starlark_inline_stages.star + format: starlark + type: github + +stages: + test: + steps: + - name: test + image: alpine + commands: + - echo from inline \ No newline at end of file diff --git a/compiler/native/testdata/services.yml b/compiler/native/testdata/services.yml new file mode 100644 index 000000000..c4d0288a4 --- /dev/null +++ b/compiler/native/testdata/services.yml @@ -0,0 +1,6 @@ +--- +services: + - name: cache + image: redis + - name: database + image: mongo \ No newline at end of file diff --git a/compiler/native/testdata/stage_inline_template.yml b/compiler/native/testdata/stage_inline_template.yml new file mode 100644 index 000000000..7d25f4328 --- /dev/null +++ b/compiler/native/testdata/stage_inline_template.yml @@ -0,0 +1,21 @@ +version: "1" + +metadata: + render_inline: true + +templates: + - name: golang + source: github.example.com/github/octocat/golang_inline_stages.yml + format: golang + type: github + vars: + image: golang:latest + +stages: + test: + steps: + - name: golang + template: + name: golang + vars: + image: golang:latest \ No newline at end of file diff --git a/compiler/native/testdata/stages_pipeline_template.yml b/compiler/native/testdata/stages_pipeline_template.yml index 0354ec737..25189ed73 100644 --- a/compiler/native/testdata/stages_pipeline_template.yml +++ b/compiler/native/testdata/stages_pipeline_template.yml @@ -6,7 +6,7 @@ metadata: templates: - name: gradle - source: github.example.com/github/octocat/template.yml + source: github.example.com/github/octocat/long_template.yml type: github stages: diff --git a/compiler/native/testdata/starlark_inline_stages.star b/compiler/native/testdata/starlark_inline_stages.star new file mode 100644 index 000000000..55ffee07f --- /dev/null +++ b/compiler/native/testdata/starlark_inline_stages.star @@ -0,0 +1,25 @@ +def main(ctx): + stageNames = ["foo", "bar"] + + stages = {} + + for name in stageNames: + stages[name] = stage(name) + + return { + 'version': '1', + 'stages': stages + } + +def stage(word): + return { + "steps": [ + { + "name": "build_%s" % word, + "image": "alpine", + 'commands': [ + "echo hello from %s" % word + ] + } + ] + } \ No newline at end of file diff --git a/compiler/native/testdata/starlark_inline_steps.star b/compiler/native/testdata/starlark_inline_steps.star new file mode 100644 index 000000000..51a2e6603 --- /dev/null +++ b/compiler/native/testdata/starlark_inline_steps.star @@ -0,0 +1,20 @@ +def main(ctx): + stepNames = ["foo", "bar"] + + steps = [] + + for name in stepNames: + steps.append( + { + "name": "build_%s" % name, + "image": "alpine", + 'commands': [ + "echo hello from %s" % name + ] + } + ) + + return { + 'version': '1', + 'steps': steps + } \ No newline at end of file diff --git a/compiler/native/testdata/step_inline_template.yml b/compiler/native/testdata/step_inline_template.yml new file mode 100644 index 000000000..6b29e2aa3 --- /dev/null +++ b/compiler/native/testdata/step_inline_template.yml @@ -0,0 +1,19 @@ +version: "1" + +metadata: + render_inline: true + +templates: + - name: golang + source: github.example.com/github/octocat/golang_inline_stages.yml + format: golang + type: github + vars: + image: golang:latest + +steps: + - name: golang + template: + name: golang + vars: + image: golang:latest \ No newline at end of file diff --git a/compiler/native/testdata/steps_pipeline_template.yml b/compiler/native/testdata/steps_pipeline_template.yml index 9e8402e8e..7d8e8db28 100644 --- a/compiler/native/testdata/steps_pipeline_template.yml +++ b/compiler/native/testdata/steps_pipeline_template.yml @@ -5,7 +5,7 @@ metadata: templates: - name: gradle - source: github.example.com/foo/bar/template.yml + source: github.example.com/foo/bar/long_template.yml type: github steps: diff --git a/compiler/native/testdata/template-gradle.json b/compiler/native/testdata/template-gradle.json deleted file mode 100644 index 8ad5f9091..000000000 --- a/compiler/native/testdata/template-gradle.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "file", - "encoding": "base64", - "size": 5362, - "name": "gradle.yml", - "path": "gradle.yml", - "content": "bWV0YWRhdGE6CiAgdGVtcGxhdGU6IHRydWUKCnN0ZXBzOgogIC0gbmFtZTogaW5zdGFsbAogICAgY29tbWFuZHM6CiAgICAgIC0gLi9ncmFkbGV3IGRvd25sb2FkRGVwZW5kZW5jaWVzCiAgICBlbnZpcm9ubWVudDoge3sgLmVudmlyb25tZW50IH19CiAgICBpbWFnZToge3sgLmltYWdlIH19CiAgICB7eyAucHVsbF9wb2xpY3kgfX0KCiAgLSBuYW1lOiB0ZXN0CiAgICBjb21tYW5kczoKICAgICAgLSAuL2dyYWRsZXcgY2hlY2sKICAgIGVudmlyb25tZW50OiB7eyAuZW52aXJvbm1lbnQgfX0KICAgIGltYWdlOiB7eyAuaW1hZ2UgfX0KICAgIHt7IC5wdWxsX3BvbGljeSB9fQoKICAtIG5hbWU6IGJ1aWxkCiAgICBjb21tYW5kczoKICAgICAgLSAuL2dyYWRsZXcgYnVpbGQKICAgIGVudmlyb25tZW50OiB7eyAuZW52aXJvbm1lbnQgfX0KICAgIGltYWdlOiB7eyAuaW1hZ2UgfX0KICAgIHt7IC5wdWxsX3BvbGljeSB9fQoKc2VjcmV0czoKICAtIG5hbWU6IGRvY2tlcl91c2VybmFtZQogICAga2V5OiBvcmcvcmVwby9mb28vYmFyCiAgICBlbmdpbmU6IG5hdGl2ZQogICAgdHlwZTogcmVwbwoKICAtIG5hbWU6IGZvb19wYXNzd29yZAogICAga2V5OiBvcmcvcmVwby9mb28vcGFzc3dvcmQKICAgIGVuZ2luZTogdmF1bHQKICAgIHR5cGU6IHJlcG8KCiAgLSBuYW1lOiB2YXVsdF90b2tlbgoKICAtIG9yaWdpbjoKICAgICAgbmFtZTogcHJpdmF0ZSB2YXVsdAogICAgICBpbWFnZTogdGFyZ2V0L3NlY3JldC12YXVsdDpsYXRlc3QKICAgICAgcHVsbDogYWx3YXlzCiAgICAgIHNlY3JldHM6IFsgdmF1bHRfdG9rZW4gXQogICAgICBwYXJhbWV0ZXJzOgogICAgICAgIGFkZHI6IHZhdWx0LmV4YW1wbGUuY29tCiAgICAgICAgYXV0aF9tZXRob2Q6IHRva2VuCiAgICAgICAgdXNlcm5hbWU6IG9jdG9jYXQKICAgICAgICBpdGVtczoKICAgICAgICAgIC0gc291cmNlOiBzZWNyZXQvZG9ja2VyCiAgICAgICAgICAgIHBhdGg6IGRvY2tlcgoKe3sgaWYgLnNlY3JldCB9fQoKICAtIG5hbWU6IGJhcl9wYXNzd29yZAogICAga2V5OiBvcmcvcmVwby9iYXIvcGFzc3dvcmQKICAgIGVuZ2luZTogdmF1bHQKICAgIHR5cGU6IHJlcG8KCnt7IGVuZCB9fQoKc2VydmljZXM6CiAgIC0gbmFtZTogcG9zdGdyZXMKICAgICBpbWFnZTogcG9zdGdyZXM6MTIKCiB7eyBpZiAuc2VydmljZSB9fQoKICAgLSBuYW1lOiByZWRpcwogICAgIGtleTogcmVkaXM6NgoKIHt7IGVuZCB9fQo=", - "sha": "3d21ec53a331a6f037a91c368710b99387d012c1", - "url": "https://api.github.com/repos/octokit/octokit.rb/contents/gradle.yml", - "git_url": "https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1", - "html_url": "https://github.com/octokit/octokit.rb/blob/master/gradle.yml", - "download_url": "https://raw.githubusercontent.com/octokit/octokit.rb/master/gradle.yml", - "_links": { - "git": "https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1", - "self": "https://api.github.com/repos/octokit/octokit.rb/contents/gradle.yml", - "html": "https://github.com/octokit/octokit.rb/blob/master/gradle.yml" - } -} diff --git a/compiler/native/testdata/template-maven.json b/compiler/native/testdata/template-maven.json deleted file mode 100644 index afc763414..000000000 --- a/compiler/native/testdata/template-maven.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "file", - "encoding": "base64", - "size": 5362, - "name": "maven.yml", - "path": "maven.yml", - "content": "bWV0YWRhdGE6CiAgdGVtcGxhdGU6IHRydWUKCnN0ZXBzOgogIC0gbmFtZTogaW5zdGFsbAogICAgY29tbWFuZHM6CiAgICAgIC0gbXZuIGRvd25sb2FkRGVwZW5kZW5jaWVzCiAgICBlbnZpcm9ubWVudDoge3sgLmVudmlyb25tZW50IH19CiAgICBpbWFnZToge3sgLmltYWdlIH19CiAgICB7eyAucHVsbF9wb2xpY3kgfX0KCiAgLSBuYW1lOiB0ZXN0CiAgICBjb21tYW5kczoKICAgICAgLSBtdm4gY2hlY2sKICAgIGVudmlyb25tZW50OiB7eyAuZW52aXJvbm1lbnQgfX0KICAgIGltYWdlOiB7eyAuaW1hZ2UgfX0KICAgIHt7IC5wdWxsX3BvbGljeSB9fQoKICAtIG5hbWU6IGJ1aWxkCiAgICBjb21tYW5kczoKICAgICAgLSBtdm4gYnVpbGQKICAgIGVudmlyb25tZW50OiB7eyAuZW52aXJvbm1lbnQgfX0KICAgIGltYWdlOiB7eyAuaW1hZ2UgfX0KICAgIHt7IC5wdWxsX3BvbGljeSB9fQoKc2VjcmV0czoKICAtIG5hbWU6IGRvY2tlcl91c2VybmFtZQogICAga2V5OiBvcmcvcmVwby9mb28vYmFyCiAgICBlbmdpbmU6IG5hdGl2ZQogICAgdHlwZTogcmVwbwoKICAtIG5hbWU6IGZvb19wYXNzd29yZAogICAga2V5OiBvcmcvcmVwby9mb28vcGFzc3dvcmQKICAgIGVuZ2luZTogdmF1bHQKICAgIHR5cGU6IHJlcG8KCiAgLSBuYW1lOiB2YXVsdF90b2tlbgoKICAtIG9yaWdpbjoKICAgICAgbmFtZTogcHJpdmF0ZSB2YXVsdAogICAgICBpbWFnZTogdGFyZ2V0L3NlY3JldC12YXVsdDpsYXRlc3QKICAgICAgcHVsbDogYWx3YXlzCiAgICAgIHNlY3JldHM6IFsgdmF1bHRfdG9rZW4gXQogICAgICBwYXJhbWV0ZXJzOgogICAgICAgIGFkZHI6IHZhdWx0LmV4YW1wbGUuY29tCiAgICAgICAgYXV0aF9tZXRob2Q6IHRva2VuCiAgICAgICAgdXNlcm5hbWU6IG9jdG9jYXQKICAgICAgICBpdGVtczoKICAgICAgICAgIC0gc291cmNlOiBzZWNyZXQvZG9ja2VyCiAgICAgICAgICAgIHBhdGg6IGRvY2tlcgoKe3sgaWYgLnNlY3JldCB9fQoKICAtIG5hbWU6IGJhcl9wYXNzd29yZAogICAga2V5OiBvcmcvcmVwby9iYXIvcGFzc3dvcmQKICAgIGVuZ2luZTogdmF1bHQKICAgIHR5cGU6IHJlcG8KCnt7IGVuZCB9fQoKc2VydmljZXM6CiAgIC0gbmFtZTogcG9zdGdyZXMKICAgICBpbWFnZTogcG9zdGdyZXM6MTIKCiB7eyBpZiAuc2VydmljZSB9fQoKICAgLSBuYW1lOiByZWRpcwogICAgIGtleTogcmVkaXM6NgoKIHt7IGVuZCB9fQo=", - "sha": "3d21ec53a331a6f037a91c368710b99387d012c1", - "url": "https://api.github.com/repos/octokit/octokit.rb/contents/maven.yml", - "git_url": "https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1", - "html_url": "https://github.com/octokit/octokit.rb/blob/master/maven.yml", - "download_url": "https://raw.githubusercontent.com/octokit/octokit.rb/master/maven.yml", - "_links": { - "git": "https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1", - "self": "https://api.github.com/repos/octokit/octokit.rb/contents/maven.yml", - "html": "https://github.com/octokit/octokit.rb/blob/master/maven.yml" - } -} diff --git a/compiler/native/testdata/template-starlark.json b/compiler/native/testdata/template-starlark.json deleted file mode 100644 index 5a29da9df..000000000 --- a/compiler/native/testdata/template-starlark.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "file", - "encoding": "base64", - "size": 5362, - "name": "template.star", - "path": "template.star", - "content": "ZGVmIG1haW4oY3R4KToKICByZXR1cm4gewogICAgJ3ZlcnNpb24nOiAnMScsCiAgICAnZW52aXJvbm1lbnQnOiB7CiAgICAgICdzdGFyJzogJ3Rlc3QzJywKICAgICAgJ2Jhcic6ICd0ZXN0NCcsCiAgICB9LAogICAgJ3N0ZXBzJzogWwogICAgICB7CiAgICAgICAgJ25hbWUnOiAnYnVpbGQnLAogICAgICAgICdpbWFnZSc6ICdnb2xhbmc6bGF0ZXN0JywKICAgICAgICAnY29tbWFuZHMnOiBbCiAgICAgICAgICAnZ28gYnVpbGQnLAogICAgICAgICAgJ2dvIHRlc3QnLAogICAgICAgIF0KICAgICAgfSwKICAgIF0sCn0K\n", - "sha": "3d21ec53a331a6f037a91c368710b99387d012c1", - "url": "https://api.github.com/repos/octokit/octokit.rb/contents/template.star", - "git_url": "https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1", - "html_url": "https://github.com/octokit/octokit.rb/blob/master/template.star", - "download_url": "https://raw.githubusercontent.com/octokit/octokit.rb/master/template.star", - "_links": { - "git": "https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1", - "self": "https://api.github.com/repos/octokit/octokit.rb/contents/template.star", - "html": "https://github.com/octokit/octokit.rb/blob/master/template.star" - } -} diff --git a/compiler/native/testdata/template.json b/compiler/native/testdata/template.json deleted file mode 100644 index f65b38008..000000000 --- a/compiler/native/testdata/template.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "type": "file", - "encoding": "base64", - "size": 5362, - "name": "template.yml", - "path": "template.yml", - "content": "ZW52aXJvbm1lbnQ6CiAgc3RhcjogInRlc3QzIgogIGJhcjogInRlc3Q0IgoKbWV0YWRhdGE6CiAgdGVtcGxhdGU6IHRydWUKCnN0ZXBzOgogIC0gbmFtZTogaW5zdGFsbAogICAgY29tbWFuZHM6CiAgICAgIC0gLi9ncmFkbGV3IGRvd25sb2FkRGVwZW5kZW5jaWVzCiAgICBlbnZpcm9ubWVudDoge3sgLmVudmlyb25tZW50IH19CiAgICBpbWFnZToge3sgLmltYWdlIH19CiAgICB7eyAucHVsbF9wb2xpY3kgfX0KCiAgLSBuYW1lOiB0ZXN0CiAgICBjb21tYW5kczoKICAgICAgLSAuL2dyYWRsZXcgY2hlY2sKICAgIGVudmlyb25tZW50OiB7eyAuZW52aXJvbm1lbnQgfX0KICAgIGltYWdlOiB7eyAuaW1hZ2UgfX0KICAgIHt7IC5wdWxsX3BvbGljeSB9fQoKICAtIG5hbWU6IGJ1aWxkCiAgICBjb21tYW5kczoKICAgICAgLSAuL2dyYWRsZXcgYnVpbGQKICAgIGVudmlyb25tZW50OiB7eyAuZW52aXJvbm1lbnQgfX0KICAgIGltYWdlOiB7eyAuaW1hZ2UgfX0KICAgIHt7IC5wdWxsX3BvbGljeSB9fQoKc2VjcmV0czoKICAtIG5hbWU6IGRvY2tlcl91c2VybmFtZQogICAga2V5OiBvcmcvcmVwby9mb28vYmFyCiAgICBlbmdpbmU6IG5hdGl2ZQogICAgdHlwZTogcmVwbwoKICAtIG5hbWU6IGZvb19wYXNzd29yZAogICAga2V5OiBvcmcvcmVwby9mb28vcGFzc3dvcmQKICAgIGVuZ2luZTogdmF1bHQKICAgIHR5cGU6IHJlcG8KCnt7IGlmIC5zZWNyZXQgfX0KCiAgLSBuYW1lOiBiYXJfcGFzc3dvcmQKICAgIGtleTogb3JnL3JlcG8vYmFyL3Bhc3N3b3JkCiAgICBlbmdpbmU6IHZhdWx0CiAgICB0eXBlOiByZXBvCgp7eyBlbmQgfX0KCnNlcnZpY2VzOgogICAtIG5hbWU6IHBvc3RncmVzCiAgICAgaW1hZ2U6IHBvc3RncmVzOjEyCgoge3sgaWYgLnNlcnZpY2UgfX0KCiAgIC0gbmFtZTogcmVkaXMKICAgICBrZXk6IHJlZGlzOjYKCiB7eyBlbmQgfX0K", - "sha": "3d21ec53a331a6f037a91c368710b99387d012c1", - "url": "https://api.github.com/repos/octokit/octokit.rb/contents/template.yml", - "git_url": "https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1", - "html_url": "https://github.com/octokit/octokit.rb/blob/master/template.yml", - "download_url": "https://raw.githubusercontent.com/octokit/octokit.rb/master/template.yml", - "_links": { - "git": "https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1", - "self": "https://api.github.com/repos/octokit/octokit.rb/contents/template.yml", - "html": "https://github.com/octokit/octokit.rb/blob/master/template.yml" - } -} diff --git a/compiler/native/testdata/template.star b/compiler/native/testdata/template.star index 079d70943..9c2815791 100644 --- a/compiler/native/testdata/template.star +++ b/compiler/native/testdata/template.star @@ -1,6 +1,10 @@ def main(ctx): return { 'version': '1', + 'environment': { + 'star': 'test3', + 'bar': 'test4', + }, 'steps': [ { 'name': 'build', @@ -11,4 +15,4 @@ def main(ctx): ] }, ], -} \ No newline at end of file +} diff --git a/compiler/native/testdata/template_calls_itself.yml b/compiler/native/testdata/template_calls_itself.yml new file mode 100644 index 000000000..9b467a675 --- /dev/null +++ b/compiler/native/testdata/template_calls_itself.yml @@ -0,0 +1,11 @@ +version: "1" + +templates: + - name: test + source: github.example.com/bad/design/template_calls_itself.yml + type: github + +steps: + - name: call template + template: + name: test diff --git a/compiler/native/testdata/template_calls_template.yml b/compiler/native/testdata/template_calls_template.yml new file mode 100644 index 000000000..b6197169d --- /dev/null +++ b/compiler/native/testdata/template_calls_template.yml @@ -0,0 +1,15 @@ +version: "1" + +templates: + - name: test + source: github.example.com/foo/bar/long_template.yml + type: github + +steps: + - name: call template + template: + name: test + vars: + image: openjdk:latest + environment: "{ GRADLE_USER_HOME: .gradle, GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.workers.max=1 -Dorg.gradle.parallel=false }" + pull_policy: "pull: true" diff --git a/compiler/native/testdata/template_name.yml b/compiler/native/testdata/template_name.yml new file mode 100644 index 000000000..652cdb2fd --- /dev/null +++ b/compiler/native/testdata/template_name.yml @@ -0,0 +1,16 @@ +--- +version: "1" + +metadata: + template: false + +templates: + - name: inline_templatename + source: github.example.com/github/octocat/template_name_template.yml + type: github + +steps: + - name: sample + template: + name: inline_templatename + diff --git a/compiler/native/testdata/template_name_inline.yml b/compiler/native/testdata/template_name_inline.yml new file mode 100644 index 000000000..f35801376 --- /dev/null +++ b/compiler/native/testdata/template_name_inline.yml @@ -0,0 +1,11 @@ +--- +version: "1" + +metadata: + template: false + render_inline: true + +templates: + - name: inline_templatename + source: github.example.com/github/octocat/template_name_template.yml + type: github diff --git a/compiler/native/testdata/template_name_template.yml b/compiler/native/testdata/template_name_template.yml new file mode 100644 index 000000000..3ff2c0262 --- /dev/null +++ b/compiler/native/testdata/template_name_template.yml @@ -0,0 +1,8 @@ +metadata: + template: true + +steps: + - name: hello + image: {{ vela "template_name" }} + commands: + - echo {{ vela "template_name" }} diff --git a/compiler/native/transform.go b/compiler/native/transform.go index 85be86d0b..4742ae700 100644 --- a/compiler/native/transform.go +++ b/compiler/native/transform.go @@ -33,7 +33,7 @@ const ( // default ID for secrets in a pipeline. // format: `secret____` // - // nolint: gosec // ignore gosec keying off of secret as no credentials are hardcoded + //nolint:gosec // ignore gosec keying off of secret as no credentials are hardcoded secretID = "secret_%s_%s_%d_%s" ) diff --git a/compiler/native/validate.go b/compiler/native/validate.go index 7d588a89f..dec7c7af9 100644 --- a/compiler/native/validate.go +++ b/compiler/native/validate.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Target Brands, Inc. All rights reserved. +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -7,45 +7,64 @@ package native import ( "fmt" + "github.com/hashicorp/go-multierror" + "github.com/go-vela/types/yaml" ) -// Validate verifies the the yaml configuration is valid. +// Validate verifies the yaml configuration is valid. func (c *client) Validate(p *yaml.Build) error { + var result error // check a version is provided if len(p.Version) == 0 { - return fmt.Errorf("no version provided") + result = multierror.Append(result, fmt.Errorf("no \"version:\" YAML property provided")) } // check that stages or steps are provided - if len(p.Stages) == 0 && len(p.Steps) == 0 { - return fmt.Errorf("no stages or steps provided") + if len(p.Stages) == 0 && len(p.Steps) == 0 && (!p.Metadata.RenderInline && len(p.Templates) == 0) { + result = multierror.Append(result, fmt.Errorf("no stages, steps or templates provided")) } // check that stages and steps aren't provided if len(p.Stages) > 0 && len(p.Steps) > 0 { - return fmt.Errorf("stages and steps provided") + result = multierror.Append(result, fmt.Errorf("stages and steps provided")) + } + + if p.Metadata.RenderInline { + for _, step := range p.Steps { + if step.Template.Name != "" { + result = multierror.Append(result, fmt.Errorf("step %s: cannot combine render_inline and a step that references a template", step.Name)) + } + } + + for _, stage := range p.Stages { + for _, step := range stage.Steps { + if step.Template.Name != "" { + result = multierror.Append(result, fmt.Errorf("step %s.%s: cannot combine render_inline and a step that references a template", stage.Name, step.Name)) + } + } + } } // validate the services block provided err := validateServices(p.Services) if err != nil { - return err + result = multierror.Append(result, err) } // validate the stages block provided err = validateStages(p.Stages) if err != nil { - return err + result = multierror.Append(result, err) } // validate the steps block provided err = validateSteps(p.Steps) if err != nil { - return err + result = multierror.Append(result, err) } - return nil + return result } // validateServices is a helper function that verifies the @@ -84,7 +103,6 @@ func validateStages(s yaml.StageSlice) error { return fmt.Errorf("no name provided for step for stage %s", stage.Name) } - // nolint: lll // ignore simplification here if len(step.Image) == 0 && len(step.Template.Name) == 0 { return fmt.Errorf("no image or template provided for step %s for stage %s", step.Name, stage.Name) } @@ -93,7 +111,6 @@ func validateStages(s yaml.StageSlice) error { continue } - // nolint: lll // ignore simplification here if len(step.Commands) == 0 && len(step.Environment) == 0 && len(step.Parameters) == 0 && len(step.Secrets) == 0 && len(step.Template.Name) == 0 && !step.Detach { @@ -121,7 +138,6 @@ func validateSteps(s yaml.StepSlice) error { continue } - // nolint: lll // ignore simplification here if len(step.Commands) == 0 && len(step.Environment) == 0 && len(step.Parameters) == 0 && len(step.Secrets) == 0 && len(step.Template.Name) == 0 && !step.Detach { diff --git a/compiler/native/validate_test.go b/compiler/native/validate_test.go index 6380a2df2..772494aa7 100644 --- a/compiler/native/validate_test.go +++ b/compiler/native/validate_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Target Brands, Inc. All rights reserved. +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. diff --git a/compiler/registry/doc.go b/compiler/registry/doc.go index 70cb52e2b..2064281e4 100644 --- a/compiler/registry/doc.go +++ b/compiler/registry/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/compiler/registry" +// import "github.com/go-vela/server/compiler/registry" package registry diff --git a/compiler/registry/github/doc.go b/compiler/registry/github/doc.go index 87e9427a1..1bab3812e 100644 --- a/compiler/registry/github/doc.go +++ b/compiler/registry/github/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/compiler/registry/github" +// import "github.com/go-vela/server/compiler/registry/github" package github diff --git a/compiler/registry/github/github.go b/compiler/registry/github/github.go index 22815b75a..7fb6f22b7 100644 --- a/compiler/registry/github/github.go +++ b/compiler/registry/github/github.go @@ -9,7 +9,7 @@ import ( "net/url" "strings" - "github.com/google/go-github/v42/github" + "github.com/google/go-github/v53/github" "golang.org/x/oauth2" ) @@ -27,7 +27,7 @@ type client struct { // New returns a Registry implementation that integrates // with GitHub or a GitHub Enterprise instance. // -// nolint: revive // ignore returning unexported client +//nolint:revive // ignore returning unexported client func New(address, token string) (*client, error) { // create the client object c := &client{ @@ -84,5 +84,6 @@ func (c *client) newClientToken(token string) *github.Client { // ensure the proper URL is set github.BaseURL, _ = url.Parse(c.API) + return github } diff --git a/compiler/registry/github/github_test.go b/compiler/registry/github/github_test.go index 11d40700f..530b8f644 100644 --- a/compiler/registry/github/github_test.go +++ b/compiler/registry/github/github_test.go @@ -12,7 +12,7 @@ import ( "reflect" "testing" - "github.com/google/go-github/v42/github" + "github.com/google/go-github/v53/github" "golang.org/x/oauth2" ) @@ -125,6 +125,7 @@ func TestGithub_NewURL(t *testing.T) { if got.URL != test.want.URL { t.Errorf("New URL is %v, want %v", got.URL, test.want.URL) } + if got.API != test.want.API { t.Errorf("New API is %v, want %v", got.API, test.want.API) } diff --git a/compiler/registry/github/parse.go b/compiler/registry/github/parse.go index 7e7a824c2..9c40cf1c8 100644 --- a/compiler/registry/github/parse.go +++ b/compiler/registry/github/parse.go @@ -34,13 +34,10 @@ func (c *client) Parse(path string) (*registry.Source, error) { // this will handle multiple cases for the path: // * // // * //// - // nolint: gomnd // ignore magic number parts := strings.SplitN(u.Path, "/", 3) // ensure org, repo and filename parts exist - // nolint: gomnd // ignore magic number if len(parts) < 3 { - // nolint: lll // ignore long line length due to error message return ®istry.Source{}, fmt.Errorf("invalid template source %s, must contain org/repo/path_to_template", path) } diff --git a/compiler/registry/github/template.go b/compiler/registry/github/template.go index a712ef4c5..956a7608d 100644 --- a/compiler/registry/github/template.go +++ b/compiler/registry/github/template.go @@ -13,7 +13,7 @@ import ( "github.com/go-vela/types/library" - "github.com/google/go-github/v42/github" + "github.com/google/go-github/v53/github" ) // Template captures the templated pipeline configuration from the GitHub repo. @@ -38,16 +38,18 @@ func (c *client) Template(u *library.User, s *registry.Source) ([]byte, error) { // send API call to capture the templated pipeline configuration // - // nolint: lll // ignore long line length due to variable names + data, _, resp, err := cli.Repositories.GetContents(context.Background(), s.Org, s.Repo, s.Name, opts) if err != nil { if resp != nil && resp.StatusCode != http.StatusNotFound { // return different error message depending on if a branch was provided if len(s.Ref) == 0 { - errString := "unexpected error fetching template %s/%s/%s: %v" + errString := "unexpected error fetching template %s/%s/%s: %w" return nil, fmt.Errorf(errString, s.Org, s.Repo, s.Name, err) } - errString := "unexpected error fetching template %s/%s/%s@%s: %v" + + errString := "unexpected error fetching template %s/%s/%s@%s: %w" + return nil, fmt.Errorf(errString, s.Org, s.Repo, s.Name, s.Ref, err) } @@ -55,6 +57,7 @@ func (c *client) Template(u *library.User, s *registry.Source) ([]byte, error) { if len(s.Ref) == 0 { return nil, fmt.Errorf("no Vela template found at %s/%s/%s", s.Org, s.Repo, s.Name) } + return nil, fmt.Errorf("no Vela template found at %s/%s/%s@%s", s.Org, s.Repo, s.Name, s.Ref) } @@ -72,5 +75,6 @@ func (c *client) Template(u *library.User, s *registry.Source) ([]byte, error) { if len(s.Ref) == 0 { return nil, fmt.Errorf("no Vela template found at %s/%s/%s", s.Org, s.Repo, s.Name) } + return nil, fmt.Errorf("no Vela template found at %s/%s/%s@%s", s.Org, s.Repo, s.Name, s.Ref) } diff --git a/compiler/registry/github/template_test.go b/compiler/registry/github/template_test.go index bffa1d47c..af59a6d61 100644 --- a/compiler/registry/github/template_test.go +++ b/compiler/registry/github/template_test.go @@ -5,9 +5,9 @@ package github import ( - "io/ioutil" "net/http" "net/http/httptest" + "os" "reflect" "testing" @@ -21,6 +21,7 @@ import ( func TestGithub_Template(t *testing.T) { // setup context gin.SetMode(gin.TestMode) + resp := httptest.NewRecorder() _, engine := gin.CreateTestContext(resp) @@ -30,7 +31,9 @@ func TestGithub_Template(t *testing.T) { c.Status(http.StatusOK) c.File("testdata/template.json") }) + s := httptest.NewServer(engine) + defer s.Close() // setup types @@ -46,7 +49,7 @@ func TestGithub_Template(t *testing.T) { Name: "template.yml", } - want, err := ioutil.ReadFile("testdata/template.yml") + want, err := os.ReadFile("testdata/template.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } @@ -75,6 +78,7 @@ func TestGithub_Template(t *testing.T) { func TestGithub_TemplateSourceRef(t *testing.T) { // setup context gin.SetMode(gin.TestMode) + resp := httptest.NewRecorder() _, engine := gin.CreateTestContext(resp) @@ -88,7 +92,9 @@ func TestGithub_TemplateSourceRef(t *testing.T) { c.Status(http.StatusOK) c.File("testdata/template.json") }) + s := httptest.NewServer(engine) + defer s.Close() // setup types @@ -105,7 +111,7 @@ func TestGithub_TemplateSourceRef(t *testing.T) { Ref: "main", } - want, err := ioutil.ReadFile("testdata/template.yml") + want, err := os.ReadFile("testdata/template.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } @@ -138,6 +144,7 @@ func TestGithub_TemplateSourceRef(t *testing.T) { func TestGithub_TemplateEmptySourceRef(t *testing.T) { // setup context gin.SetMode(gin.TestMode) + resp := httptest.NewRecorder() _, engine := gin.CreateTestContext(resp) @@ -151,7 +158,9 @@ func TestGithub_TemplateEmptySourceRef(t *testing.T) { c.Status(http.StatusOK) c.File("testdata/template.json") }) + s := httptest.NewServer(engine) + defer s.Close() // setup types @@ -167,7 +176,7 @@ func TestGithub_TemplateEmptySourceRef(t *testing.T) { Name: "template.yml", } - want, err := ioutil.ReadFile("testdata/template.yml") + want, err := os.ReadFile("testdata/template.yml") if err != nil { t.Errorf("Reading file returned err: %v", err) } @@ -200,6 +209,7 @@ func TestGithub_TemplateEmptySourceRef(t *testing.T) { func TestGithub_Template_BadRequest(t *testing.T) { // setup context gin.SetMode(gin.TestMode) + resp := httptest.NewRecorder() _, engine := gin.CreateTestContext(resp) @@ -207,7 +217,9 @@ func TestGithub_Template_BadRequest(t *testing.T) { engine.GET("/api/v3/repos/foo/bar/contents/:path", func(c *gin.Context) { c.Status(http.StatusBadRequest) }) + s := httptest.NewServer(engine) + defer s.Close() // setup types @@ -247,6 +259,7 @@ func TestGithub_Template_BadRequest(t *testing.T) { func TestGithub_Template_NotFound(t *testing.T) { // setup context gin.SetMode(gin.TestMode) + resp := httptest.NewRecorder() _, engine := gin.CreateTestContext(resp) @@ -254,7 +267,9 @@ func TestGithub_Template_NotFound(t *testing.T) { engine.GET("/api/v3/repos/foo/bar/contents/:path", func(c *gin.Context) { c.Status(http.StatusNotFound) }) + s := httptest.NewServer(engine) + defer s.Close() // setup types diff --git a/compiler/template/doc.go b/compiler/template/doc.go index 66c9d028b..f9e37fb29 100644 --- a/compiler/template/doc.go +++ b/compiler/template/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/template" +// import "github.com/go-vela/server/template" package template diff --git a/compiler/template/native/convert.go b/compiler/template/native/convert.go index ed83f9ff3..68385c6cb 100644 --- a/compiler/template/native/convert.go +++ b/compiler/template/native/convert.go @@ -1,3 +1,7 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + package native import ( @@ -13,9 +17,23 @@ import ( // within the template. func convertPlatformVars(slice raw.StringSliceMap, name string) raw.StringSliceMap { envs := make(map[string]string) + + // iterate through the list of key/value pairs provided for key, value := range slice { + // lowercase the key key = strings.ToLower(key) + + // check if the key has a 'deployment_parameter_*' prefix + if strings.HasPrefix(key, "deployment_parameter_") { + // add the key/value pair with the 'deployment_parameter_` prefix + // + // this is used to ensure we prevent conflicts with `vela_*` prefixed variables + envs[key] = value + } + + // check if the key has a 'vela_*' prefix if strings.HasPrefix(key, "vela_") { + // add the key/value pair without the 'vela_` prefix envs[strings.TrimPrefix(key, "vela_")] = value } } @@ -29,7 +47,7 @@ func convertPlatformVars(slice raw.StringSliceMap, name string) raw.StringSliceM // always return a string, even on marshal error (empty string). // // This code is under copyright (full attribution in NOTICE) and is from: -// nolint: lll // ignore long line length due to url + // https://github.com/helm/helm/blob/a499b4b179307c267bdf3ec49b880e3dbd2a5591/pkg/engine/funcs.go#L83 // // This is designed to be called from a template. @@ -39,6 +57,7 @@ func toYAML(v interface{}) string { // Swallow errors inside of a template. return "" } + return strings.TrimSuffix(string(data), "\n") } @@ -48,14 +67,21 @@ type funcHandler struct { // returnPlatformVar returns the value of the platform // variable if it exists within the environment map. -func (h funcHandler) returnPlatformVar(input string) string { - input = strings.ToLower(input) - input = strings.TrimPrefix(input, "vela_") - // check if key exists within map - if _, ok := h.envs[input]; ok { - // return value if exists - return h.envs[input] +func (h funcHandler) returnPlatformVar(key string) string { + // lowercase the key + key = strings.ToLower(key) + + // iterate through the list of possible prefixes to look for + for _, prefix := range []string{"deployment_parameter_", "vela_"} { + // trim the prefix from the input key + trimmed := strings.TrimPrefix(key, prefix) + // check if the key exists within map + if _, ok := h.envs[trimmed]; ok { + // return the non-prefixed value if exists + return h.envs[trimmed] + } } + // return empty string if not exists return "" } diff --git a/compiler/template/native/convert_test.go b/compiler/template/native/convert_test.go index 525e6a027..db0b42a8f 100644 --- a/compiler/template/native/convert_test.go +++ b/compiler/template/native/convert_test.go @@ -1,3 +1,7 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + package native import ( @@ -14,6 +18,14 @@ func Test_convertPlatformVars(t *testing.T) { templateName string want raw.StringSliceMap }{ + { + name: "with all deployment parameter prefixed vars", + slice: raw.StringSliceMap{ + "DEPLOYMENT_PARAMETER_IMAGE": "alpine:3.14", + }, + templateName: "foo", + want: raw.StringSliceMap{"deployment_parameter_image": "alpine:3.14", "template_name": "foo"}, + }, { name: "with all vela prefixed vars", slice: raw.StringSliceMap{ @@ -26,17 +38,21 @@ func Test_convertPlatformVars(t *testing.T) { want: raw.StringSliceMap{"build_author": "octocat", "repo_full_name": "go-vela/hello-world", "user_admin": "true", "workspace": "/vela/src/github.com/go-vela/hello-world", "template_name": "foo"}, }, { - name: "with combination of vela and user vars", + name: "with combination of deployment parameter, vela, and user vars", slice: raw.StringSliceMap{ - "VELA_BUILD_AUTHOR": "octocat", - "VELA_REPO_FULL_NAME": "go-vela/hello-world", - "FOO_VAR1": "test1", - "BAR_VAR1": "test2", + "DEPLOYMENT_PARAMETER_IMAGE": "alpine:3.14", + "VELA_BUILD_AUTHOR": "octocat", + "VELA_REPO_FULL_NAME": "go-vela/hello-world", + "VELA_USER_ADMIN": "true", + "VELA_WORKSPACE": "/vela/src/github.com/go-vela/hello-world", + "FOO_VAR1": "test1", + "BAR_VAR1": "test2", }, templateName: "foo", - want: raw.StringSliceMap{"build_author": "octocat", "repo_full_name": "go-vela/hello-world", "template_name": "foo"}, + want: raw.StringSliceMap{"deployment_parameter_image": "alpine:3.14", "build_author": "octocat", "repo_full_name": "go-vela/hello-world", "user_admin": "true", "workspace": "/vela/src/github.com/go-vela/hello-world", "template_name": "foo"}, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := convertPlatformVars(tt.slice, tt.templateName); !reflect.DeepEqual(got, tt.want) { @@ -50,15 +66,57 @@ func Test_funcHandler_returnPlatformVar(t *testing.T) { type fields struct { envs raw.StringSliceMap } + type args struct { input string } + tests := []struct { name string fields fields args args want string }{ + { + name: "existing deployment parameter without prefix (lowercase)", + fields: fields{ + envs: raw.StringSliceMap{ + "image": "alpine", + }, + }, + args: args{input: "image"}, + want: "alpine", + }, + { + name: "existing deployment parameter without prefix (uppercase)", + fields: fields{ + envs: raw.StringSliceMap{ + "image": "alpine", + }, + }, + args: args{input: "IMAGE"}, + want: "alpine", + }, + { + name: "existing deployment parameter with prefix (lowercase)", + fields: fields{ + envs: raw.StringSliceMap{ + "image": "alpine", + }, + }, + args: args{input: "deployment_parameter_image"}, + want: "alpine", + }, + { + name: "existing deployment parameter with prefix (uppercase)", + fields: fields{ + envs: raw.StringSliceMap{ + "image": "alpine", + }, + }, + args: args{input: "DEPLOYMENT_PARAMETER_IMAGE"}, + want: "alpine", + }, { name: "existing platform var without prefix (lowercase)", fields: fields{ @@ -110,6 +168,7 @@ func Test_funcHandler_returnPlatformVar(t *testing.T) { want: "", }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { h := funcHandler{ diff --git a/compiler/template/native/doc.go b/compiler/template/native/doc.go index 55af6bfcb..34c41a275 100644 --- a/compiler/template/native/doc.go +++ b/compiler/template/native/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/compiler/template/native" +// import "github.com/go-vela/server/compiler/template/native" package native diff --git a/compiler/template/native/render.go b/compiler/template/native/render.go index b71cf7afc..869e4588a 100644 --- a/compiler/template/native/render.go +++ b/compiler/template/native/render.go @@ -1,3 +1,7 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + package native import ( @@ -14,13 +18,12 @@ import ( "github.com/buildkite/yaml" ) -// RenderStep combines the template with the step in the yaml pipeline. -// nolint: lll // ignore long line length due to return args -func RenderStep(tmpl string, s *types.Step) (types.StepSlice, types.SecretSlice, types.ServiceSlice, raw.StringSliceMap, error) { +// Render combines the template with the step in the yaml pipeline. +func Render(tmpl string, name string, tName string, environment raw.StringSliceMap, variables map[string]interface{}) (*types.Build, error) { buffer := new(bytes.Buffer) config := new(types.Build) - velaFuncs := funcHandler{envs: convertPlatformVars(s.Environment, s.Name)} + velaFuncs := funcHandler{envs: convertPlatformVars(environment, name)} templateFuncMap := map[string]interface{}{ "vela": velaFuncs.returnPlatformVar, "toYaml": toYAML, @@ -36,40 +39,37 @@ func RenderStep(tmpl string, s *types.Step) (types.StepSlice, types.SecretSlice, // parse the template with Masterminds/sprig functions // // https://pkg.go.dev/github.com/Masterminds/sprig?tab=doc#TxtFuncMap - t, err := template.New(s.Name).Funcs(sf).Funcs(templateFuncMap).Parse(tmpl) + t, err := template.New(name).Funcs(sf).Funcs(templateFuncMap).Parse(tmpl) if err != nil { - // nolint: lll // ignore long line length due to return arguments - return types.StepSlice{}, types.SecretSlice{}, types.ServiceSlice{}, raw.StringSliceMap{}, fmt.Errorf("unable to parse template %s: %v", s.Template.Name, err) + return nil, fmt.Errorf("unable to parse template %s: %w", tName, err) } // apply the variables to the parsed template - err = t.Execute(buffer, s.Template.Variables) + err = t.Execute(buffer, variables) if err != nil { - // nolint: lll // ignore long line length due to return arguments - return types.StepSlice{}, types.SecretSlice{}, types.ServiceSlice{}, raw.StringSliceMap{}, fmt.Errorf("unable to execute template %s: %v", s.Template.Name, err) + return nil, fmt.Errorf("unable to execute template %s: %w", tName, err) } // unmarshal the template to the pipeline err = yaml.Unmarshal(buffer.Bytes(), config) if err != nil { - // nolint: lll // ignore long line length due to return args - return types.StepSlice{}, types.SecretSlice{}, types.ServiceSlice{}, raw.StringSliceMap{}, fmt.Errorf("unable to unmarshal yaml: %v", err) + return nil, fmt.Errorf("unable to unmarshal yaml: %w", err) } // ensure all templated steps have template prefix for index, newStep := range config.Steps { - config.Steps[index].Name = fmt.Sprintf("%s_%s", s.Name, newStep.Name) + config.Steps[index].Name = fmt.Sprintf("%s_%s", name, newStep.Name) } - return config.Steps, config.Secrets, config.Services, config.Environment, nil + return &types.Build{Metadata: config.Metadata, Steps: config.Steps, Secrets: config.Secrets, Services: config.Services, Environment: config.Environment, Templates: config.Templates}, nil } // RenderBuild renders the templated build. -func RenderBuild(b string, envs map[string]string) (*types.Build, error) { +func RenderBuild(tmpl string, b string, envs map[string]string, variables map[string]interface{}) (*types.Build, error) { buffer := new(bytes.Buffer) config := new(types.Build) - velaFuncs := funcHandler{envs: convertPlatformVars(envs, "")} + velaFuncs := funcHandler{envs: convertPlatformVars(envs, tmpl)} templateFuncMap := map[string]interface{}{ "vela": velaFuncs.returnPlatformVar, "toYaml": toYAML, @@ -85,13 +85,13 @@ func RenderBuild(b string, envs map[string]string) (*types.Build, error) { // parse the template with Masterminds/sprig functions // // https://pkg.go.dev/github.com/Masterminds/sprig?tab=doc#TxtFuncMap - t, err := template.New("build").Funcs(sf).Funcs(templateFuncMap).Parse(b) + t, err := template.New(tmpl).Funcs(sf).Funcs(templateFuncMap).Parse(b) if err != nil { return nil, err } // execute the template - err = t.Execute(buffer, "") + err = t.Execute(buffer, variables) if err != nil { return nil, fmt.Errorf("unable to execute template: %w", err) } diff --git a/compiler/template/native/render_test.go b/compiler/template/native/render_test.go index 04dd9fef4..1380f6703 100644 --- a/compiler/template/native/render_test.go +++ b/compiler/template/native/render_test.go @@ -5,7 +5,7 @@ package native import ( - "io/ioutil" + "os" "testing" goyaml "github.com/buildkite/yaml" @@ -15,11 +15,12 @@ import ( "github.com/go-vela/types/yaml" ) -func TestNative_RenderStep(t *testing.T) { +func TestNative_Render(t *testing.T) { type args struct { velaFile string templateFile string } + tests := []struct { name string args args @@ -39,9 +40,10 @@ func TestNative_RenderStep(t *testing.T) { {"disallowed env func", args{velaFile: "testdata/step/basic/step.yml", templateFile: "testdata/step/disallowed/tmpl_env.yml"}, "", true}, {"disallowed expandenv func", args{velaFile: "testdata/step/basic/step.yml", templateFile: "testdata/step/disallowed/tmpl_expandenv.yml"}, "", true}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sFile, err := ioutil.ReadFile(tt.args.velaFile) + sFile, err := os.ReadFile(tt.args.velaFile) if err != nil { t.Error(err) } @@ -54,19 +56,19 @@ func TestNative_RenderStep(t *testing.T) { "VELA_REPO_FULL_NAME": "octocat/hello-world", } - tmpl, err := ioutil.ReadFile(tt.args.templateFile) + tmpl, err := os.ReadFile(tt.args.templateFile) if err != nil { t.Error(err) } - steps, secrets, services, environment, err := RenderStep(string(tmpl), b.Steps[0]) + tmplBuild, err := Render(string(tmpl), b.Steps[0].Name, b.Steps[0].Template.Name, b.Steps[0].Environment, b.Steps[0].Template.Variables) if (err != nil) != tt.wantErr { - t.Errorf("RenderStep() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Render() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr != true { - wFile, err := ioutil.ReadFile(tt.wantFile) + wFile, err := os.ReadFile(tt.wantFile) if err != nil { t.Error(err) } @@ -80,17 +82,17 @@ func TestNative_RenderStep(t *testing.T) { wantServices := w.Services wantEnvironment := w.Environment - if diff := cmp.Diff(wantSteps, steps); diff != "" { - t.Errorf("RenderStep() mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(wantSteps, tmplBuild.Steps); diff != "" { + t.Errorf("Render() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(wantSecrets, secrets); diff != "" { - t.Errorf("RenderStep() mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(wantSecrets, tmplBuild.Secrets); diff != "" { + t.Errorf("Render() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(wantServices, services); diff != "" { - t.Errorf("RenderStep() mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(wantServices, tmplBuild.Services); diff != "" { + t.Errorf("Render() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(wantEnvironment, environment); diff != "" { - t.Errorf("RenderStep() mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(wantEnvironment, tmplBuild.Environment); diff != "" { + t.Errorf("Render() mismatch (-want +got):\n%s", diff) } } }) @@ -101,6 +103,7 @@ func TestNative_RenderBuild(t *testing.T) { type args struct { velaFile string } + tests := []struct { name string args args @@ -111,24 +114,25 @@ func TestNative_RenderBuild(t *testing.T) { {"stages", args{velaFile: "testdata/build/basic_stages/build.yml"}, "testdata/build/basic_stages/want.yml", false}, {"conditional match", args{velaFile: "testdata/build/conditional/build.yml"}, "testdata/build/conditional/want.yml", false}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sFile, err := ioutil.ReadFile(tt.args.velaFile) + sFile, err := os.ReadFile(tt.args.velaFile) if err != nil { t.Error(err) } - got, err := RenderBuild(string(sFile), map[string]string{ + got, err := RenderBuild("build", string(sFile), map[string]string{ "VELA_REPO_FULL_NAME": "octocat/hello-world", "VELA_BUILD_BRANCH": "master", - }) + }, map[string]interface{}{}) if (err != nil) != tt.wantErr { t.Errorf("RenderBuild() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr != true { - wFile, err := ioutil.ReadFile(tt.wantFile) + wFile, err := os.ReadFile(tt.wantFile) if err != nil { t.Error(err) } diff --git a/compiler/template/starlark/convert.go b/compiler/template/starlark/convert.go index 9112bf7d5..474086316 100644 --- a/compiler/template/starlark/convert.go +++ b/compiler/template/starlark/convert.go @@ -47,6 +47,7 @@ func convertTemplateVars(m map[string]interface{}) (*starlark.Dict, error) { // https://pkg.go.dev/go.starlark.net/starlark#StringDict func convertPlatformVars(slice raw.StringSliceMap, name string) (*starlark.Dict, error) { build := starlark.NewDict(0) + deployment := starlark.NewDict(0) repo := starlark.NewDict(0) user := starlark.NewDict(0) system := starlark.NewDict(0) @@ -56,14 +57,22 @@ func convertPlatformVars(slice raw.StringSliceMap, name string) (*starlark.Dict, if err != nil { return nil, err } + + err = dict.SetKey(starlark.String("deployment"), deployment) + if err != nil { + return nil, err + } + err = dict.SetKey(starlark.String("repo"), repo) if err != nil { return nil, err } + err = dict.SetKey(starlark.String("user"), user) if err != nil { return nil, err } + err = dict.SetKey(starlark.String("system"), system) if err != nil { return nil, err @@ -74,31 +83,47 @@ func convertPlatformVars(slice raw.StringSliceMap, name string) (*starlark.Dict, return nil, err } + // iterate through the list of key/value pairs provided for key, value := range slice { + // lowercase the key key = strings.ToLower(key) - if strings.HasPrefix(key, "vela_") { - key = strings.TrimPrefix(key, "vela_") - - switch { - case strings.HasPrefix(key, "build_"): - err := build.SetKey(starlark.String(strings.TrimPrefix(key, "build_")), starlark.String(value)) - if err != nil { - return nil, err - } - case strings.HasPrefix(key, "repo_"): - err := repo.SetKey(starlark.String(strings.TrimPrefix(key, "repo_")), starlark.String(value)) - if err != nil { - return nil, err - } - case strings.HasPrefix(key, "user_"): - err := user.SetKey(starlark.String(strings.TrimPrefix(key, "user_")), starlark.String(value)) - if err != nil { - return nil, err - } - default: - err := system.SetKey(starlark.String(key), starlark.String(value)) - if err != nil { - return nil, err + + // iterate through the list of possible prefixes to look for + for _, prefix := range []string{"deployment_parameter_", "vela_"} { + // check if the key has the prefix + if strings.HasPrefix(key, prefix) { + // trim the prefix from the input key + key = strings.TrimPrefix(key, prefix) + + // check if the prefix is from 'vela_*' + if strings.EqualFold(prefix, "vela_") { + switch { + case strings.HasPrefix(key, "build_"): + err := build.SetKey(starlark.String(strings.TrimPrefix(key, "build_")), starlark.String(value)) + if err != nil { + return nil, err + } + case strings.HasPrefix(key, "repo_"): + err := repo.SetKey(starlark.String(strings.TrimPrefix(key, "repo_")), starlark.String(value)) + if err != nil { + return nil, err + } + case strings.HasPrefix(key, "user_"): + err := user.SetKey(starlark.String(strings.TrimPrefix(key, "user_")), starlark.String(value)) + if err != nil { + return nil, err + } + default: + err := system.SetKey(starlark.String(key), starlark.String(value)) + if err != nil { + return nil, err + } + } + } else { // prefix is from 'deployment_parameter_*' + err := deployment.SetKey(starlark.String(key), starlark.String(value)) + if err != nil { + return nil, err + } } } } diff --git a/compiler/template/starlark/convert_test.go b/compiler/template/starlark/convert_test.go index 77cd577ce..b6eb03bf8 100644 --- a/compiler/template/starlark/convert_test.go +++ b/compiler/template/starlark/convert_test.go @@ -20,24 +20,28 @@ func TestStarlark_Render_convertTemplateVars(t *testing.T) { tags = append(tags, starlark.String("1.15")) commands := starlark.NewDict(16) + err := commands.SetKey(starlark.String("test"), starlark.String("go test ./...")) if err != nil { t.Error(err) } strWant := starlark.NewDict(0) + err = strWant.SetKey(starlark.String("pull"), starlark.String("always")) if err != nil { t.Error(err) } arrayWant := starlark.NewDict(0) + err = arrayWant.SetKey(starlark.String("tags"), tags) if err != nil { t.Error(err) } mapWant := starlark.NewDict(0) + err = mapWant.SetKey(starlark.String("commands"), commands) if err != nil { t.Error(err) @@ -78,27 +82,33 @@ func TestStarlark_Render_convertTemplateVars(t *testing.T) { } } -func TestStarlark_Render_velaEnvironmentData(t *testing.T) { +func TestStarlark_Render_convertPlatformVars(t *testing.T) { // setup types - build := starlark.NewDict(1) + build := starlark.NewDict(0) err := build.SetKey(starlark.String("author"), starlark.String("octocat")) if err != nil { t.Error(err) } - repo := starlark.NewDict(1) + deployment := starlark.NewDict(0) + err = deployment.SetKey(starlark.String("image"), starlark.String("alpine:3.14")) + if err != nil { + t.Error(err) + } + + repo := starlark.NewDict(0) err = repo.SetKey(starlark.String("full_name"), starlark.String("go-vela/hello-world")) if err != nil { t.Error(err) } - user := starlark.NewDict(1) + user := starlark.NewDict(0) err = user.SetKey(starlark.String("admin"), starlark.String("true")) if err != nil { t.Error(err) } - system := starlark.NewDict(2) + system := starlark.NewDict(0) err = system.SetKey(starlark.String("template_name"), starlark.String("foo")) if err != nil { t.Error(err) @@ -108,20 +118,76 @@ func TestStarlark_Render_velaEnvironmentData(t *testing.T) { t.Error(err) } - withAllPre := starlark.NewDict(0) - err = withAllPre.SetKey(starlark.String("build"), build) + // setup full dictionary + withAll := starlark.NewDict(0) + err = withAll.SetKey(starlark.String("build"), build) + if err != nil { + t.Error(err) + } + err = withAll.SetKey(starlark.String("deployment"), deployment) + if err != nil { + t.Error(err) + } + err = withAll.SetKey(starlark.String("repo"), repo) + if err != nil { + t.Error(err) + } + err = withAll.SetKey(starlark.String("user"), user) + if err != nil { + t.Error(err) + } + err = withAll.SetKey(starlark.String("system"), system) + if err != nil { + t.Error(err) + } + + // setup vela dictionary + withAllVela := starlark.NewDict(0) + err = withAllVela.SetKey(starlark.String("build"), build) + if err != nil { + t.Error(err) + } + err = withAllVela.SetKey(starlark.String("deployment"), starlark.NewDict(0)) + if err != nil { + t.Error(err) + } + err = withAllVela.SetKey(starlark.String("repo"), repo) + if err != nil { + t.Error(err) + } + err = withAllVela.SetKey(starlark.String("user"), user) + if err != nil { + t.Error(err) + } + err = withAllVela.SetKey(starlark.String("system"), system) + if err != nil { + t.Error(err) + } + + // setup deployment dictionary + withAllDeployment := starlark.NewDict(0) + err = withAllDeployment.SetKey(starlark.String("build"), starlark.NewDict(0)) if err != nil { t.Error(err) } - err = withAllPre.SetKey(starlark.String("repo"), repo) + err = withAllDeployment.SetKey(starlark.String("deployment"), deployment) if err != nil { t.Error(err) } - err = withAllPre.SetKey(starlark.String("user"), user) + err = withAllDeployment.SetKey(starlark.String("repo"), starlark.NewDict(0)) if err != nil { t.Error(err) } - err = withAllPre.SetKey(starlark.String("system"), system) + err = withAllDeployment.SetKey(starlark.String("user"), starlark.NewDict(0)) + if err != nil { + t.Error(err) + } + system = starlark.NewDict(0) + err = system.SetKey(starlark.String("template_name"), starlark.String("foo")) + if err != nil { + t.Error(err) + } + err = withAllDeployment.SetKey(starlark.String("system"), system) if err != nil { t.Error(err) } @@ -133,6 +199,14 @@ func TestStarlark_Render_velaEnvironmentData(t *testing.T) { want *starlark.Dict wantErr bool }{ + { + name: "with all deployment parameter prefixed vars", + slice: raw.StringSliceMap{ + "DEPLOYMENT_PARAMETER_IMAGE": "alpine:3.14", + }, + templateName: "foo", + want: withAllDeployment, + }, { name: "with all vela prefixed var", slice: raw.StringSliceMap{ @@ -142,9 +216,24 @@ func TestStarlark_Render_velaEnvironmentData(t *testing.T) { "VELA_WORKSPACE": "/vela/src/github.com/go-vela/hello-world", }, templateName: "foo", - want: withAllPre, + want: withAllVela, + }, + { + name: "with combination of deployment parameter, vela, and user vars", + slice: raw.StringSliceMap{ + "DEPLOYMENT_PARAMETER_IMAGE": "alpine:3.14", + "VELA_BUILD_AUTHOR": "octocat", + "VELA_REPO_FULL_NAME": "go-vela/hello-world", + "VELA_USER_ADMIN": "true", + "VELA_WORKSPACE": "/vela/src/github.com/go-vela/hello-world", + "FOO_VAR1": "test1", + "BAR_VAR1": "test2", + }, + templateName: "foo", + want: withAll, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := convertPlatformVars(tt.slice, tt.templateName) diff --git a/compiler/template/starlark/render.go b/compiler/template/starlark/render.go index 1d4cf1296..96b2f1b3d 100644 --- a/compiler/template/starlark/render.go +++ b/compiler/template/starlark/render.go @@ -8,8 +8,8 @@ import ( "bytes" "errors" "fmt" - "github.com/go-vela/types/raw" + "go.starlark.net/starlarkstruct" yaml "github.com/buildkite/yaml" types "github.com/go-vela/types/yaml" @@ -30,58 +30,60 @@ var ( ErrInvalidPipelineReturn = errors.New("invalid pipeline return in template") ) -// RenderStep combines the template with the step in the yaml pipeline. -// -// nolint: funlen,lll // ignore function length due to comments -func RenderStep(tmpl string, s *types.Step) (types.StepSlice, types.SecretSlice, types.ServiceSlice, raw.StringSliceMap, error) { +// Render combines the template with the step in the yaml pipeline. +func Render(tmpl string, name string, tName string, environment raw.StringSliceMap, variables map[string]interface{}) (*types.Build, error) { config := new(types.Build) - thread := &starlark.Thread{Name: s.Name} + thread := &starlark.Thread{Name: name} // arbitrarily limiting the steps of the thread to 5000 to help prevent infinite loops // may need to further investigate spawning a separate POSIX process if user input is problematic // see https://github.com/google/starlark-go/issues/160#issuecomment-466794230 for further details - // - // nolint: gomnd // ignore magic number - thread.SetMaxExecutionSteps(5000) - globals, err := starlark.ExecFile(thread, s.Template.Name, tmpl, nil) + thread.SetMaxExecutionSteps(GetStarlarkExecutionStepLimit()) + + predeclared := starlark.StringDict{"struct": starlark.NewBuiltin("struct", starlarkstruct.Make)} + + globals, err := starlark.ExecFile(thread, tName, tmpl, predeclared) + if err != nil { - return nil, nil, nil, nil, err + return nil, err } // check the provided template has a main function mainVal, ok := globals["main"] if !ok { - return nil, nil, nil, nil, fmt.Errorf("%s: %s", ErrMissingMainFunc, s.Template.Name) + return nil, fmt.Errorf("%w: %s", ErrMissingMainFunc, tName) } // check the provided main is a function main, ok := mainVal.(starlark.Callable) if !ok { - return nil, nil, nil, nil, fmt.Errorf("%s: %s", ErrInvalidMainFunc, s.Template.Name) + return nil, fmt.Errorf("%w: %s", ErrInvalidMainFunc, tName) } // load the user provided vars into a starlark type - userVars, err := convertTemplateVars(s.Template.Variables) + userVars, err := convertTemplateVars(variables) if err != nil { - return nil, nil, nil, nil, err + return nil, err } // load the platform provided vars into a starlark type - velaVars, err := convertPlatformVars(s.Environment, s.Name) + velaVars, err := convertPlatformVars(environment, name) if err != nil { - return nil, nil, nil, nil, err + return nil, err } // add the user and platform vars to a context to be used // within the template caller i.e. ctx["vela"] or ctx["vars"] context := starlark.NewDict(0) + err = context.SetKey(starlark.String("vela"), velaVars) if err != nil { - return nil, nil, nil, nil, err + return nil, err } + err = context.SetKey(starlark.String("vars"), userVars) if err != nil { - return nil, nil, nil, nil, err + return nil, err } args := starlark.Tuple([]starlark.Value{context}) @@ -89,7 +91,7 @@ func RenderStep(tmpl string, s *types.Step) (types.StepSlice, types.SecretSlice, // execute Starlark program from Go. mainVal, err = starlark.Call(thread, main, args, nil) if err != nil { - return nil, nil, nil, nil, err + return nil, err } buf := new(bytes.Buffer) @@ -99,50 +101,63 @@ func RenderStep(tmpl string, s *types.Step) (types.StepSlice, types.SecretSlice, case *starlark.List: for i := 0; i < v.Len(); i++ { item := v.Index(i) + buf.WriteString("---\n") + err = writeJSON(buf, item) if err != nil { - return nil, nil, nil, nil, err + return nil, err } + buf.WriteString("\n") } case *starlark.Dict: buf.WriteString("---\n") + err = writeJSON(buf, v) if err != nil { - return nil, nil, nil, nil, err + return nil, err } default: - return nil, nil, nil, nil, fmt.Errorf("%s: %s", ErrInvalidPipelineReturn, mainVal.Type()) + return nil, fmt.Errorf("%w: %s", ErrInvalidPipelineReturn, mainVal.Type()) } // unmarshal the template to the pipeline err = yaml.Unmarshal(buf.Bytes(), config) if err != nil { - // nolint: lll // ignore long line length due to return args - return types.StepSlice{}, types.SecretSlice{}, types.ServiceSlice{}, raw.StringSliceMap{}, fmt.Errorf("unable to unmarshal yaml: %v", err) + return nil, fmt.Errorf("unable to unmarshal yaml: %w", err) } // ensure all templated steps have template prefix for index, newStep := range config.Steps { - config.Steps[index].Name = fmt.Sprintf("%s_%s", s.Name, newStep.Name) + config.Steps[index].Name = fmt.Sprintf("%s_%s", name, newStep.Name) } - return config.Steps, config.Secrets, config.Services, config.Environment, nil + return &types.Build{Steps: config.Steps, Secrets: config.Secrets, Services: config.Services, Environment: config.Environment}, nil +} + +// GetStarlarkExecutionStepLimit may eventually look up config or calculate it +func GetStarlarkExecutionStepLimit() uint64 { + // arbitrarily limiting the steps of the thread to help prevent infinite loops + // may need to further investigate spawning a separate POSIX process if user input is problematic + // see https://github.com/google/starlark-go/issues/160#issuecomment-466794230 for further details + // This value was previously 5000 and that inhibited a four-dimensional build matrix from working. + return 7500 } // RenderBuild renders the templated build. -func RenderBuild(b string, envs map[string]string) (*types.Build, error) { +// +//nolint:lll // ignore function length due to input args +func RenderBuild(tmpl string, b string, envs map[string]string, variables map[string]interface{}) (*types.Build, error) { config := new(types.Build) thread := &starlark.Thread{Name: "templated-base"} - // arbitrarily limiting the steps of the thread to 5000 to help prevent infinite loops - // may need to further investigate spawning a separate POSIX process if user input is problematic - // see https://github.com/google/starlark-go/issues/160#issuecomment-466794230 for further details - // - // nolint: gomnd // ignore magic number - thread.SetMaxExecutionSteps(5000) - globals, err := starlark.ExecFile(thread, "templated-base", b, nil) + + thread.SetMaxExecutionSteps(GetStarlarkExecutionStepLimit()) + + predeclared := starlark.StringDict{"struct": starlark.NewBuiltin("struct", starlarkstruct.Make)} + + globals, err := starlark.ExecFile(thread, "templated-base", b, predeclared) if err != nil { return nil, err } @@ -150,17 +165,23 @@ func RenderBuild(b string, envs map[string]string) (*types.Build, error) { // check the provided template has a main function mainVal, ok := globals["main"] if !ok { - return nil, fmt.Errorf("%s: %s", ErrMissingMainFunc, "templated-base") + return nil, fmt.Errorf("%w: %s", ErrMissingMainFunc, "templated-base") } // check the provided main is a function main, ok := mainVal.(starlark.Callable) if !ok { - return nil, fmt.Errorf("%s: %s", ErrInvalidMainFunc, "templated-base") + return nil, fmt.Errorf("%w: %s", ErrInvalidMainFunc, "templated-base") + } + + // load the user provided vars into a starlark type + userVars, err := convertTemplateVars(variables) + if err != nil { + return nil, err } // load the platform provided vars into a starlark type - velaVars, err := convertPlatformVars(envs, "") + velaVars, err := convertPlatformVars(envs, tmpl) if err != nil { return nil, err } @@ -168,11 +189,17 @@ func RenderBuild(b string, envs map[string]string) (*types.Build, error) { // add the user and platform vars to a context to be used // within the template caller i.e. ctx["vela"] or ctx["vars"] context := starlark.NewDict(0) + err = context.SetKey(starlark.String("vela"), velaVars) if err != nil { return nil, err } + err = context.SetKey(starlark.String("vars"), userVars) + if err != nil { + return nil, err + } + args := starlark.Tuple([]starlark.Value{context}) // execute Starlark program from Go. @@ -188,27 +215,31 @@ func RenderBuild(b string, envs map[string]string) (*types.Build, error) { case *starlark.List: for i := 0; i < v.Len(); i++ { item := v.Index(i) + buf.WriteString("---\n") + err = writeJSON(buf, item) if err != nil { return nil, err } + buf.WriteString("\n") } case *starlark.Dict: buf.WriteString("---\n") + err = writeJSON(buf, v) if err != nil { return nil, err } default: - return nil, fmt.Errorf("%s: %s", ErrInvalidPipelineReturn, mainVal.Type()) + return nil, fmt.Errorf("%w: %s", ErrInvalidPipelineReturn, mainVal.Type()) } // unmarshal the template to the pipeline err = yaml.Unmarshal(buf.Bytes(), config) if err != nil { - return nil, fmt.Errorf("unable to unmarshal yaml: %v", err) + return nil, fmt.Errorf("unable to unmarshal yaml: %w", err) } return config, nil diff --git a/compiler/template/starlark/render_test.go b/compiler/template/starlark/render_test.go index aa14caae9..c602bac38 100644 --- a/compiler/template/starlark/render_test.go +++ b/compiler/template/starlark/render_test.go @@ -5,7 +5,7 @@ package starlark import ( - "io/ioutil" + "os" "testing" goyaml "github.com/buildkite/yaml" @@ -14,11 +14,12 @@ import ( "github.com/google/go-cmp/cmp" ) -func TestStarlark_RenderStep(t *testing.T) { +func TestStarlark_Render(t *testing.T) { type args struct { velaFile string starlarkFile string } + tests := []struct { name string args args @@ -31,9 +32,10 @@ func TestStarlark_RenderStep(t *testing.T) { {"platform vars", args{velaFile: "testdata/step/with_vars_plat/step.yml", starlarkFile: "testdata/step/with_vars_plat/template.star"}, "testdata/step/with_vars_plat/want.yml", false}, {"cancel due to complexity", args{velaFile: "testdata/step/cancel/step.yml", starlarkFile: "testdata/step/cancel/template.star"}, "", true}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sFile, err := ioutil.ReadFile(tt.args.velaFile) + sFile, err := os.ReadFile(tt.args.velaFile) if err != nil { t.Error(err) } @@ -46,19 +48,19 @@ func TestStarlark_RenderStep(t *testing.T) { "VELA_REPO_FULL_NAME": "octocat/hello-world", } - tmpl, err := ioutil.ReadFile(tt.args.starlarkFile) + tmpl, err := os.ReadFile(tt.args.starlarkFile) if err != nil { t.Error(err) } - steps, secrets, services, environment, err := RenderStep(string(tmpl), b.Steps[0]) + tmplBuild, err := Render(string(tmpl), b.Steps[0].Name, b.Steps[0].Template.Name, b.Steps[0].Environment, b.Steps[0].Template.Variables) if (err != nil) != tt.wantErr { - t.Errorf("RenderStep() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Render() error = %v, wantErr %v", err, tt.wantErr) return } if tt.wantErr != true { - wFile, err := ioutil.ReadFile(tt.wantFile) + wFile, err := os.ReadFile(tt.wantFile) if err != nil { t.Error(err) } @@ -72,17 +74,17 @@ func TestStarlark_RenderStep(t *testing.T) { wantServices := w.Services wantEnvironment := w.Environment - if diff := cmp.Diff(wantSteps, steps); diff != "" { - t.Errorf("RenderStep() mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(wantSteps, tmplBuild.Steps); diff != "" { + t.Errorf("Render() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(wantSecrets, secrets); diff != "" { - t.Errorf("RenderStep() mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(wantSecrets, tmplBuild.Secrets); diff != "" { + t.Errorf("Render() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(wantServices, services); diff != "" { - t.Errorf("RenderStep() mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(wantServices, tmplBuild.Services); diff != "" { + t.Errorf("Render() mismatch (-want +got):\n%s", diff) } - if diff := cmp.Diff(wantEnvironment, environment); diff != "" { - t.Errorf("RenderStep() mismatch (-want +got):\n%s", diff) + if diff := cmp.Diff(wantEnvironment, tmplBuild.Environment); diff != "" { + t.Errorf("Render() mismatch (-want +got):\n%s", diff) } } }) @@ -90,9 +92,12 @@ func TestStarlark_RenderStep(t *testing.T) { } func TestNative_RenderBuild(t *testing.T) { + noWantFile := "none" + type args struct { velaFile string } + tests := []struct { name string args args @@ -102,25 +107,29 @@ func TestNative_RenderBuild(t *testing.T) { {"steps", args{velaFile: "testdata/build/basic/build.star"}, "testdata/build/basic/want.yml", false}, {"stages", args{velaFile: "testdata/build/basic_stages/build.star"}, "testdata/build/basic_stages/want.yml", false}, {"conditional match", args{velaFile: "testdata/build/conditional/build.star"}, "testdata/build/conditional/want.yml", false}, + {"steps, with structs", args{velaFile: "testdata/build/with_struct/build.star"}, "testdata/build/with_struct/want.yml", false}, + {"large build to stress execution steps", args{velaFile: "testdata/build/large/build.star"}, noWantFile, false}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - sFile, err := ioutil.ReadFile(tt.args.velaFile) + sFile, err := os.ReadFile(tt.args.velaFile) if err != nil { t.Error(err) } - got, err := RenderBuild(string(sFile), map[string]string{ + got, err := RenderBuild("build", string(sFile), map[string]string{ "VELA_REPO_FULL_NAME": "octocat/hello-world", "VELA_BUILD_BRANCH": "master", - }) + "VELA_REPO_ORG": "octocat", + }, map[string]interface{}{}) if (err != nil) != tt.wantErr { t.Errorf("RenderBuild() error = %v, wantErr %v", err, tt.wantErr) return } - if tt.wantErr != true { - wFile, err := ioutil.ReadFile(tt.wantFile) + if tt.wantErr != true && tt.wantFile != noWantFile { + wFile, err := os.ReadFile(tt.wantFile) if err != nil { t.Error(err) } diff --git a/compiler/template/starlark/starlark.go b/compiler/template/starlark/starlark.go index fd987f6dc..0299ef259 100644 --- a/compiler/template/starlark/starlark.go +++ b/compiler/template/starlark/starlark.go @@ -33,7 +33,7 @@ var ( // // https://github.com/wonderix/shalm/blob/899b8f7787883d40619eefcc39bd12f42a09b5e7/pkg/shalm/convert.go#L14-L85 // -// nolint: gocyclo,funlen,lll // ignore above line length and function length due to comments +//nolint:gocyclo // ignore complexity func toStarlark(value interface{}) (starlark.Value, error) { logrus.Tracef("converting %v to starlark type", value) @@ -78,6 +78,7 @@ func toStarlark(value interface{}) (starlark.Value, error) { if err != nil { return nil, err } + a = append(a, val) } @@ -90,11 +91,11 @@ func toStarlark(value interface{}) (starlark.Value, error) { return val, nil case reflect.Map: - // nolint: gomnd // ignore magic number d := starlark.NewDict(16) for _, key := range v.MapKeys() { strct := v.MapIndex(key) + keyValue, err := toStarlark(key.Interface()) if err != nil { return nil, err @@ -137,7 +138,7 @@ func toStarlark(value interface{}) (starlark.Value, error) { } } - return nil, fmt.Errorf("%s: %v", ErrUnableToConvertStarlark, value) + return nil, fmt.Errorf("%w: %v", ErrUnableToConvertStarlark, value) } // writeJSON takes an starlark input and return the valid JSON @@ -151,7 +152,7 @@ func toStarlark(value interface{}) (starlark.Value, error) { // if/when we try to return values it breaks the recursion. Panics were swapped to error // returns from implementation. // -// nolint: gocyclo,funlen // ignore cyclomatic complexity and function length +//nolint:gocyclo // ignore cyclomatic complexity func writeJSON(out *bytes.Buffer, v starlark.Value) error { logrus.Tracef("converting %v to JSON", v) @@ -268,7 +269,7 @@ func writeJSON(out *bytes.Buffer, v starlark.Value) error { logrus.Error(err) } default: - return fmt.Errorf("%s: %v", ErrUnableToConvertJSON, v) + return fmt.Errorf("%w: %v", ErrUnableToConvertJSON, v) } return nil @@ -286,5 +287,6 @@ func goQuoteIsSafe(s string) bool { return false } } + return true } diff --git a/compiler/template/starlark/starlark_test.go b/compiler/template/starlark/starlark_test.go index 378da6a2f..e3aa65626 100644 --- a/compiler/template/starlark/starlark_test.go +++ b/compiler/template/starlark/starlark_test.go @@ -1,3 +1,7 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + package starlark import ( @@ -9,16 +13,20 @@ import ( func TestStarlark_toStarlark(t *testing.T) { dict := starlark.NewDict(16) + err := dict.SetKey(starlark.String("foo"), starlark.String("bar")) if err != nil { t.Error(err) } + a := make([]starlark.Value, 0) a = append(a, starlark.Value(starlark.String("foo"))) a = append(a, starlark.Value(starlark.String("bar"))) + type args struct { value interface{} } + tests := []struct { name string args args @@ -42,6 +50,7 @@ func TestStarlark_toStarlark(t *testing.T) { {"nil", args{value: nil}, starlark.None, false}, {"map", args{value: map[string]string{"foo": "bar"}}, dict, false}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := toStarlark(tt.args.value) diff --git a/compiler/template/starlark/testdata/build/large/build.star b/compiler/template/starlark/testdata/build/large/build.star new file mode 100644 index 000000000..5ffbc482a --- /dev/null +++ b/compiler/template/starlark/testdata/build/large/build.star @@ -0,0 +1,202 @@ +###### +## Setup the build matrix with the base versions a human will maintain. +###### + +DISTRO_WITH_VERSIONS = { + # n.b. these reduce to DockerHub tags + # https://hub.docker.com/_/python/tags?name=alpine + # https://endoflife.date/alpine + 'alpine': [ + '3.17', # EOL 22 Nov 2024 + '3.18' # EOL 09 May 2025 + ], + # https://hub.docker.com/_/python/tags?name=slim + # https://endoflife.date/debian + 'debian': [ + 'slim-bullseye', # EOL 30 Jun 2026 + 'slim-bookworm' # EOL 10 Jun 2028 + ] +} +PYTHON_VERSIONS = [ + '3.8', + '3.9', + '3.10', + '3.11' +] +POETRY_VERSIONS = [ + '1.6.1' +] + +KANIKO_IMAGE = 'target/vela-kaniko:latest' +HADOLINT_IMAGE = 'hadolint/hadolint:v2.12.0-alpine' + + +## The base Docker container build step's config for push builds +def base(): + return { + 'image': KANIKO_IMAGE, + 'ruleset': { + 'event': 'push', + 'branch': 'main' + }, + 'pull': 'not_present', + 'secrets': [ + { + 'source': 'artifactory_password', + 'target': 'docker_password' + } + ] + } + + +## The base Docker container plugin params for push builds +## +## These are parameters passed to Kaniko. +def base_params(): + return { + 'username': 'ibuildallthings', + 'registry': 'docker.example.com', + 'repo': 'docker.example.com/app/multibuild' + } + + +## The step config for pull request builds +def pull_request(): + pr = base() + pr['ruleset']['event'] = 'pull_request' + pr['ruleset'].pop('branch') + return pr + + +## The Kaniko params for pull request builds +def pull_request_params(): + prp = base_params() + prp['dry_run'] = True + return prp + + +## Define a linting stage that uses Hadolint inside of a Make task +## +## This keeps our Dockerfiles tidy and compliant with conventions +def stage_linting(): + return { + 'linting': { + 'steps': [{ + 'name': 'check-docker', + 'image': HADOLINT_IMAGE, + 'pull': 'not_present', + 'commands': [ + 'time apk add --no-cache make', + 'time make check-docker' + ] + }] + } + } + + +## Build stages comprised of a step for push and pull_request builds +def stage_build_tuple(distro, distro_version, python_version, poetry_version): + pr = build_template("build", distro, distro_version, python_version, poetry_version, pull_request(), pull_request_params()) + base_step = build_template("publish", distro, distro_version, python_version, poetry_version, base(), base_params()) + combined = base_step | pr + return combined + + +## Build a single stage for a build tuple, with its base step config and plugin parameters +def build_template(step_name, distro, distro_version, python_version, poetry_version, step_def_base, step_def_params): + return { + ('python_%s_%s_%s %s' % (python_version, distro, distro_version, step_def_base['ruleset']['event'])): { + 'steps': [step_def_base | { + 'name': ('%s python-%s %s %s' % (step_name, python_version, distro, distro_version)), + 'parameters': step_def_params | { + 'dockerfile': ('python-%s.Dockerfile' % distro), + 'build_args': [ + 'PYTHON_VERSION=%s' % python_version, + '%s_VERSION=%s' % (distro.upper(), distro_version), + 'POETRY_VERSION=%s' % poetry_version + ], + 'tags': [ + '%s-%s-%s-%s' % (python_version, distro, distro_version, poetry_version), + '%s-%s-%s' % (python_version, distro, distro_version), + '%s-%s' % (python_version, distro) + ] + } + }] + } + } + + +## Define a stage that uses the Slack template +def stage_slack_notify(needs): + return { + 'slack': { + 'needs': needs, + 'steps': [{ + 'name': 'slack', + 'template': { + 'name': 'slack' + } + }] + } + } + + +## Builds the build matrix in the form of list of tuples from the constants defined at the top of the file +def build_matrix(): + BUILD_MATRIX = [] + for poetry_version in POETRY_VERSIONS: + for python_version in PYTHON_VERSIONS: + for distro in DISTRO_WITH_VERSIONS: + for distro_version in DISTRO_WITH_VERSIONS[distro]: + BUILD_MATRIX.append((distro, + distro_version, + python_version, + poetry_version)) + return BUILD_MATRIX + + +## Construct a secret +def secret(name, key, secret_type, engine='native'): + return {'name': name, 'key': key, 'engine': engine, 'type': secret_type} + + +## Construct a template +def template(name, source, version=None, template_type='github'): + real_source = '%s@%s' % (source, version) if version else source + return { + 'name': name, + 'source': real_source, + 'type': template_type + } + +## The main method, the real deal. +## +## Vela actually calls this function, its return is what Vela uses. +def main(ctx): + # Retrieve the org dynamically since we're using some org secrets + vela_repo_org = ctx['vela']['repo']['org'] if 'vela' in ctx else "UNKNOWN-ORG" + + # Build the stages from the build matrix + build_stages = {} + for (distro, distro_version, python_version, poetry_version) in build_matrix(): + build_stages = build_stages | (stage_build_tuple(distro, distro_version, python_version, poetry_version)) + + # assemble the stage list with the bookends of linting and notifications in place + stages = stage_linting() | build_stages | stage_slack_notify(build_stages.keys()) + + # Build the final output + final = { + 'version': '1', + 'templates': [ + template(name='slack', + source='git.example.com/vela/vela-templates/slack/slack.yml') + ], + 'stages': stages, + 'secrets': [ + secret('artifactory_password','platform/vela-secrets/artifactory_password_for_ibuildallthings', 'shared'), + secret('slack_webhook', vela_repo_org + '/slack_webhook', 'org') + ] + } + + return final + diff --git a/compiler/template/starlark/testdata/build/with_struct/build.star b/compiler/template/starlark/testdata/build/with_struct/build.star new file mode 100644 index 000000000..d344b64cb --- /dev/null +++ b/compiler/template/starlark/testdata/build/with_struct/build.star @@ -0,0 +1,25 @@ +def main(ctx): + step_list = [ + struct(name="foo"), + struct(name="bar"), + struct(name="star") + ] + + steps = [] + + for step in step_list: + steps.append(build_step(step)) + + return { + 'version': '1', + 'steps': steps + } + +def build_step(step): + return { + "name": "build_%s" % step.name, + "image": "alpine:latest", + 'commands': [ + "echo %s" % step.name + ] + } diff --git a/compiler/template/starlark/testdata/build/with_struct/want.yml b/compiler/template/starlark/testdata/build/with_struct/want.yml new file mode 100644 index 000000000..6297ea2b9 --- /dev/null +++ b/compiler/template/starlark/testdata/build/with_struct/want.yml @@ -0,0 +1,16 @@ +version: 1 +steps: + - name: build_foo + image: alpine:latest + commands: + - echo foo + + - name: build_bar + image: alpine:latest + commands: + - echo bar + + - name: build_star + image: alpine:latest + commands: + - echo star diff --git a/compiler/template/template.go b/compiler/template/template.go index 36dec9a6a..75cc8eda9 100644 --- a/compiler/template/template.go +++ b/compiler/template/template.go @@ -12,7 +12,7 @@ type Engine interface { // RenderBuild defines a function that combines // the template with the build. RenderBuild(template string, step *yaml.Step) (yaml.StepSlice, error) - // RenderStep defines a function that combines + // Render defines a function that combines // the template with the step. - RenderStep(template string, step *yaml.Step) (yaml.StepSlice, error) + Render(template string, step *yaml.Step) (yaml.StepSlice, error) } diff --git a/database/build/build.go b/database/build/build.go new file mode 100644 index 000000000..ce750e5ee --- /dev/null +++ b/database/build/build.go @@ -0,0 +1,83 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the BuildInterface interface. + config struct { + // specifies to skip creating tables and indexes for the Build engine + SkipCreation bool + } + + // engine represents the build functionality that implements the BuildInterface interface. + engine struct { + // engine configuration settings used in build functions + config *config + + ctx context.Context + + // gorm.io/gorm database client used in build functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in build functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with builds in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new Build engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating build database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of builds table and indexes in the database") + + return e, nil + } + + // create the builds table + err := e.CreateBuildTable(e.ctx, e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableBuild, err) + } + + // create the indexes for the builds table + err = e.CreateBuildIndexes(e.ctx) + if err != nil { + return nil, fmt.Errorf("unable to create indexes for %s table: %w", constants.TableBuild, err) + } + + return e, nil +} diff --git a/database/build/build_test.go b/database/build/build_test.go new file mode 100644 index 000000000..0e6eb9fd9 --- /dev/null +++ b/database/build/build_test.go @@ -0,0 +1,289 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "database/sql/driver" + "reflect" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestBuild_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateCreatedIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateSourceIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateStatusIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{SkipCreation: false}, + ctx: context.TODO(), + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{SkipCreation: false}, + ctx: context.TODO(), + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithContext(context.TODO()), + WithClient(test.client), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateCreatedIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateSourceIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateStatusIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithContext(context.TODO()), + WithClient(_postgres), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres build engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithContext(context.TODO()), + WithClient(_sqlite), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite build engine: %v", err) + } + + return _engine +} + +// testBuild is a test helper function to create a library +// Build type with all fields set to their zero values. +func testBuild() *library.Build { + return &library.Build{ + ID: new(int64), + RepoID: new(int64), + PipelineID: new(int64), + Number: new(int), + Parent: new(int), + Event: new(string), + EventAction: new(string), + Status: new(string), + Error: new(string), + Enqueued: new(int64), + Created: new(int64), + Started: new(int64), + Finished: new(int64), + Deploy: new(string), + Clone: new(string), + Source: new(string), + Title: new(string), + Message: new(string), + Commit: new(string), + Sender: new(string), + Author: new(string), + Email: new(string), + Link: new(string), + Branch: new(string), + Ref: new(string), + BaseRef: new(string), + HeadRef: new(string), + Host: new(string), + Runtime: new(string), + Distribution: new(string), + } +} + +// testDeployment is a test helper function to create a library +// Repo type with all fields set to their zero values. +func testDeployment() *library.Deployment { + return &library.Deployment{ + ID: new(int64), + RepoID: new(int64), + URL: new(string), + User: new(string), + Commit: new(string), + Ref: new(string), + Task: new(string), + Target: new(string), + Description: new(string), + } +} + +// testRepo is a test helper function to create a library +// Repo type with all fields set to their zero values. +func testRepo() *library.Repo { + return &library.Repo{ + ID: new(int64), + UserID: new(int64), + BuildLimit: new(int64), + Timeout: new(int64), + Counter: new(int), + PipelineType: new(string), + Hash: new(string), + Org: new(string), + Name: new(string), + FullName: new(string), + Link: new(string), + Clone: new(string), + Branch: new(string), + Visibility: new(string), + PreviousName: new(string), + Private: new(bool), + Trusted: new(bool), + Active: new(bool), + AllowPull: new(bool), + AllowPush: new(bool), + AllowDeploy: new(bool), + AllowTag: new(bool), + AllowComment: new(bool), + } +} + +// This will be used with the github.com/DATA-DOG/go-sqlmock library to compare values +// that are otherwise not easily compared. These typically would be values generated +// before adding or updating them in the database. +// +// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime +type AnyArgument struct{} + +// Match satisfies sqlmock.Argument interface. +func (a AnyArgument) Match(v driver.Value) bool { + return true +} + +// NowTimestamp is used to test whether timestamps get updated correctly to the current time with lenience. +type NowTimestamp struct{} + +// Match satisfies sqlmock.Argument interface. +func (t NowTimestamp) Match(v driver.Value) bool { + ts, ok := v.(int64) + if !ok { + return false + } + now := time.Now().Unix() + + return now-ts < 10 +} diff --git a/database/build/clean.go b/database/build/clean.go new file mode 100644 index 000000000..700998687 --- /dev/null +++ b/database/build/clean.go @@ -0,0 +1,36 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "time" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CleanBuilds updates builds to an error with a provided message with a created timestamp prior to a defined moment. +func (e *engine) CleanBuilds(ctx context.Context, msg string, before int64) (int64, error) { + logrus.Tracef("cleaning pending or running builds in the database created prior to %d", before) + + b := new(library.Build) + b.SetStatus(constants.StatusError) + b.SetError(msg) + b.SetFinished(time.Now().UTC().Unix()) + + build := database.BuildFromLibrary(b) + + // send query to the database + result := e.client. + Table(constants.TableBuild). + Where("created < ?", before). + Where("status = 'running' OR status = 'pending'"). + Updates(build) + + return result.RowsAffected, result.Error +} diff --git a/database/build/clean_test.go b/database/build/clean_test.go new file mode 100644 index 000000000..279268ff4 --- /dev/null +++ b/database/build/clean_test.go @@ -0,0 +1,120 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestBuild_Engine_CleanBuilds(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetCreated(1) + _buildOne.SetStatus("pending") + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(1) + _buildTwo.SetNumber(2) + _buildTwo.SetCreated(2) + _buildTwo.SetStatus("running") + + // setup types + _buildThree := testBuild() + _buildThree.SetID(3) + _buildThree.SetRepoID(1) + _buildThree.SetNumber(3) + _buildThree.SetCreated(1) + _buildThree.SetStatus("success") + + _buildFour := testBuild() + _buildFour.SetID(4) + _buildFour.SetRepoID(1) + _buildFour.SetNumber(4) + _buildFour.SetCreated(5) + _buildFour.SetStatus("running") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the name query + _mock.ExpectExec(`UPDATE "builds" SET "status"=$1,"error"=$2,"finished"=$3,"deploy_payload"=$4 WHERE created < $5 AND (status = 'running' OR status = 'pending')`). + WithArgs("error", "msg", NowTimestamp{}, AnyArgument{}, 3). + WillReturnResult(sqlmock.NewResult(1, 2)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildThree) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildFour) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CleanBuilds(context.TODO(), "msg", 3) + + if test.failure { + if err == nil { + t.Errorf("CleanBuilds for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CleanBuilds for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CleanBuilds for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/count.go b/database/build/count.go new file mode 100644 index 000000000..57c84030c --- /dev/null +++ b/database/build/count.go @@ -0,0 +1,27 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" +) + +// CountBuilds gets the count of all builds from the database. +func (e *engine) CountBuilds(ctx context.Context) (int64, error) { + e.logger.Tracef("getting count of all builds from the database") + + // variable to store query results + var b int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuild). + Count(&b). + Error + + return b, err +} diff --git a/database/build/count_deployment.go b/database/build/count_deployment.go new file mode 100644 index 000000000..21e58c6c8 --- /dev/null +++ b/database/build/count_deployment.go @@ -0,0 +1,34 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CountBuildsForDeployment gets the count of builds by deployment URL from the database. +func (e *engine) CountBuildsForDeployment(ctx context.Context, d *library.Deployment, filters map[string]interface{}) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "deployment": d.GetURL(), + }).Tracef("getting count of builds for deployment %s from the database", d.GetURL()) + + // variable to store query results + var b int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuild). + Where("source = ?", d.GetURL()). + Where(filters). + Order("number DESC"). + Count(&b). + Error + + return b, err +} diff --git a/database/build/count_deployment_test.go b/database/build/count_deployment_test.go new file mode 100644 index 000000000..4b067896d --- /dev/null +++ b/database/build/count_deployment_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestBuild_Engine_CountBuildsForDeployment(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetDeployPayload(nil) + _buildOne.SetSource("https://github.com/github/octocat/deployments/1") + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(1) + _buildTwo.SetNumber(2) + _buildTwo.SetDeployPayload(nil) + _buildTwo.SetSource("https://github.com/github/octocat/deployments/1") + + _deployment := testDeployment() + _deployment.SetID(1) + _deployment.SetRepoID(1) + _deployment.SetURL("https://github.com/github/octocat/deployments/1") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "builds" WHERE source = $1`).WithArgs("https://github.com/github/octocat/deployments/1").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountBuildsForDeployment(context.TODO(), _deployment, filters) + + if test.failure { + if err == nil { + t.Errorf("CountBuildsForDeployment for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountBuildsForDeployment for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountBuildsForDeployment for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/count_org.go b/database/build/count_org.go new file mode 100644 index 000000000..90e01c5e8 --- /dev/null +++ b/database/build/count_org.go @@ -0,0 +1,33 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" +) + +// CountBuildsForOrg gets the count of builds by org name from the database. +func (e *engine) CountBuildsForOrg(ctx context.Context, org string, filters map[string]interface{}) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": org, + }).Tracef("getting count of builds for org %s from the database", org) + + // variable to store query results + var b int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuild). + Joins("JOIN repos ON builds.repo_id = repos.id"). + Where("repos.org = ?", org). + Where(filters). + Count(&b). + Error + + return b, err +} diff --git a/database/build/count_org_test.go b/database/build/count_org_test.go new file mode 100644 index 000000000..77b60652b --- /dev/null +++ b/database/build/count_org_test.go @@ -0,0 +1,160 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" +) + +func TestBuild_Engine_CountBuildsForOrg(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetDeployPayload(nil) + _buildOne.SetEvent("push") + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(2) + _buildTwo.SetNumber(2) + _buildTwo.SetDeployPayload(nil) + _buildTwo.SetEvent("push") + + _repoOne := testRepo() + _repoOne.SetID(1) + _repoOne.SetUserID(1) + _repoOne.SetHash("baz") + _repoOne.SetOrg("foo") + _repoOne.SetName("bar") + _repoOne.SetFullName("foo/bar") + _repoOne.SetVisibility("public") + _repoOne.SetPipelineType("yaml") + _repoOne.SetTopics([]string{}) + + _repoTwo := testRepo() + _repoTwo.SetID(2) + _repoTwo.SetUserID(1) + _repoTwo.SetHash("bar") + _repoTwo.SetOrg("foo") + _repoTwo.SetName("baz") + _repoTwo.SetFullName("foo/baz") + _repoTwo.SetVisibility("public") + _repoTwo.SetPipelineType("yaml") + _repoTwo.SetTopics([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result without filters in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + // ensure the mock expects the query without filters + _mock.ExpectQuery(`SELECT count(*) FROM "builds" JOIN repos ON builds.repo_id = repos.id WHERE repos.org = $1`).WithArgs("foo").WillReturnRows(_rows) + + // create expected result with event filter in mock + _rows = sqlmock.NewRows([]string{"count"}).AddRow(2) + // ensure the mock expects the query with event filter + _mock.ExpectQuery(`SELECT count(*) FROM "builds" JOIN repos ON builds.repo_id = repos.id WHERE repos.org = $1 AND "event" = $2`).WithArgs("foo", "push").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + err = _sqlite.client.AutoMigrate(&database.Repo{}) + if err != nil { + t.Errorf("unable to create repo table for sqlite: %v", err) + } + + err = _sqlite.client.Table(constants.TableRepo).Create(database.RepoFromLibrary(_repoOne)).Error + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + err = _sqlite.client.Table(constants.TableRepo).Create(database.RepoFromLibrary(_repoTwo)).Error + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + filters map[string]interface{} + want int64 + }{ + { + failure: false, + name: "postgres without filters", + database: _postgres, + filters: map[string]interface{}{}, + want: 2, + }, + { + failure: false, + name: "postgres with event filter", + database: _postgres, + filters: map[string]interface{}{ + "event": "push", + }, + want: 2, + }, + { + failure: false, + name: "sqlite3 without filters", + database: _sqlite, + filters: map[string]interface{}{}, + want: 2, + }, + { + failure: false, + name: "sqlite3 with event filter", + database: _sqlite, + filters: map[string]interface{}{ + "event": "push", + }, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountBuildsForOrg(context.TODO(), "foo", test.filters) + + if test.failure { + if err == nil { + t.Errorf("CountBuildsForOrg for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountBuildsForOrg for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountBuildsForOrg for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/count_repo.go b/database/build/count_repo.go new file mode 100644 index 000000000..e7fcee5b8 --- /dev/null +++ b/database/build/count_repo.go @@ -0,0 +1,34 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CountBuildsForRepo gets the count of builds by repo ID from the database. +func (e *engine) CountBuildsForRepo(ctx context.Context, r *library.Repo, filters map[string]interface{}) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("getting count of builds for repo %s from the database", r.GetFullName()) + + // variable to store query results + var b int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuild). + Where("repo_id = ?", r.GetID()). + Where(filters). + Count(&b). + Error + + return b, err +} diff --git a/database/build/count_repo_test.go b/database/build/count_repo_test.go new file mode 100644 index 000000000..c7f406b32 --- /dev/null +++ b/database/build/count_repo_test.go @@ -0,0 +1,105 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestBuild_Engine_CountBuildsForRepo(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetDeployPayload(nil) + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(1) + _buildTwo.SetNumber(2) + _buildTwo.SetDeployPayload(nil) + + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "builds" WHERE repo_id = $1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountBuildsForRepo(context.TODO(), _repo, filters) + + if test.failure { + if err == nil { + t.Errorf("CountBuildsForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountBuildsForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountBuildsForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/count_status.go b/database/build/count_status.go new file mode 100644 index 000000000..98d4acf5b --- /dev/null +++ b/database/build/count_status.go @@ -0,0 +1,29 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" +) + +// CountBuildsForStatus gets the count of builds by status from the database. +func (e *engine) CountBuildsForStatus(ctx context.Context, status string, filters map[string]interface{}) (int64, error) { + e.logger.Tracef("getting count of builds for status %s from the database", status) + + // variable to store query results + var b int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuild). + Where("status = ?", status). + Where(filters). + Count(&b). + Error + + return b, err +} diff --git a/database/build/count_status_test.go b/database/build/count_status_test.go new file mode 100644 index 000000000..c35d694e0 --- /dev/null +++ b/database/build/count_status_test.go @@ -0,0 +1,98 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestBuild_Engine_CountBuildsForStatus(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetDeployPayload(nil) + _buildOne.SetStatus("running") + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(1) + _buildTwo.SetNumber(2) + _buildTwo.SetDeployPayload(nil) + _buildTwo.SetStatus("running") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "builds" WHERE status = $1`).WithArgs("running").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountBuildsForStatus(context.TODO(), "running", filters) + + if test.failure { + if err == nil { + t.Errorf("CountBuildsForStatus for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountBuildsForStatus for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountBuildsForStatus for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/count_test.go b/database/build/count_test.go new file mode 100644 index 000000000..c29d3f04f --- /dev/null +++ b/database/build/count_test.go @@ -0,0 +1,94 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestBuild_Engine_CountBuilds(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetDeployPayload(nil) + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(1) + _buildTwo.SetNumber(2) + _buildTwo.SetDeployPayload(nil) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "builds"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountBuilds(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("CountBuilds for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountBuilds for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountBuilds for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/create.go b/database/build/create.go new file mode 100644 index 000000000..ff5d7a91d --- /dev/null +++ b/database/build/create.go @@ -0,0 +1,43 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with update.go +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreateBuild creates a new build in the database. +func (e *engine) CreateBuild(ctx context.Context, b *library.Build) (*library.Build, error) { + e.logger.WithFields(logrus.Fields{ + "build": b.GetNumber(), + }).Tracef("creating build %d in the database", b.GetNumber()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#BuildFromLibrary + build := database.BuildFromLibrary(b) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Build.Validate + err := build.Validate() + if err != nil { + return nil, err + } + + // crop build if any columns are too large + build = build.Crop() + + // send query to the database + result := e.client.Table(constants.TableBuild).Create(build) + + return build.ToLibrary(), result.Error +} diff --git a/database/build/create_test.go b/database/build/create_test.go new file mode 100644 index 000000000..c425c5db7 --- /dev/null +++ b/database/build/create_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestBuild_Engine_CreateBuild(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + _build.SetDeployPayload(nil) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`INSERT INTO "builds" +("repo_id","pipeline_id","number","parent","event","event_action","status","error","enqueued","created","started","finished","deploy","deploy_payload","clone","source","title","message","commit","sender","author","email","link","branch","ref","base_ref","head_ref","host","runtime","distribution","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31) RETURNING "id"`). + WithArgs(1, nil, 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, AnyArgument{}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CreateBuild(context.TODO(), _build) + + if test.failure { + if err == nil { + t.Errorf("CreateBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateBuild for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _build) { + t.Errorf("CreateBuild for %s returned %s, want %s", test.name, got, _build) + } + }) + } +} diff --git a/database/build/delete.go b/database/build/delete.go new file mode 100644 index 000000000..a7048c7e7 --- /dev/null +++ b/database/build/delete.go @@ -0,0 +1,32 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// DeleteBuild deletes an existing build from the database. +func (e *engine) DeleteBuild(ctx context.Context, b *library.Build) error { + e.logger.WithFields(logrus.Fields{ + "build": b.GetNumber(), + }).Tracef("deleting build %d from the database", b.GetNumber()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#BuildFromLibrary + build := database.BuildFromLibrary(b) + + // send query to the database + return e.client. + Table(constants.TableBuild). + Delete(build). + Error +} diff --git a/database/build/delete_test.go b/database/build/delete_test.go new file mode 100644 index 000000000..93d1b5459 --- /dev/null +++ b/database/build/delete_test.go @@ -0,0 +1,74 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestBuild_Engine_DeleteBuild(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + _build.SetDeployPayload(nil) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`DELETE FROM "builds" WHERE "builds"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _build) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeleteBuild(context.TODO(), _build) + + if test.failure { + if err == nil { + t.Errorf("DeleteBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeleteBuild for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/build/get.go b/database/build/get.go new file mode 100644 index 000000000..8c57d7fe6 --- /dev/null +++ b/database/build/get.go @@ -0,0 +1,33 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetBuild gets a build by ID from the database. +func (e *engine) GetBuild(ctx context.Context, id int64) (*library.Build, error) { + e.logger.Tracef("getting build %d from the database", id) + + // variable to store query results + b := new(database.Build) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuild). + Where("id = ?", id). + Take(b). + Error + if err != nil { + return nil, err + } + + return b.ToLibrary(), nil +} diff --git a/database/build/get_repo.go b/database/build/get_repo.go new file mode 100644 index 000000000..7b37bc2a7 --- /dev/null +++ b/database/build/get_repo.go @@ -0,0 +1,39 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetBuildForRepo gets a build by repo ID and number from the database. +func (e *engine) GetBuildForRepo(ctx context.Context, r *library.Repo, number int) (*library.Build, error) { + e.logger.WithFields(logrus.Fields{ + "build": number, + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("getting build %s/%d from the database", r.GetFullName(), number) + + // variable to store query results + b := new(database.Build) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuild). + Where("repo_id = ?", r.GetID()). + Where("number = ?", number). + Take(b). + Error + if err != nil { + return nil, err + } + + return b.ToLibrary(), nil +} diff --git a/database/build/get_repo_test.go b/database/build/get_repo_test.go new file mode 100644 index 000000000..8a36b55e9 --- /dev/null +++ b/database/build/get_repo_test.go @@ -0,0 +1,95 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestBuild_Engine_GetBuildForRepo(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + _build.SetDeployPayload(nil) + + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "pipeline_id", "number", "parent", "event", "event_action", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}). + AddRow(1, 1, nil, 1, 0, "", "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "builds" WHERE repo_id = $1 AND number = $2 LIMIT 1`).WithArgs(1, 1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _build) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Build + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _build, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _build, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetBuildForRepo(context.TODO(), _repo, 1) + + if test.failure { + if err == nil { + t.Errorf("GetBuildForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetBuildForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetBuildForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/get_test.go b/database/build/get_test.go new file mode 100644 index 000000000..9646224b7 --- /dev/null +++ b/database/build/get_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestBuild_Engine_GetBuild(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + _build.SetDeployPayload(nil) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "pipeline_id", "number", "parent", "event", "event_action", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}). + AddRow(1, 1, nil, 1, 0, "", "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "builds" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _build) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Build + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _build, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _build, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetBuild(context.TODO(), 1) + + if test.failure { + if err == nil { + t.Errorf("GetBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetBuild for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetBuild for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/index.go b/database/build/index.go new file mode 100644 index 000000000..51dec6eba --- /dev/null +++ b/database/build/index.go @@ -0,0 +1,71 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import "context" + +const ( + // CreateCreatedIndex represents a query to create an + // index on the builds table for the created column. + CreateCreatedIndex = ` +CREATE INDEX +IF NOT EXISTS +builds_created +ON builds (created); +` + + // CreateRepoIDIndex represents a query to create an + // index on the builds table for the repo_id column. + CreateRepoIDIndex = ` +CREATE INDEX +IF NOT EXISTS +builds_repo_id +ON builds (repo_id); +` + + // CreateSourceIndex represents a query to create an + // index on the builds table for the source column. + CreateSourceIndex = ` +CREATE INDEX +IF NOT EXISTS +builds_source +ON builds (source); +` + + // CreateStatusIndex represents a query to create an + // index on the builds table for the status column. + CreateStatusIndex = ` +CREATE INDEX +IF NOT EXISTS +builds_status +ON builds (status); +` +) + +// CreateBuildIndexes creates the indexes for the builds table in the database. +func (e *engine) CreateBuildIndexes(ctx context.Context) error { + e.logger.Tracef("creating indexes for builds table in the database") + + // create the created column index for the builds table + err := e.client.Exec(CreateCreatedIndex).Error + if err != nil { + return err + } + + // create the repo_id column index for the builds table + err = e.client.Exec(CreateRepoIDIndex).Error + if err != nil { + return err + } + + // create the source column index for the builds table + err = e.client.Exec(CreateSourceIndex).Error + if err != nil { + return err + } + + // create the status column index for the builds table + return e.client.Exec(CreateStatusIndex).Error +} diff --git a/database/build/index_test.go b/database/build/index_test.go new file mode 100644 index 000000000..35dd25008 --- /dev/null +++ b/database/build/index_test.go @@ -0,0 +1,63 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestBuild_Engine_CreateBuildIndexes(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreateCreatedIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateSourceIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateStatusIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateBuildIndexes(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("CreateBuildIndexes for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateBuildIndexes for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/build/interface.go b/database/build/interface.go new file mode 100644 index 000000000..d2b8d9f41 --- /dev/null +++ b/database/build/interface.go @@ -0,0 +1,65 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/library" +) + +// BuildInterface represents the Vela interface for build +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type BuildInterface interface { + // Build Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreateBuildIndexes defines a function that creates the indexes for the builds table. + CreateBuildIndexes(context.Context) error + // CreateBuildTable defines a function that creates the builds table. + CreateBuildTable(context.Context, string) error + + // Build Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CleanBuilds defines a function that sets pending or running builds to error created before a given time. + CleanBuilds(context.Context, string, int64) (int64, error) + // CountBuilds defines a function that gets the count of all builds. + CountBuilds(context.Context) (int64, error) + // CountBuildsForDeployment defines a function that gets the count of builds by deployment url. + CountBuildsForDeployment(context.Context, *library.Deployment, map[string]interface{}) (int64, error) + // CountBuildsForOrg defines a function that gets the count of builds by org name. + CountBuildsForOrg(context.Context, string, map[string]interface{}) (int64, error) + // CountBuildsForRepo defines a function that gets the count of builds by repo ID. + CountBuildsForRepo(context.Context, *library.Repo, map[string]interface{}) (int64, error) + // CountBuildsForStatus defines a function that gets the count of builds by status. + CountBuildsForStatus(context.Context, string, map[string]interface{}) (int64, error) + // CreateBuild defines a function that creates a new build. + CreateBuild(context.Context, *library.Build) (*library.Build, error) + // DeleteBuild defines a function that deletes an existing build. + DeleteBuild(context.Context, *library.Build) error + // GetBuild defines a function that gets a build by ID. + GetBuild(context.Context, int64) (*library.Build, error) + // GetBuildForRepo defines a function that gets a build by repo ID and number. + GetBuildForRepo(context.Context, *library.Repo, int) (*library.Build, error) + // LastBuildForRepo defines a function that gets the last build ran by repo ID and branch. + LastBuildForRepo(context.Context, *library.Repo, string) (*library.Build, error) + // ListBuilds defines a function that gets a list of all builds. + ListBuilds(context.Context) ([]*library.Build, error) + // ListBuildsForDeployment defines a function that gets a list of builds by deployment url. + ListBuildsForDeployment(context.Context, *library.Deployment, map[string]interface{}, int, int) ([]*library.Build, int64, error) + // ListBuildsForOrg defines a function that gets a list of builds by org name. + ListBuildsForOrg(context.Context, string, map[string]interface{}, int, int) ([]*library.Build, int64, error) + // ListBuildsForRepo defines a function that gets a list of builds by repo ID. + ListBuildsForRepo(context.Context, *library.Repo, map[string]interface{}, int64, int64, int, int) ([]*library.Build, int64, error) + // ListPendingAndRunningBuilds defines a function that gets a list of pending and running builds. + ListPendingAndRunningBuilds(context.Context, string) ([]*library.BuildQueue, error) + // UpdateBuild defines a function that updates an existing build. + UpdateBuild(context.Context, *library.Build) (*library.Build, error) +} diff --git a/database/build/last_repo.go b/database/build/last_repo.go new file mode 100644 index 000000000..81861c8b4 --- /dev/null +++ b/database/build/last_repo.go @@ -0,0 +1,48 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "errors" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// LastBuildForRepo gets the last build by repo ID and branch from the database. +func (e *engine) LastBuildForRepo(ctx context.Context, r *library.Repo, branch string) (*library.Build, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("getting last build for repo %s from the database", r.GetFullName()) + + // variable to store query results + b := new(database.Build) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuild). + Where("repo_id = ?", r.GetID()). + Where("branch = ?", branch). + Order("number DESC"). + Take(b). + Error + if err != nil { + // check if the query returned a record not found error + if errors.Is(err, gorm.ErrRecordNotFound) { + // the record will not exist if it is a new repo + return nil, nil + } + + return nil, err + } + + return b.ToLibrary(), nil +} diff --git a/database/build/last_repo_test.go b/database/build/last_repo_test.go new file mode 100644 index 000000000..07e5549b2 --- /dev/null +++ b/database/build/last_repo_test.go @@ -0,0 +1,96 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestBuild_Engine_LastBuildForRepo(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + _build.SetDeployPayload(nil) + _build.SetBranch("master") + + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "pipeline_id", "number", "parent", "event", "event_action", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}). + AddRow(1, 1, nil, 1, 0, "", "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "master", "", "", "", "", "", "", 0) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "builds" WHERE repo_id = $1 AND branch = $2 ORDER BY number DESC LIMIT 1`).WithArgs(1, "master").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _build) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Build + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _build, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _build, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.LastBuildForRepo(context.TODO(), _repo, "master") + + if test.failure { + if err == nil { + t.Errorf("LastBuildForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("LastBuildForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("LastBuildForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/list.go b/database/build/list.go new file mode 100644 index 000000000..9809af5d9 --- /dev/null +++ b/database/build/list.go @@ -0,0 +1,56 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListBuilds gets a list of all builds from the database. +func (e *engine) ListBuilds(ctx context.Context) ([]*library.Build, error) { + e.logger.Trace("listing all builds from the database") + + // variables to store query results and return value + count := int64(0) + b := new([]database.Build) + builds := []*library.Build{} + + // count the results + count, err := e.CountBuilds(ctx) + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return builds, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableBuild). + Find(&b). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, build := range *b { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := build + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Build.ToLibrary + builds = append(builds, tmp.ToLibrary()) + } + + return builds, nil +} diff --git a/database/build/list_deployment.go b/database/build/list_deployment.go new file mode 100644 index 000000000..0d931fae7 --- /dev/null +++ b/database/build/list_deployment.go @@ -0,0 +1,68 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListBuildsForDeployment gets a list of builds by deployment url from the database. +// +//nolint:lll // ignore long line length due to variable names +func (e *engine) ListBuildsForDeployment(ctx context.Context, d *library.Deployment, filters map[string]interface{}, page, perPage int) ([]*library.Build, int64, error) { + e.logger.WithFields(logrus.Fields{ + "deployment": d.GetURL(), + }).Tracef("listing builds for deployment %s from the database", d.GetURL()) + + // variables to store query results and return values + count := int64(0) + b := new([]database.Build) + builds := []*library.Build{} + + // count the results + count, err := e.CountBuildsForDeployment(context.TODO(), d, filters) + if err != nil { + return builds, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return builds, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + err = e.client. + Table(constants.TableBuild). + Where("source = ?", d.GetURL()). + Where(filters). + Order("number DESC"). + Limit(perPage). + Offset(offset). + Find(&b). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, build := range *b { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := build + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Build.ToLibrary + builds = append(builds, tmp.ToLibrary()) + } + + return builds, count, nil +} diff --git a/database/build/list_deployment_test.go b/database/build/list_deployment_test.go new file mode 100644 index 000000000..57a54522d --- /dev/null +++ b/database/build/list_deployment_test.go @@ -0,0 +1,113 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestBuild_Engine_ListBuildsForDeployment(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetDeployPayload(nil) + _buildOne.SetSource("https://github.com/github/octocat/deployments/1") + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(1) + _buildTwo.SetNumber(2) + _buildTwo.SetDeployPayload(nil) + _buildTwo.SetSource("https://github.com/github/octocat/deployments/1") + + _deployment := testDeployment() + _deployment.SetID(1) + _deployment.SetRepoID(1) + _deployment.SetURL("https://github.com/github/octocat/deployments/1") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected count query result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the count query + _mock.ExpectQuery(`SELECT count(*) FROM "builds" WHERE source = $1`).WithArgs("https://github.com/github/octocat/deployments/1").WillReturnRows(_rows) + + // create expected query result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "pipeline_id", "number", "parent", "event", "event_action", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}). + AddRow(2, 1, nil, 2, 0, "", "", "", "", 0, 0, 0, 0, "", nil, "", "https://github.com/github/octocat/deployments/1", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). + AddRow(1, 1, nil, 1, 0, "", "", "", "", 0, 0, 0, 0, "", nil, "", "https://github.com/github/octocat/deployments/1", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "builds" WHERE source = $1 ORDER BY number DESC LIMIT 10`).WithArgs("https://github.com/github/octocat/deployments/1").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Build + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Build{_buildTwo, _buildOne}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Build{_buildTwo, _buildOne}, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListBuildsForDeployment(context.TODO(), _deployment, filters, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListBuildsForDeployment for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListBuildsForDeployment for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListBuildsForDeployment for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/list_org.go b/database/build/list_org.go new file mode 100644 index 000000000..254c0c9e9 --- /dev/null +++ b/database/build/list_org.go @@ -0,0 +1,71 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListBuildsForOrg gets a list of builds by org name from the database. +// +//nolint:lll // ignore long line length due to variable names +func (e *engine) ListBuildsForOrg(ctx context.Context, org string, filters map[string]interface{}, page, perPage int) ([]*library.Build, int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": org, + }).Tracef("listing builds for org %s from the database", org) + + // variables to store query results and return values + count := int64(0) + b := new([]database.Build) + builds := []*library.Build{} + + // count the results + count, err := e.CountBuildsForOrg(ctx, org, filters) + if err != nil { + return builds, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return builds, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + err = e.client. + Table(constants.TableBuild). + Select("builds.*"). + Joins("JOIN repos ON builds.repo_id = repos.id"). + Where("repos.org = ?", org). + Where(filters). + Order("created DESC"). + Order("id"). + Limit(perPage). + Offset(offset). + Find(&b). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, build := range *b { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := build + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Build.ToLibrary + builds = append(builds, tmp.ToLibrary()) + } + + return builds, count, nil +} diff --git a/database/build/list_org_test.go b/database/build/list_org_test.go new file mode 100644 index 000000000..95fc9a456 --- /dev/null +++ b/database/build/list_org_test.go @@ -0,0 +1,205 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +func TestBuild_Engine_ListBuildsForOrg(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetDeployPayload(nil) + _buildOne.SetEvent("push") + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(2) + _buildTwo.SetNumber(2) + _buildTwo.SetDeployPayload(nil) + _buildTwo.SetEvent("push") + + _repoOne := testRepo() + _repoOne.SetID(1) + _repoOne.SetUserID(1) + _repoOne.SetHash("baz") + _repoOne.SetOrg("foo") + _repoOne.SetName("bar") + _repoOne.SetFullName("foo/bar") + _repoOne.SetVisibility("public") + _repoOne.SetPipelineType("yaml") + _repoOne.SetTopics([]string{}) + + _repoTwo := testRepo() + _repoTwo.SetID(2) + _repoTwo.SetUserID(1) + _repoTwo.SetHash("bar") + _repoTwo.SetOrg("foo") + _repoTwo.SetName("baz") + _repoTwo.SetFullName("foo/baz") + _repoTwo.SetVisibility("public") + _repoTwo.SetPipelineType("yaml") + _repoTwo.SetTopics([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected count query without filters result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + // ensure the mock expects the count query without filters + _mock.ExpectQuery(`SELECT count(*) FROM "builds" JOIN repos ON builds.repo_id = repos.id WHERE repos.org = $1`).WithArgs("foo").WillReturnRows(_rows) + // create expected query without filters result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "pipeline_id", "number", "parent", "event", "event_action", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}). + AddRow(1, 1, nil, 1, 0, "push", "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). + AddRow(2, 2, nil, 2, 0, "push", "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) + // ensure the mock expects the query without filters + _mock.ExpectQuery(`SELECT builds.* FROM "builds" JOIN repos ON builds.repo_id = repos.id WHERE repos.org = $1 ORDER BY created DESC,id LIMIT 10`).WithArgs("foo").WillReturnRows(_rows) + + // create expected count query with event filter result in mock + _rows = sqlmock.NewRows([]string{"count"}).AddRow(2) + // ensure the mock expects the count query with event filter + _mock.ExpectQuery(`SELECT count(*) FROM "builds" JOIN repos ON builds.repo_id = repos.id WHERE repos.org = $1 AND "event" = $2`).WithArgs("foo", "push").WillReturnRows(_rows) + // create expected query with event filter result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "pipeline_id", "number", "parent", "event", "event_action", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}). + AddRow(1, 1, nil, 1, 0, "push", "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). + AddRow(2, 2, nil, 2, 0, "push", "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) + // ensure the mock expects the query with event filter + _mock.ExpectQuery(`SELECT builds.* FROM "builds" JOIN repos ON builds.repo_id = repos.id WHERE repos.org = $1 AND "event" = $2 ORDER BY created DESC,id LIMIT 10`).WithArgs("foo", "push").WillReturnRows(_rows) + + // create expected count query with visibility filter result in mock + _rows = sqlmock.NewRows([]string{"count"}).AddRow(2) + // ensure the mock expects the count query with visibility filter + _mock.ExpectQuery(`SELECT count(*) FROM "builds" JOIN repos ON builds.repo_id = repos.id WHERE repos.org = $1 AND "visibility" = $2`).WithArgs("foo", "public").WillReturnRows(_rows) + // create expected query with visibility filter result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "pipeline_id", "number", "parent", "event", "event_action", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}). + AddRow(1, 1, nil, 1, 0, "push", "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). + AddRow(2, 2, nil, 2, 0, "push", "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) + // ensure the mock expects the query with visibility filter + _mock.ExpectQuery(`SELECT builds.* FROM "builds" JOIN repos ON builds.repo_id = repos.id WHERE repos.org = $1 AND "visibility" = $2 ORDER BY created DESC,id LIMIT 10`).WithArgs("foo", "public").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + err = _sqlite.client.AutoMigrate(&database.Repo{}) + if err != nil { + t.Errorf("unable to create repo table for sqlite: %v", err) + } + + err = _sqlite.client.Table(constants.TableRepo).Create(database.RepoFromLibrary(_repoOne)).Error + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + err = _sqlite.client.Table(constants.TableRepo).Create(database.RepoFromLibrary(_repoTwo)).Error + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + filters map[string]interface{} + want []*library.Build + }{ + { + failure: false, + name: "postgres without filters", + database: _postgres, + filters: map[string]interface{}{}, + want: []*library.Build{_buildOne, _buildTwo}, + }, + { + failure: false, + name: "postgres with event filter", + database: _postgres, + filters: map[string]interface{}{ + "event": "push", + }, + want: []*library.Build{_buildOne, _buildTwo}, + }, + { + failure: false, + name: "postgres with visibility filter", + database: _postgres, + filters: map[string]interface{}{ + "visibility": "public", + }, + want: []*library.Build{_buildOne, _buildTwo}, + }, + { + failure: false, + name: "sqlite3 without filters", + database: _sqlite, + filters: map[string]interface{}{}, + want: []*library.Build{_buildOne, _buildTwo}, + }, + { + failure: false, + name: "sqlite3 with event filter", + database: _sqlite, + filters: map[string]interface{}{ + "event": "push", + }, + want: []*library.Build{_buildOne, _buildTwo}, + }, + { + failure: false, + name: "sqlite3 with visibility filter", + database: _sqlite, + filters: map[string]interface{}{ + "visibility": "public", + }, + want: []*library.Build{_buildOne, _buildTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListBuildsForOrg(context.TODO(), "foo", test.filters, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListBuildsForOrg for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListBuildsForOrg for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListBuildsForOrg for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/list_pending_running.go b/database/build/list_pending_running.go new file mode 100644 index 000000000..dbee3e704 --- /dev/null +++ b/database/build/list_pending_running.go @@ -0,0 +1,48 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListPendingAndRunningBuilds gets a list of all pending and running builds in the provided timeframe from the database. +func (e *engine) ListPendingAndRunningBuilds(ctx context.Context, after string) ([]*library.BuildQueue, error) { + e.logger.Trace("listing all pending and running builds from the database") + + // variables to store query results and return value + b := new([]database.BuildQueue) + builds := []*library.BuildQueue{} + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuild). + Select("builds.created, builds.number, builds.status, repos.full_name"). + InnerJoins("INNER JOIN repos ON builds.repo_id = repos.id"). + Where("builds.created > ?", after). + Where("builds.status = 'running' OR builds.status = 'pending'"). + Find(&b). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, build := range *b { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := build + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Build.ToLibrary + builds = append(builds, tmp.ToLibrary()) + } + + return builds, nil +} diff --git a/database/build/list_pending_running_test.go b/database/build/list_pending_running_test.go new file mode 100644 index 000000000..4ee71e1c7 --- /dev/null +++ b/database/build/list_pending_running_test.go @@ -0,0 +1,132 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +func TestBuild_Engine_ListPendingAndRunningBuilds(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetStatus("running") + _buildOne.SetCreated(1) + _buildOne.SetDeployPayload(nil) + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(1) + _buildTwo.SetNumber(2) + _buildTwo.SetStatus("pending") + _buildTwo.SetCreated(1) + _buildTwo.SetDeployPayload(nil) + + _queueOne := new(library.BuildQueue) + _queueOne.SetCreated(1) + _queueOne.SetFullName("foo/bar") + _queueOne.SetNumber(1) + _queueOne.SetStatus("running") + + _queueTwo := new(library.BuildQueue) + _queueTwo.SetCreated(1) + _queueTwo.SetFullName("foo/bar") + _queueTwo.SetNumber(2) + _queueTwo.SetStatus("pending") + + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected name query result in mock + _rows := sqlmock.NewRows([]string{"created", "full_name", "number", "status"}).AddRow(1, "foo/bar", 2, "pending").AddRow(1, "foo/bar", 1, "running") + + // ensure the mock expects the name query + _mock.ExpectQuery(`SELECT builds.created, builds.number, builds.status, repos.full_name FROM "builds" INNER JOIN repos ON builds.repo_id = repos.id WHERE builds.created > $1 AND (builds.status = 'running' OR builds.status = 'pending')`).WithArgs("0").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + err = _sqlite.client.AutoMigrate(&database.Repo{}) + if err != nil { + t.Errorf("unable to create repo table for sqlite: %v", err) + } + + err = _sqlite.client.Table(constants.TableRepo).Create(database.RepoFromLibrary(_repo)).Error + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.BuildQueue + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.BuildQueue{_queueTwo, _queueOne}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.BuildQueue{_queueTwo, _queueOne}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListPendingAndRunningBuilds(context.TODO(), "0") + + if test.failure { + if err == nil { + t.Errorf("ListPendingAndRunningBuilds for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListPendingAndRunningBuilds for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListPendingAndRunningBuilds for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/list_repo.go b/database/build/list_repo.go new file mode 100644 index 000000000..8ee78e35c --- /dev/null +++ b/database/build/list_repo.go @@ -0,0 +1,71 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListBuildsForRepo gets a list of builds by repo ID from the database. +// +//nolint:lll // ignore long line length due to variable names +func (e *engine) ListBuildsForRepo(ctx context.Context, r *library.Repo, filters map[string]interface{}, before, after int64, page, perPage int) ([]*library.Build, int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("listing builds for repo %s from the database", r.GetFullName()) + + // variables to store query results and return values + count := int64(0) + b := new([]database.Build) + builds := []*library.Build{} + + // count the results + count, err := e.CountBuildsForRepo(ctx, r, filters) + if err != nil { + return builds, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return builds, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + err = e.client. + Table(constants.TableBuild). + Where("repo_id = ?", r.GetID()). + Where("created < ?", before). + Where("created > ?", after). + Where(filters). + Order("number DESC"). + Limit(perPage). + Offset(offset). + Find(&b). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, build := range *b { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := build + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Build.ToLibrary + builds = append(builds, tmp.ToLibrary()) + } + + return builds, count, nil +} diff --git a/database/build/list_repo_test.go b/database/build/list_repo_test.go new file mode 100644 index 000000000..f16ed714d --- /dev/null +++ b/database/build/list_repo_test.go @@ -0,0 +1,118 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestBuild_Engine_ListBuildsForRepo(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetDeployPayload(nil) + _buildOne.SetCreated(1) + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(1) + _buildTwo.SetNumber(2) + _buildTwo.SetDeployPayload(nil) + _buildTwo.SetCreated(2) + + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected count query result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the count query + _mock.ExpectQuery(`SELECT count(*) FROM "builds" WHERE repo_id = $1`).WithArgs(1).WillReturnRows(_rows) + + // create expected query result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "pipeline_id", "number", "parent", "event", "event_action", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}). + AddRow(2, 1, nil, 2, 0, "", "", "", "", 0, 2, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). + AddRow(1, 1, nil, 1, 0, "", "", "", "", 0, 1, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "builds" WHERE repo_id = $1 AND created < $2 AND created > $3 ORDER BY number DESC LIMIT 10`).WithArgs(1, AnyArgument{}, 0).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Build + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Build{_buildTwo, _buildOne}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Build{_buildTwo, _buildOne}, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListBuildsForRepo(context.TODO(), _repo, filters, time.Now().UTC().Unix(), 0, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListBuildsForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListBuildsForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListBuildsForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/list_test.go b/database/build/list_test.go new file mode 100644 index 000000000..69d826755 --- /dev/null +++ b/database/build/list_test.go @@ -0,0 +1,104 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestBuild_Engine_ListBuilds(t *testing.T) { + // setup types + _buildOne := testBuild() + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetDeployPayload(nil) + + _buildTwo := testBuild() + _buildTwo.SetID(2) + _buildTwo.SetRepoID(1) + _buildTwo.SetNumber(2) + _buildTwo.SetDeployPayload(nil) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "builds"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "pipeline_id", "number", "parent", "event", "event_action", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}). + AddRow(1, 1, nil, 1, 0, "", "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). + AddRow(2, 1, nil, 2, 0, "", "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "builds"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _buildOne) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + _, err = _sqlite.CreateBuild(context.TODO(), _buildTwo) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Build + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Build{_buildOne, _buildTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Build{_buildOne, _buildTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListBuilds(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("ListBuilds for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListBuilds for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListBuilds for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/build/opts.go b/database/build/opts.go new file mode 100644 index 000000000..69c912435 --- /dev/null +++ b/database/build/opts.go @@ -0,0 +1,55 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Builds. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Builds. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the build engine + e.client = client + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Builds. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the build engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Builds. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the build engine + e.config.SkipCreation = skipCreation + + return nil + } +} + +// WithContext sets the context in the database engine for Builds. +func WithContext(ctx context.Context) EngineOpt { + return func(e *engine) error { + e.ctx = ctx + + return nil + } +} diff --git a/database/build/opts_test.go b/database/build/opts_test.go new file mode 100644 index 000000000..69afe0b4a --- /dev/null +++ b/database/build/opts_test.go @@ -0,0 +1,211 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestBuild_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestBuild_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestBuild_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} + +func TestBuild_EngineOpt_WithContext(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + ctx context.Context + want context.Context + }{ + { + failure: false, + name: "context set to TODO", + ctx: context.TODO(), + want: context.TODO(), + }, + { + failure: false, + name: "context set to nil", + ctx: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithContext(test.ctx)(e) + + if test.failure { + if err == nil { + t.Errorf("WithContext for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithContext returned err: %v", err) + } + + if !reflect.DeepEqual(e.ctx, test.want) { + t.Errorf("WithContext is %v, want %v", e.ctx, test.want) + } + }) + } +} diff --git a/database/build/table.go b/database/build/table.go new file mode 100644 index 000000000..91499ef4a --- /dev/null +++ b/database/build/table.go @@ -0,0 +1,112 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres builds table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +builds ( + id SERIAL PRIMARY KEY, + repo_id INTEGER, + pipeline_id INTEGER, + number INTEGER, + parent INTEGER, + event VARCHAR(250), + event_action VARCHAR(250), + status VARCHAR(250), + error VARCHAR(1000), + enqueued INTEGER, + created INTEGER, + started INTEGER, + finished INTEGER, + deploy VARCHAR(500), + deploy_payload VARCHAR(2000), + clone VARCHAR(1000), + source VARCHAR(1000), + title VARCHAR(1000), + message VARCHAR(2000), + commit VARCHAR(500), + sender VARCHAR(250), + author VARCHAR(250), + email VARCHAR(500), + link VARCHAR(1000), + branch VARCHAR(500), + ref VARCHAR(500), + base_ref VARCHAR(500), + head_ref VARCHAR(500), + host VARCHAR(250), + runtime VARCHAR(250), + distribution VARCHAR(250), + timestamp INTEGER, + UNIQUE(repo_id, number) +); +` + + // CreateSqliteTable represents a query to create the Sqlite builds table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +builds ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_id INTEGER, + pipeline_id INTEGER, + number INTEGER, + parent INTEGER, + event TEXT, + event_action TEXT, + status TEXT, + error TEXT, + enqueued INTEGER, + created INTEGER, + started INTEGER, + finished INTEGER, + deploy TEXT, + deploy_payload TEXT, + clone TEXT, + source TEXT, + title TEXT, + message TEXT, + 'commit' TEXT, + sender TEXT, + author TEXT, + email TEXT, + link TEXT, + branch TEXT, + ref TEXT, + base_ref TEXT, + head_ref TEXT, + host TEXT, + runtime TEXT, + distribution TEXT, + timestamp INTEGER, + UNIQUE(repo_id, number) +); +` +) + +// CreateBuildTable creates the builds table in the database. +func (e *engine) CreateBuildTable(ctx context.Context, driver string) error { + e.logger.Tracef("creating builds table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the builds table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the builds table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/build/table_test.go b/database/build/table_test.go new file mode 100644 index 000000000..a08997fce --- /dev/null +++ b/database/build/table_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestBuild_Engine_CreateBuildTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateBuildTable(context.TODO(), test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateBuildTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateBuildTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/build/update.go b/database/build/update.go new file mode 100644 index 000000000..bdd506187 --- /dev/null +++ b/database/build/update.go @@ -0,0 +1,43 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with create.go +package build + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// UpdateBuild updates an existing build in the database. +func (e *engine) UpdateBuild(ctx context.Context, b *library.Build) (*library.Build, error) { + e.logger.WithFields(logrus.Fields{ + "build": b.GetNumber(), + }).Tracef("updating build %d in the database", b.GetNumber()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#BuildFromLibrary + build := database.BuildFromLibrary(b) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Build.Validate + err := build.Validate() + if err != nil { + return nil, err + } + + // crop build if any columns are too large + build = build.Crop() + + // send query to the database + result := e.client.Table(constants.TableBuild).Save(build) + + return build.ToLibrary(), result.Error +} diff --git a/database/build/update_test.go b/database/build/update_test.go new file mode 100644 index 000000000..7e5441145 --- /dev/null +++ b/database/build/update_test.go @@ -0,0 +1,81 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package build + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestBuild_Engine_UpdateBuild(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + _build.SetDeployPayload(nil) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`UPDATE "builds" +SET "repo_id"=$1,"pipeline_id"=$2,"number"=$3,"parent"=$4,"event"=$5,"event_action"=$6,"status"=$7,"error"=$8,"enqueued"=$9,"created"=$10,"started"=$11,"finished"=$12,"deploy"=$13,"deploy_payload"=$14,"clone"=$15,"source"=$16,"title"=$17,"message"=$18,"commit"=$19,"sender"=$20,"author"=$21,"email"=$22,"link"=$23,"branch"=$24,"ref"=$25,"base_ref"=$26,"head_ref"=$27,"host"=$28,"runtime"=$29,"distribution"=$30 +WHERE "id" = $31`). + WithArgs(1, nil, 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, AnyArgument{}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateBuild(context.TODO(), _build) + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.UpdateBuild(context.TODO(), _build) + + if test.failure { + if err == nil { + t.Errorf("UpdateBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateBuild for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _build) { + t.Errorf("UpdateBuild for %s returned %s, want %s", test.name, got, _build) + } + }) + } +} diff --git a/database/close.go b/database/close.go new file mode 100644 index 000000000..0aff8e51d --- /dev/null +++ b/database/close.go @@ -0,0 +1,18 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +// Close stops and terminates the connection to the database. +func (e *engine) Close() error { + e.logger.Tracef("closing connection to the %s database", e.Driver()) + + // capture database/sql database from gorm.io/gorm database + _sql, err := e.client.DB() + if err != nil { + return err + } + + return _sql.Close() +} diff --git a/database/close_test.go b/database/close_test.go new file mode 100644 index 000000000..fe0c55b00 --- /dev/null +++ b/database/close_test.go @@ -0,0 +1,83 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +import ( + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestDatabase_Engine_Close(t *testing.T) { + _postgres, _mock := testPostgres(t) + defer _postgres.Close() + // ensure the mock expects the close + _mock.ExpectClose() + + // create a test database without mocking the call + _unmocked, _ := testPostgres(t) + + _sqlite := testSqlite(t) + defer _sqlite.Close() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + name: "success with postgres", + failure: false, + database: _postgres, + }, + { + name: "success with sqlite3", + failure: false, + database: _sqlite, + }, + { + name: "failure without mocked call", + failure: true, + database: _unmocked, + }, + { + name: "failure with invalid gorm database", + failure: true, + database: &engine{ + config: &config{ + Driver: "invalid", + }, + client: &gorm.DB{ + Config: &gorm.Config{ + ConnPool: nil, + }, + }, + logger: logrus.NewEntry(logrus.StandardLogger()), + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.Close() + + if test.failure { + if err == nil { + t.Errorf("Close for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("Close for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/context.go b/database/context.go index 7e315511c..fd08cc371 100644 --- a/database/context.go +++ b/database/context.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -6,6 +6,9 @@ package database import ( "context" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" ) const key = "database" @@ -15,14 +18,14 @@ type Setter interface { Set(string, interface{}) } -// FromContext returns the database Service associated with this context. -func FromContext(c context.Context) Service { +// FromContext returns the database Interface associated with this context. +func FromContext(c context.Context) Interface { v := c.Value(key) if v == nil { return nil } - d, ok := v.(Service) + d, ok := v.(Interface) if !ok { return nil } @@ -30,8 +33,24 @@ func FromContext(c context.Context) Service { return d } -// ToContext adds the database Service to this context if it supports +// ToContext adds the database Interface to this context if it supports // the Setter interface. -func ToContext(c Setter, d Service) { +func ToContext(c Setter, d Interface) { c.Set(key, d) } + +// FromCLIContext creates and returns a database engine from the urfave/cli context. +func FromCLIContext(c *cli.Context) (Interface, error) { + logrus.Debug("creating database engine from CLI configuration") + + return New( + WithAddress(c.String("database.addr")), + WithCompressionLevel(c.Int("database.compression.level")), + WithConnectionLife(c.Duration("database.connection.life")), + WithConnectionIdle(c.Int("database.connection.idle")), + WithConnectionOpen(c.Int("database.connection.open")), + WithDriver(c.String("database.driver")), + WithEncryptionKey(c.String("database.encryption.key")), + WithSkipCreation(c.Bool("database.skip_creation")), + ) +} diff --git a/database/context_test.go b/database/context_test.go index 4c997ef04..4037b26c0 100644 --- a/database/context_test.go +++ b/database/context_test.go @@ -1,89 +1,152 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. package database import ( + "flag" + "reflect" "testing" + "time" "github.com/gin-gonic/gin" - "github.com/go-vela/server/database/sqlite" + "github.com/urfave/cli/v2" ) func TestDatabase_FromContext(t *testing.T) { - // setup types - want, _ := sqlite.NewTest() - defer func() { _sql, _ := want.Sqlite.DB(); _sql.Close() }() + _postgres, _ := testPostgres(t) + defer _postgres.Close() - // setup context gin.SetMode(gin.TestMode) - context, _ := gin.CreateTestContext(nil) - context.Set(key, want) - - // run test - got := FromContext(context) + ctx, _ := gin.CreateTestContext(nil) + ctx.Set(key, _postgres) + + typeCtx, _ := gin.CreateTestContext(nil) + typeCtx.Set(key, nil) + + nilCtx, _ := gin.CreateTestContext(nil) + nilCtx.Set(key, nil) + + // setup tests + tests := []struct { + name string + context *gin.Context + want Interface + }{ + { + name: "success", + context: ctx, + want: _postgres, + }, + { + name: "failure with nil", + context: nilCtx, + want: nil, + }, + { + name: "failure with wrong type", + context: typeCtx, + want: nil, + }, + } - if got != want { - t.Errorf("FromContext is %v, want %v", got, want) + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := FromContext(test.context) + if !reflect.DeepEqual(got, test.want) { + t.Errorf("FromContext for %s is %v, want %v", test.name, got, test.want) + } + }) } } -func TestDatabase_FromContext_Bad(t *testing.T) { - // setup context - gin.SetMode(gin.TestMode) +func TestDatabase_ToContext(t *testing.T) { context, _ := gin.CreateTestContext(nil) - context.Set(key, nil) - // run test - got := FromContext(context) - - if got != nil { - t.Errorf("FromContext is %v, want nil", got) + _postgres, _ := testPostgres(t) + defer _postgres.Close() + + _sqlite := testSqlite(t) + defer _sqlite.Close() + + // setup tests + tests := []struct { + name string + database *engine + want *engine + }{ + { + name: "success with postgres", + database: _postgres, + want: _postgres, + }, + { + name: "success with sqlite3", + database: _sqlite, + want: _sqlite, + }, } -} - -func TestDatabase_FromContext_WrongType(t *testing.T) { - // setup context - gin.SetMode(gin.TestMode) - context, _ := gin.CreateTestContext(nil) - context.Set(key, 1) - // run test - got := FromContext(context) + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ToContext(context, test.want) - if got != nil { - t.Errorf("FromContext is %v, want nil", got) + got := context.Value(key) + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ToContext for %s is %v, want %v", test.name, got, test.want) + } + }) } } -func TestDatabase_FromContext_Empty(t *testing.T) { - // setup context - gin.SetMode(gin.TestMode) - context, _ := gin.CreateTestContext(nil) - - // run test - got := FromContext(context) - - if got != nil { - t.Errorf("FromContext is %v, want nil", got) +func TestDatabase_FromCLIContext(t *testing.T) { + flags := flag.NewFlagSet("test", 0) + flags.String("database.driver", "sqlite3", "doc") + flags.String("database.addr", "file::memory:?cache=shared", "doc") + flags.Int("database.compression.level", 3, "doc") + flags.Duration("database.connection.life", 10*time.Second, "doc") + flags.Int("database.connection.idle", 5, "doc") + flags.Int("database.connection.open", 20, "doc") + flags.String("database.encryption.key", "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", "doc") + flags.Bool("database.skip_creation", true, "doc") + + // setup tests + tests := []struct { + name string + failure bool + context *cli.Context + }{ + { + name: "success", + failure: false, + context: cli.NewContext(&cli.App{Name: "vela"}, flags, nil), + }, + { + name: "failure", + failure: true, + context: cli.NewContext(&cli.App{Name: "vela"}, flag.NewFlagSet("test", 0), nil), + }, } -} -func TestDatabase_ToContext(t *testing.T) { - // setup types - want, _ := sqlite.NewTest() - defer func() { _sql, _ := want.Sqlite.DB(); _sql.Close() }() + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := FromCLIContext(test.context) - // setup context - gin.SetMode(gin.TestMode) - context, _ := gin.CreateTestContext(nil) - ToContext(context, want) + if test.failure { + if err == nil { + t.Errorf("FromCLIContext for %s should have returned err", test.name) + } - // run test - got := context.Value(key) + return + } - if got != want { - t.Errorf("ToContext is %v, want %v", got, want) + if err != nil { + t.Errorf("FromCLIContext for %s returned err: %v", test.name, err) + } + }) } } diff --git a/database/database.go b/database/database.go index 884d8a734..ea93bfaaf 100644 --- a/database/database.go +++ b/database/database.go @@ -1,50 +1,174 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. package database import ( + "context" "fmt" + "time" + "github.com/go-vela/server/database/build" + "github.com/go-vela/server/database/executable" + "github.com/go-vela/server/database/hook" + "github.com/go-vela/server/database/log" + "github.com/go-vela/server/database/pipeline" + "github.com/go-vela/server/database/repo" + "github.com/go-vela/server/database/schedule" + "github.com/go-vela/server/database/secret" + "github.com/go-vela/server/database/service" + "github.com/go-vela/server/database/step" + "github.com/go-vela/server/database/user" + "github.com/go-vela/server/database/worker" "github.com/go-vela/types/constants" - "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) -// nolint: godot // top level comment ends in a list -// -// New creates and returns a Vela service capable of -// integrating with the configured database provider. +type ( + // config represents the settings required to create the engine that implements the Interface. + config struct { + // specifies the address to use for the database engine + Address string + // specifies the level of compression to use for the database engine + CompressionLevel int + // specifies the connection duration to use for the database engine + ConnectionLife time.Duration + // specifies the maximum idle connections for the database engine + ConnectionIdle int + // specifies the maximum open connections for the database engine + ConnectionOpen int + // specifies the driver to use for the database engine + Driver string + // specifies the encryption key to use for the database engine + EncryptionKey string + // specifies to skip creating tables and indexes for the database engine + SkipCreation bool + } + + // engine represents the functionality that implements the Interface. + engine struct { + // gorm.io/gorm database client used in database functions + client *gorm.DB + // engine configuration settings used in database functions + config *config + // engine context used in database functions + ctx context.Context + // sirupsen/logrus logger used in database functions + logger *logrus.Entry + + build.BuildInterface + executable.BuildExecutableInterface + hook.HookInterface + log.LogInterface + pipeline.PipelineInterface + repo.RepoInterface + schedule.ScheduleInterface + secret.SecretInterface + service.ServiceInterface + step.StepInterface + user.UserInterface + worker.WorkerInterface + } +) + +// New creates and returns an engine capable of integrating with the configured database provider. // -// Currently the following database providers are supported: +// Currently, the following database providers are supported: // -// * Postgres -// * Sqlite -func New(s *Setup) (Service, error) { - // validate the setup being provided - // - // https://pkg.go.dev/github.com/go-vela/server/database?tab=doc#Setup.Validate - err := s.Validate() +// * postgres +// * sqlite3 +func New(opts ...EngineOpt) (Interface, error) { + // create new database engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + e.ctx = context.TODO() + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // validate the configuration being provided + err := e.config.Validate() if err != nil { return nil, err } - logrus.Debug("creating database service from setup") + // update the logger with additional metadata + e.logger = logrus.NewEntry(logrus.StandardLogger()).WithField("database", e.Driver()) + + e.logger.Trace("creating database engine from configuration") // process the database driver being provided - switch s.Driver { + switch e.config.Driver { case constants.DriverPostgres: - // handle the Postgres database driver being provided - // - // https://pkg.go.dev/github.com/go-vela/server/database?tab=doc#Setup.Postgres - return s.Postgres() + // create the new Postgres database client + e.client, err = gorm.Open(postgres.Open(e.config.Address), &gorm.Config{}) + if err != nil { + return nil, err + } case constants.DriverSqlite: - // handle the Sqlite database driver being provided - // - // https://pkg.go.dev/github.com/go-vela/server/database?tab=doc#Setup.Sqlite - return s.Sqlite() + // create the new Sqlite database client + e.client, err = gorm.Open(sqlite.Open(e.config.Address), &gorm.Config{}) + if err != nil { + return nil, err + } default: // handle an invalid database driver being provided - return nil, fmt.Errorf("invalid database driver provided: %s", s.Driver) + return nil, fmt.Errorf("invalid database driver provided: %s", e.Driver()) } + + // capture database/sql database from gorm.io/gorm database + db, err := e.client.DB() + if err != nil { + return nil, err + } + + // set the maximum amount of time a connection may be reused + db.SetConnMaxLifetime(e.config.ConnectionLife) + // set the maximum number of connections in the idle connection pool + db.SetMaxIdleConns(e.config.ConnectionIdle) + // set the maximum number of open connections to the database + db.SetMaxOpenConns(e.config.ConnectionOpen) + + // verify connection to the database + err = e.Ping() + if err != nil { + return nil, err + } + + // create database agnostic engines for resources + err = e.NewResources(e.ctx) + if err != nil { + return nil, err + } + + return e, nil +} + +// NewTest creates and returns an engine that integrates with an in-memory database provider. +// +// This function is ONLY intended to be used for testing purposes. +func NewTest() (Interface, error) { + return New( + WithAddress("file::memory:?cache=shared"), + WithCompressionLevel(3), + WithConnectionLife(30*time.Minute), + WithConnectionIdle(2), + WithConnectionOpen(0), + WithDriver("sqlite3"), + WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), + WithSkipCreation(false), + ) } diff --git a/database/database_test.go b/database/database_test.go index 14081a382..a1cfecc8a 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -7,17 +7,26 @@ package database import ( "testing" "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) func TestDatabase_New(t *testing.T) { // setup tests tests := []struct { failure bool - setup *Setup + name string + config *config }{ { + name: "failure with postgres", failure: true, - setup: &Setup{ + config: &config{ Driver: "postgres", Address: "postgres://foo:bar@localhost:5432/vela", CompressionLevel: 3, @@ -29,8 +38,9 @@ func TestDatabase_New(t *testing.T) { }, }, { + name: "success with sqlite3", failure: false, - setup: &Setup{ + config: &config{ Driver: "sqlite3", Address: "file::memory:?cache=shared", CompressionLevel: 3, @@ -42,10 +52,11 @@ func TestDatabase_New(t *testing.T) { }, }, { + name: "failure with invalid config", failure: true, - setup: &Setup{ - Driver: "mysql", - Address: "foo:bar@tcp(localhost:3306)/vela?charset=utf8mb4&parseTime=True&loc=Local", + config: &config{ + Driver: "postgres", + Address: "", CompressionLevel: 3, ConnectionLife: 10 * time.Second, ConnectionIdle: 5, @@ -55,10 +66,11 @@ func TestDatabase_New(t *testing.T) { }, }, { + name: "failure with invalid driver", failure: true, - setup: &Setup{ - Driver: "postgres", - Address: "", + config: &config{ + Driver: "mysql", + Address: "foo:bar@tcp(localhost:3306)/vela?charset=utf8mb4&parseTime=True&loc=Local", CompressionLevel: 3, ConnectionLife: 10 * time.Second, ConnectionIdle: 5, @@ -71,18 +83,99 @@ func TestDatabase_New(t *testing.T) { // run tests for _, test := range tests { - _, err := New(test.setup) + t.Run(test.name, func(t *testing.T) { + _, err := New( + WithAddress(test.config.Address), + WithCompressionLevel(test.config.CompressionLevel), + WithConnectionLife(test.config.ConnectionLife), + WithConnectionIdle(test.config.ConnectionIdle), + WithConnectionOpen(test.config.ConnectionOpen), + WithDriver(test.config.Driver), + WithEncryptionKey(test.config.EncryptionKey), + WithSkipCreation(test.config.SkipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } - if test.failure { - if err == nil { - t.Errorf("New should have returned err") + return } - continue - } + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the engine with test configuration + _engine := &engine{ + config: &config{ + CompressionLevel: 3, + ConnectionLife: 30 * time.Minute, + ConnectionIdle: 2, + ConnectionOpen: 0, + Driver: "postgres", + EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + SkipCreation: false, + }, + logger: logrus.NewEntry(logrus.StandardLogger()), + } + + // create the new mock sql database + _sql, _mock, err := sqlmock.New( + sqlmock.MonitorPingsOption(true), + sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual), + ) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + // ensure the mock expects the ping + _mock.ExpectPing() + + // create the new mock Postgres database client + _engine.client, err = gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new test postgres database: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + var err error - if err != nil { - t.Errorf("New returned err: %v", err) - } + // create the engine with test configuration + _engine := &engine{ + config: &config{ + Address: "file::memory:?cache=shared", + CompressionLevel: 3, + ConnectionLife: 30 * time.Minute, + ConnectionIdle: 2, + ConnectionOpen: 0, + Driver: "sqlite3", + EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + SkipCreation: false, + }, + logger: logrus.NewEntry(logrus.StandardLogger()), } + + // create the new mock Sqlite database client + _engine.client, err = gorm.Open( + sqlite.Open(_engine.config.Address), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new test sqlite database: %v", err) + } + + return _engine } diff --git a/database/doc.go b/database/doc.go index 42b459f39..42fe1feae 100644 --- a/database/doc.go +++ b/database/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/database" +// import "github.com/go-vela/server/database" package database diff --git a/database/driver.go b/database/driver.go new file mode 100644 index 000000000..1c3130094 --- /dev/null +++ b/database/driver.go @@ -0,0 +1,10 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +// Driver outputs the configured database driver. +func (e *engine) Driver() string { + return e.config.Driver +} diff --git a/database/driver_test.go b/database/driver_test.go new file mode 100644 index 000000000..6d382d8cd --- /dev/null +++ b/database/driver_test.go @@ -0,0 +1,47 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +import ( + "strings" + "testing" +) + +func TestDatabase_Engine_Driver(t *testing.T) { + _postgres, _ := testPostgres(t) + defer _postgres.Close() + + _sqlite := testSqlite(t) + defer _sqlite.Close() + + // setup tests + tests := []struct { + name string + database *engine + want string + }{ + { + name: "success with postgres", + database: _postgres, + want: "postgres", + }, + { + name: "success with sqlite3", + database: _sqlite, + want: "sqlite3", + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := test.database.Driver() + + if !strings.EqualFold(got, test.want) { + t.Errorf("Driver for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/executable/create.go b/database/executable/create.go new file mode 100644 index 000000000..ac29ec768 --- /dev/null +++ b/database/executable/create.go @@ -0,0 +1,57 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package executable + +import ( + "context" + "fmt" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreateBuildExecutable creates a new build executable in the database. +func (e *engine) CreateBuildExecutable(ctx context.Context, b *library.BuildExecutable) error { + e.logger.WithFields(logrus.Fields{ + "build": b.GetBuildID(), + }).Tracef("creating build executable for build %d in the database", b.GetBuildID()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#BuildExecutableFromLibrary + executable := database.BuildExecutableFromLibrary(b) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#BuildExecutable.Validate + err := executable.Validate() + if err != nil { + return err + } + + // compress data for the build executable + // + // https://pkg.go.dev/github.com/go-vela/types/database#BuildExecutable.Compress + err = executable.Compress(e.config.CompressionLevel) + if err != nil { + return err + } + + // encrypt the data field for the build executable + // + // https://pkg.go.dev/github.com/go-vela/types/database#BuildExecutable.Encrypt + err = executable.Encrypt(e.config.EncryptionKey) + if err != nil { + return fmt.Errorf("unable to encrypt build executable for build %d: %w", b.GetBuildID(), err) + } + + // send query to the database + return e.client. + Table(constants.TableBuildExecutable). + Create(executable). + Error +} diff --git a/database/executable/create_test.go b/database/executable/create_test.go new file mode 100644 index 000000000..e3f664314 --- /dev/null +++ b/database/executable/create_test.go @@ -0,0 +1,72 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package executable + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestExecutable_Engine_CreateBuildExecutable(t *testing.T) { + // setup types + _bExecutable := testBuildExecutable() + _bExecutable.SetID(1) + _bExecutable.SetBuildID(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`INSERT INTO "build_executables" +("build_id","data","id") +VALUES ($1,$2,$3) RETURNING "id"`). + WithArgs(1, AnyArgument{}, 1). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateBuildExecutable(context.TODO(), _bExecutable) + + if test.failure { + if err == nil { + t.Errorf("CreateBuildExecutable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateBuildExecutable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/executable/executable.go b/database/executable/executable.go new file mode 100644 index 000000000..556d86754 --- /dev/null +++ b/database/executable/executable.go @@ -0,0 +1,83 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package executable + +import ( + "context" + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the BuildExecutableService interface. + config struct { + // specifies the level of compression to use for the BuildExecutable engine + CompressionLevel int + // specifies the encryption key to use for the BuildExecutable engine + EncryptionKey string + // specifies to skip creating tables and indexes for the BuildExecutable engine + SkipCreation bool + // specifies the driver for proper popping query + Driver string + } + + // engine represents the build executable functionality that implements the BuildExecutableService interface. + engine struct { + // engine configuration settings used in build executable functions + config *config + + ctx context.Context + + // gorm.io/gorm database client used in build executable functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in build executable functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with build executables in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new BuildExecutable engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating build executable database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of build executables table and indexes in the database") + + return e, nil + } + + // create the build executables table + err := e.CreateBuildExecutableTable(e.ctx, e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableBuildExecutable, err) + } + + return e, nil +} diff --git a/database/executable/executable_test.go b/database/executable/executable_test.go new file mode 100644 index 000000000..20ebd6b49 --- /dev/null +++ b/database/executable/executable_test.go @@ -0,0 +1,213 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package executable + +import ( + "database/sql/driver" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestExecutable_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + level int + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + level: 1, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{ + CompressionLevel: 1, + EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + SkipCreation: false, + Driver: "postgres", + }, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + level: 1, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{ + CompressionLevel: 1, + EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + SkipCreation: false, + Driver: "sqlite3", + }, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithClient(test.client), + WithCompressionLevel(test.level), + WithEncryptionKey(test.key), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + WithDriver(test.name), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithCompressionLevel(0), + WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + WithDriver(constants.DriverPostgres), + ) + if err != nil { + t.Errorf("unable to create new postgres build_itnerary engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithCompressionLevel(0), + WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + WithDriver(constants.DriverSqlite), + ) + if err != nil { + t.Errorf("unable to create new sqlite build_itnerary engine: %v", err) + } + + return _engine +} + +// testBuildExecutable is a test helper function to create a library +// BuildExecutable type with all fields set to their zero values. +func testBuildExecutable() *library.BuildExecutable { + return &library.BuildExecutable{ + ID: new(int64), + BuildID: new(int64), + Data: new([]byte), + } +} + +// This will be used with the github.com/DATA-DOG/go-sqlmock library to compare values +// that are otherwise not easily compared. These typically would be values generated +// before adding or updating them in the database. +// +// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime +type AnyArgument struct{} + +// Match satisfies sqlmock.Argument interface. +func (a AnyArgument) Match(v driver.Value) bool { + return v != nil +} diff --git a/database/executable/interface.go b/database/executable/interface.go new file mode 100644 index 000000000..9e3bd50b3 --- /dev/null +++ b/database/executable/interface.go @@ -0,0 +1,29 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package executable + +import ( + "context" + + "github.com/go-vela/types/library" +) + +// BuildExecutableInterface represents the Vela interface for build executable +// functions with the supported Database backends. +type BuildExecutableInterface interface { + // BuildExecutable Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + CreateBuildExecutableTable(context.Context, string) error + + // BuildExecutable Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CreateBuildExecutable defines a function that creates a build executable. + CreateBuildExecutable(context.Context, *library.BuildExecutable) error + // PopBuildExecutable defines a function that gets and deletes a build executable. + PopBuildExecutable(context.Context, int64) (*library.BuildExecutable, error) +} diff --git a/database/executable/opts.go b/database/executable/opts.go new file mode 100644 index 000000000..fa601a03d --- /dev/null +++ b/database/executable/opts.go @@ -0,0 +1,85 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package executable + +import ( + "context" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for build executables. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for build executables. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the build executable engine + e.client = client + + return nil + } +} + +// WithCompressionLevel sets the compression level in the database engine for build executables. +func WithCompressionLevel(level int) EngineOpt { + return func(e *engine) error { + // set the compression level in the build executable engine + e.config.CompressionLevel = level + + return nil + } +} + +// WithEncryptionKey sets the encryption key in the database engine for build executables. +func WithEncryptionKey(key string) EngineOpt { + return func(e *engine) error { + // set the encryption key in the build executables engine + e.config.EncryptionKey = key + + return nil + } +} + +// WithDriver sets the driver type in the database engine for build executables. +func WithDriver(driver string) EngineOpt { + return func(e *engine) error { + // set the driver type in the build executable engine + e.config.Driver = driver + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for build executables. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the build executable engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for build executables. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the build executable engine + e.config.SkipCreation = skipCreation + + return nil + } +} + +// WithContext sets the context in the database engine for build executables. +func WithContext(ctx context.Context) EngineOpt { + return func(e *engine) error { + e.ctx = ctx + + return nil + } +} diff --git a/database/executable/opts_test.go b/database/executable/opts_test.go new file mode 100644 index 000000000..61876dd32 --- /dev/null +++ b/database/executable/opts_test.go @@ -0,0 +1,315 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package executable + +import ( + "context" + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestExecutable_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestExecutable_EngineOpt_WithCompressionLevel(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + level int + want int + }{ + { + failure: false, + name: "compression level set to -1", + level: -1, + want: -1, + }, + { + failure: false, + name: "compression level set to 0", + level: 0, + want: 0, + }, + { + failure: false, + name: "compression level set to 1", + level: 1, + want: 1, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithCompressionLevel(test.level)(e) + + if test.failure { + if err == nil { + t.Errorf("WithCompressionLevel for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithCompressionLevel returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.CompressionLevel, test.want) { + t.Errorf("WithCompressionLevel is %v, want %v", e.config.CompressionLevel, test.want) + } + }) + } +} + +func TestExecutable_EngineOpt_WithEncryptionKey(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + key string + want string + }{ + { + failure: false, + name: "encryption key set", + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + want: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + }, + { + failure: false, + name: "encryption key not set", + key: "", + want: "", + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithEncryptionKey(test.key)(e) + + if test.failure { + if err == nil { + t.Errorf("WithEncryptionKey for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithEncryptionKey returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.EncryptionKey, test.want) { + t.Errorf("WithEncryptionKey is %v, want %v", e.config.EncryptionKey, test.want) + } + }) + } +} + +func TestExecutable_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestExecutable_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} + +func TestExecutable_EngineOpt_WithContext(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + ctx context.Context + want context.Context + }{ + { + failure: false, + name: "context set to TODO", + ctx: context.TODO(), + want: context.TODO(), + }, + { + failure: false, + name: "context set to nil", + ctx: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithContext(test.ctx)(e) + + if test.failure { + if err == nil { + t.Errorf("WithContext for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithContext returned err: %v", err) + } + + if !reflect.DeepEqual(e.ctx, test.want) { + t.Errorf("WithContext is %v, want %v", e.ctx, test.want) + } + }) + } +} diff --git a/database/executable/pop.go b/database/executable/pop.go new file mode 100644 index 000000000..64a2fbcf5 --- /dev/null +++ b/database/executable/pop.go @@ -0,0 +1,80 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package executable + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "gorm.io/gorm/clause" +) + +// PopBuildExecutable pops a build executable by build_id from the database. +func (e *engine) PopBuildExecutable(ctx context.Context, id int64) (*library.BuildExecutable, error) { + e.logger.Tracef("popping build executable for build %d from the database", id) + + // variable to store query results + b := new(database.BuildExecutable) + + // at the time of coding, GORM does not implement a version of Sqlite3 that supports RETURNING. + // so we have to select and delete for the Sqlite driver. + switch e.config.Driver { + case constants.DriverPostgres: + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuildExecutable). + Clauses(clause.Returning{}). + Where("build_id = ?", id). + Delete(b). + Error + + if err != nil { + return nil, err + } + + case constants.DriverSqlite: + // send query to the database and store result in variable + err := e.client. + Table(constants.TableBuildExecutable). + Where("id = ?", id). + Take(b). + Error + if err != nil { + return nil, err + } + + // send query to the database to delete result just got + err = e.client. + Table(constants.TableBuildExecutable). + Delete(b). + Error + if err != nil { + return nil, err + } + } + + // decrypt the fields for the build executable + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt + err := b.Decrypt(e.config.EncryptionKey) + if err != nil { + return nil, err + } + + // decompress data for the build executable + // + // https://pkg.go.dev/github.com/go-vela/types/database#BuildExecutable.Decompress + err = b.Decompress() + if err != nil { + return nil, err + } + + // return the decompressed build executable + // + // https://pkg.go.dev/github.com/go-vela/types/database#BuildExecutable.ToLibrary + return b.ToLibrary(), nil +} diff --git a/database/executable/pop_test.go b/database/executable/pop_test.go new file mode 100644 index 000000000..cf282d013 --- /dev/null +++ b/database/executable/pop_test.go @@ -0,0 +1,85 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package executable + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestExecutable_Engine_PopBuildExecutable(t *testing.T) { + // setup types + _bExecutable := testBuildExecutable() + _bExecutable.SetID(1) + _bExecutable.SetBuildID(1) + _bExecutable.SetData([]byte("foo")) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "build_id", "data"}). + AddRow(1, 1, "+//18dbf7mF+v7ZPK3Wo5h2TD6v4Zg95sCMUJYO2tpwY37DEgTxW5xdyt3Tey9w=") + + // ensure the mock expects the query + _mock.ExpectQuery(`DELETE FROM "build_executables" WHERE build_id = $1 RETURNING *`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateBuildExecutable(context.TODO(), _bExecutable) + if err != nil { + t.Errorf("unable to create test build executable for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.BuildExecutable + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _bExecutable, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _bExecutable, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.PopBuildExecutable(context.TODO(), 1) + + if test.failure { + if err == nil { + t.Errorf("PopBuildExecutable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("PopBuildExecutable for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("PopBuildExecutable for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/executable/table.go b/database/executable/table.go new file mode 100644 index 000000000..c36834fe6 --- /dev/null +++ b/database/executable/table.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package executable + +import ( + "context" + + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres build_executables table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +build_executables ( + id SERIAL PRIMARY KEY, + build_id INTEGER, + data BYTEA, + UNIQUE(build_id) +); +` + + // CreateSqliteTable represents a query to create the Sqlite build_executables table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +build_executables ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + build_id INTEGER, + data BLOB, + UNIQUE(build_id) +); +` +) + +// CreateBuildExecutableTable creates the build executables table in the database. +func (e *engine) CreateBuildExecutableTable(ctx context.Context, driver string) error { + e.logger.Tracef("creating build_executables table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the build_executables table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the build_executables table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/executable/table_test.go b/database/executable/table_test.go new file mode 100644 index 000000000..b4f3cd057 --- /dev/null +++ b/database/executable/table_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package executable + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestExecutable_Engine_CreateBuildExecutableTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateBuildExecutableTable(context.TODO(), test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateBuildExecutableTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateBuildExecutableTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/flags.go b/database/flags.go index 75d3c109b..f8d4fd36e 100644 --- a/database/flags.go +++ b/database/flags.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -11,13 +11,8 @@ import ( "github.com/urfave/cli/v2" ) -// Flags represents all supported command line -// interface (CLI) flags for the database. -// -// https://pkg.go.dev/github.com/urfave/cli?tab=doc#Flag +// Flags represents all supported command line interface (CLI) flags for the database. var Flags = []cli.Flag{ - // Database Flags - &cli.StringFlag{ EnvVars: []string{"VELA_DATABASE_DRIVER", "DATABASE_DRIVER"}, FilePath: "/vela/database/driver", diff --git a/database/hook/count.go b/database/hook/count.go new file mode 100644 index 000000000..a1a9689f8 --- /dev/null +++ b/database/hook/count.go @@ -0,0 +1,25 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "github.com/go-vela/types/constants" +) + +// CountHooks gets the count of all hooks from the database. +func (e *engine) CountHooks() (int64, error) { + e.logger.Tracef("getting count of all hooks from the database") + + // variable to store query results + var h int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableHook). + Count(&h). + Error + + return h, err +} diff --git a/database/sqlite/hook_count.go b/database/hook/count_repo.go similarity index 62% rename from database/sqlite/hook_count.go rename to database/hook/count_repo.go index 793ec10b8..eb6d6763b 100644 --- a/database/sqlite/hook_count.go +++ b/database/hook/count_repo.go @@ -2,18 +2,17 @@ // // Use of this source code is governed by the LICENSE file in this repository. -package sqlite +package hook import ( - "github.com/go-vela/server/database/sqlite/dml" "github.com/go-vela/types/constants" "github.com/go-vela/types/library" "github.com/sirupsen/logrus" ) -// GetRepoHookCount gets the count of webhooks by repo ID from the database. -func (c *client) GetRepoHookCount(r *library.Repo) (int64, error) { - c.Logger.WithFields(logrus.Fields{ +// CountHooksForRepo gets the count of hooks by repo ID from the database. +func (e *engine) CountHooksForRepo(r *library.Repo) (int64, error) { + e.logger.WithFields(logrus.Fields{ "org": r.GetOrg(), "repo": r.GetName(), }).Tracef("getting count of hooks for repo %s from the database", r.GetFullName()) @@ -22,10 +21,11 @@ func (c *client) GetRepoHookCount(r *library.Repo) (int64, error) { var h int64 // send query to the database and store result in variable - err := c.Sqlite. + err := e.client. Table(constants.TableHook). - Raw(dml.SelectRepoHookCount, r.GetID()). - Pluck("count", &h).Error + Where("repo_id = ?", r.GetID()). + Count(&h). + Error return h, err } diff --git a/database/hook/count_repo_test.go b/database/hook/count_repo_test.go new file mode 100644 index 000000000..5814764e6 --- /dev/null +++ b/database/hook/count_repo_test.go @@ -0,0 +1,104 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestHook_Engine_CountHooksForRepo(t *testing.T) { + // setup types + _hookOne := testHook() + _hookOne.SetID(1) + _hookOne.SetRepoID(1) + _hookOne.SetBuildID(1) + _hookOne.SetNumber(1) + _hookOne.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hookOne.SetWebhookID(1) + + _hookTwo := testHook() + _hookTwo.SetID(2) + _hookTwo.SetRepoID(2) + _hookTwo.SetBuildID(2) + _hookTwo.SetNumber(2) + _hookTwo.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hookTwo.SetWebhookID(1) + + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "hooks" WHERE repo_id = $1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateHook(_hookOne) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + _, err = _sqlite.CreateHook(_hookTwo) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 1, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 1, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountHooksForRepo(_repo) + + if test.failure { + if err == nil { + t.Errorf("CountHooksForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountHooksForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountHooksForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/hook/count_test.go b/database/hook/count_test.go new file mode 100644 index 000000000..dce97587f --- /dev/null +++ b/database/hook/count_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestHook_Engine_CountHooks(t *testing.T) { + // setup types + _hookOne := testHook() + _hookOne.SetID(1) + _hookOne.SetRepoID(1) + _hookOne.SetBuildID(1) + _hookOne.SetNumber(1) + _hookOne.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hookOne.SetWebhookID(1) + + _hookTwo := testHook() + _hookTwo.SetID(2) + _hookTwo.SetRepoID(1) + _hookTwo.SetBuildID(2) + _hookTwo.SetNumber(2) + _hookTwo.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hookTwo.SetWebhookID(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "hooks"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateHook(_hookOne) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + _, err = _sqlite.CreateHook(_hookTwo) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountHooks() + + if test.failure { + if err == nil { + t.Errorf("CountHooks for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountHooks for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountHooks for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/hook/create.go b/database/hook/create.go new file mode 100644 index 000000000..e2bf5489d --- /dev/null +++ b/database/hook/create.go @@ -0,0 +1,37 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreateHook creates a new hook in the database. +func (e *engine) CreateHook(h *library.Hook) (*library.Hook, error) { + e.logger.WithFields(logrus.Fields{ + "hook": h.GetNumber(), + }).Tracef("creating hook %d in the database", h.GetNumber()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#HookFromLibrary + hook := database.HookFromLibrary(h) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Hook.Validate + err := hook.Validate() + if err != nil { + return nil, err + } + + result := e.client.Table(constants.TableHook).Create(hook) + + // send query to the database + return hook.ToLibrary(), result.Error +} diff --git a/database/hook/create_test.go b/database/hook/create_test.go new file mode 100644 index 000000000..f05bd8a9d --- /dev/null +++ b/database/hook/create_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestHook_Engine_CreateHook(t *testing.T) { + // setup types + _hook := testHook() + _hook.SetID(1) + _hook.SetRepoID(1) + _hook.SetBuildID(1) + _hook.SetNumber(1) + _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hook.SetWebhookID(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`INSERT INTO "hooks" +("repo_id","build_id","number","source_id","created","host","event","event_action","branch","error","status","link","webhook_id","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING "id"`). + WithArgs(1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", nil, nil, nil, nil, nil, nil, nil, nil, 1, 1). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := test.database.CreateHook(_hook) + + if test.failure { + if err == nil { + t.Errorf("CreateHook for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateHook for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/hook/delete.go b/database/hook/delete.go new file mode 100644 index 000000000..d4e688f1c --- /dev/null +++ b/database/hook/delete.go @@ -0,0 +1,30 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// DeleteHook deletes an existing hook from the database. +func (e *engine) DeleteHook(h *library.Hook) error { + e.logger.WithFields(logrus.Fields{ + "hook": h.GetNumber(), + }).Tracef("deleting hook %d in the database", h.GetNumber()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#HookFromLibrary + hook := database.HookFromLibrary(h) + + // send query to the database + return e.client. + Table(constants.TableHook). + Delete(hook). + Error +} diff --git a/database/hook/delete_test.go b/database/hook/delete_test.go new file mode 100644 index 000000000..2fb91d567 --- /dev/null +++ b/database/hook/delete_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestHook_Engine_DeleteHook(t *testing.T) { + // setup types + _hook := testHook() + _hook.SetID(1) + _hook.SetRepoID(1) + _hook.SetBuildID(1) + _hook.SetNumber(1) + _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hook.SetWebhookID(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`DELETE FROM "hooks" WHERE "hooks"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateHook(_hook) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeleteHook(_hook) + + if test.failure { + if err == nil { + t.Errorf("DeleteHook for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeleteHook for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/hook/get.go b/database/hook/get.go new file mode 100644 index 000000000..13547669c --- /dev/null +++ b/database/hook/get.go @@ -0,0 +1,34 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetHook gets a hook by ID from the database. +func (e *engine) GetHook(id int64) (*library.Hook, error) { + e.logger.Tracef("getting hook %d from the database", id) + + // variable to store query results + h := new(database.Hook) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableHook). + Where("id = ?", id). + Take(h). + Error + if err != nil { + return nil, err + } + + // return the hook + // + // https://pkg.go.dev/github.com/go-vela/types/database#Hook.ToLibrary + return h.ToLibrary(), nil +} diff --git a/database/hook/get_repo.go b/database/hook/get_repo.go new file mode 100644 index 000000000..4c7e3b857 --- /dev/null +++ b/database/hook/get_repo.go @@ -0,0 +1,40 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetHookForRepo gets a hook by repo ID and number from the database. +func (e *engine) GetHookForRepo(r *library.Repo, number int) (*library.Hook, error) { + e.logger.WithFields(logrus.Fields{ + "hook": number, + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("getting hook %s/%d from the database", r.GetFullName(), number) + + // variable to store query results + h := new(database.Hook) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableHook). + Where("repo_id = ?", r.GetID()). + Where("number = ?", number). + Take(h). + Error + if err != nil { + return nil, err + } + + // return the hook + // + // https://pkg.go.dev/github.com/go-vela/types/database#Hook.ToLibrary + return h.ToLibrary(), nil +} diff --git a/database/hook/get_repo_test.go b/database/hook/get_repo_test.go new file mode 100644 index 000000000..32fb9a813 --- /dev/null +++ b/database/hook/get_repo_test.go @@ -0,0 +1,94 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestHook_Engine_GetHookForRepo(t *testing.T) { + // setup types + _hook := testHook() + _hook.SetID(1) + _hook.SetRepoID(1) + _hook.SetBuildID(1) + _hook.SetNumber(1) + _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hook.SetWebhookID(1) + + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "source_id", "created", "host", "event", "event_action", "branch", "error", "status", "link", "webhook_id"}). + AddRow(1, 1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", "", "", 1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "hooks" WHERE repo_id = $1 AND number = $2 LIMIT 1`).WithArgs(1, 1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateHook(_hook) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Hook + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _hook, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _hook, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetHookForRepo(_repo, 1) + + if test.failure { + if err == nil { + t.Errorf("GetHookForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetHookForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetHookForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/hook/get_test.go b/database/hook/get_test.go new file mode 100644 index 000000000..b84fe7b13 --- /dev/null +++ b/database/hook/get_test.go @@ -0,0 +1,87 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestHook_Engine_GetHook(t *testing.T) { + // setup types + _hook := testHook() + _hook.SetID(1) + _hook.SetRepoID(1) + _hook.SetBuildID(1) + _hook.SetNumber(1) + _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hook.SetWebhookID(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "source_id", "created", "host", "event", "event_action", "branch", "error", "status", "link", "webhook_id"}, + ).AddRow(1, 1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", "", "", 1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "hooks" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateHook(_hook) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Hook + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _hook, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _hook, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetHook(1) + + if test.failure { + if err == nil { + t.Errorf("GetHook for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetHook for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetHook for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/hook/hook.go b/database/hook/hook.go new file mode 100644 index 000000000..5225f901a --- /dev/null +++ b/database/hook/hook.go @@ -0,0 +1,80 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the HookInterface interface. + config struct { + // specifies to skip creating tables and indexes for the Hook engine + SkipCreation bool + } + + // engine represents the hook functionality that implements the HookInterface interface. + engine struct { + // engine configuration settings used in hook functions + config *config + + // gorm.io/gorm database client used in hook functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in hook functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with hooks in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new Hook engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating hook database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of hooks table and indexes in the database") + + return e, nil + } + + // create the hooks table + err := e.CreateHookTable(e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableHook, err) + } + + // create the indexes for the hooks table + err = e.CreateHookIndexes() + if err != nil { + return nil, fmt.Errorf("unable to create indexes for %s table: %w", constants.TableHook, err) + } + + return e, nil +} diff --git a/database/hook/hook_test.go b/database/hook/hook_test.go new file mode 100644 index 000000000..e8d3130cc --- /dev/null +++ b/database/hook/hook_test.go @@ -0,0 +1,218 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestHook_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithClient(test.client), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres hook engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite hook engine: %v", err) + } + + return _engine +} + +// testHook is a test helper function to create a library +// Hook type with all fields set to their zero values. +func testHook() *library.Hook { + return &library.Hook{ + ID: new(int64), + RepoID: new(int64), + BuildID: new(int64), + Number: new(int), + SourceID: new(string), + Created: new(int64), + Host: new(string), + Event: new(string), + EventAction: new(string), + Branch: new(string), + Error: new(string), + Status: new(string), + Link: new(string), + WebhookID: new(int64), + } +} + +// testRepo is a test helper function to create a library +// Repo type with all fields set to their zero values. +func testRepo() *library.Repo { + return &library.Repo{ + ID: new(int64), + UserID: new(int64), + BuildLimit: new(int64), + Timeout: new(int64), + Counter: new(int), + PipelineType: new(string), + Hash: new(string), + Org: new(string), + Name: new(string), + FullName: new(string), + Link: new(string), + Clone: new(string), + Branch: new(string), + Visibility: new(string), + PreviousName: new(string), + Private: new(bool), + Trusted: new(bool), + Active: new(bool), + AllowPull: new(bool), + AllowPush: new(bool), + AllowDeploy: new(bool), + AllowTag: new(bool), + AllowComment: new(bool), + } +} diff --git a/database/hook/index.go b/database/hook/index.go new file mode 100644 index 000000000..a2061eaf9 --- /dev/null +++ b/database/hook/index.go @@ -0,0 +1,24 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +const ( + // CreateRepoIDIndex represents a query to create an + // index on the hooks table for the repo_id column. + CreateRepoIDIndex = ` +CREATE INDEX +IF NOT EXISTS +hooks_repo_id +ON hooks (repo_id); +` +) + +// CreateHookIndexes creates the indexes for the hooks table in the database. +func (e *engine) CreateHookIndexes() error { + e.logger.Tracef("creating indexes for hooks table in the database") + + // create the repo_id column index for the hooks table + return e.client.Exec(CreateRepoIDIndex).Error +} diff --git a/database/hook/index_test.go b/database/hook/index_test.go new file mode 100644 index 000000000..06ab40a95 --- /dev/null +++ b/database/hook/index_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestHook_Engine_CreateHookIndexes(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateHookIndexes() + + if test.failure { + if err == nil { + t.Errorf("CreateHookIndexes for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateHookIndexes for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/hook/interface.go b/database/hook/interface.go new file mode 100644 index 000000000..4ed44fd84 --- /dev/null +++ b/database/hook/interface.go @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "github.com/go-vela/types/library" +) + +// HookInterface represents the Vela interface for hook +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type HookInterface interface { + // Hook Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreateHookIndexes defines a function that creates the indexes for the hooks table. + CreateHookIndexes() error + // CreateHookTable defines a function that creates the hooks table. + CreateHookTable(string) error + + // Hook Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CountHooks defines a function that gets the count of all hooks. + CountHooks() (int64, error) + // CountHooksForRepo defines a function that gets the count of hooks by repo ID. + CountHooksForRepo(*library.Repo) (int64, error) + // CreateHook defines a function that creates a new hook. + CreateHook(*library.Hook) (*library.Hook, error) + // DeleteHook defines a function that deletes an existing hook. + DeleteHook(*library.Hook) error + // GetHook defines a function that gets a hook by ID. + GetHook(int64) (*library.Hook, error) + // GetHookForRepo defines a function that gets a hook by repo ID and number. + GetHookForRepo(*library.Repo, int) (*library.Hook, error) + // LastHookForRepo defines a function that gets the last hook by repo ID. + LastHookForRepo(*library.Repo) (*library.Hook, error) + // ListHooks defines a function that gets a list of all hooks. + ListHooks() ([]*library.Hook, error) + // ListHooksForRepo defines a function that gets a list of hooks by repo ID. + ListHooksForRepo(*library.Repo, int, int) ([]*library.Hook, int64, error) + // UpdateHook defines a function that updates an existing hook. + UpdateHook(*library.Hook) (*library.Hook, error) +} diff --git a/database/hook/last_repo.go b/database/hook/last_repo.go new file mode 100644 index 000000000..22c3ef992 --- /dev/null +++ b/database/hook/last_repo.go @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "errors" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// LastHookForRepo gets the last hook by repo ID from the database. +func (e *engine) LastHookForRepo(r *library.Repo) (*library.Hook, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("getting last hook for repo %s from the database", r.GetFullName()) + + // variable to store query results + h := new(database.Hook) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableHook). + Where("repo_id = ?", r.GetID()). + Order("number DESC"). + Take(h). + Error + if err != nil { + // check if the query returned a record not found error + if errors.Is(err, gorm.ErrRecordNotFound) { + // the record will not exist if it is a new repo + return nil, nil + } + + return nil, err + } + + // return the hook + // + // https://pkg.go.dev/github.com/go-vela/types/database#Hook.ToLibrary + return h.ToLibrary(), nil +} diff --git a/database/hook/last_repo_test.go b/database/hook/last_repo_test.go new file mode 100644 index 000000000..ecd4a3eea --- /dev/null +++ b/database/hook/last_repo_test.go @@ -0,0 +1,94 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestHook_Engine_LastHookForRepo(t *testing.T) { + // setup types + _hook := testHook() + _hook.SetID(1) + _hook.SetRepoID(1) + _hook.SetBuildID(1) + _hook.SetNumber(1) + _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hook.SetWebhookID(1) + + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "source_id", "created", "host", "event", "event_action", "branch", "error", "status", "link", "webhook_id"}). + AddRow(1, 1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", "", "", 1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "hooks" WHERE repo_id = $1 ORDER BY number DESC LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateHook(_hook) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Hook + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _hook, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _hook, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.LastHookForRepo(_repo) + + if test.failure { + if err == nil { + t.Errorf("LastHookForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("LastHookForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("LastHookForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/hook/list.go b/database/hook/list.go new file mode 100644 index 000000000..1152738e6 --- /dev/null +++ b/database/hook/list.go @@ -0,0 +1,54 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListHooks gets a list of all hooks from the database. +func (e *engine) ListHooks() ([]*library.Hook, error) { + e.logger.Trace("listing all hooks from the database") + + // variables to store query results and return value + count := int64(0) + h := new([]database.Hook) + hooks := []*library.Hook{} + + // count the results + count, err := e.CountHooks() + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return hooks, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableHook). + Find(&h). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, hook := range *h { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := hook + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Hook.ToLibrary + hooks = append(hooks, tmp.ToLibrary()) + } + + return hooks, nil +} diff --git a/database/hook/list_repo.go b/database/hook/list_repo.go new file mode 100644 index 000000000..1060f5863 --- /dev/null +++ b/database/hook/list_repo.go @@ -0,0 +1,65 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListHooksForRepo gets a list of hooks by repo ID from the database. +func (e *engine) ListHooksForRepo(r *library.Repo, page, perPage int) ([]*library.Hook, int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("listing hooks for repo %s from the database", r.GetFullName()) + + // variables to store query results and return value + count := int64(0) + h := new([]database.Hook) + hooks := []*library.Hook{} + + // count the results + count, err := e.CountHooksForRepo(r) + if err != nil { + return nil, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return hooks, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableHook). + Where("repo_id = ?", r.GetID()). + Order("id DESC"). + Limit(perPage). + Offset(offset). + Find(&h). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, hook := range *h { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := hook + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Hook.ToLibrary + hooks = append(hooks, tmp.ToLibrary()) + } + + return hooks, count, nil +} diff --git a/database/hook/list_repo_test.go b/database/hook/list_repo_test.go new file mode 100644 index 000000000..c229f5674 --- /dev/null +++ b/database/hook/list_repo_test.go @@ -0,0 +1,114 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestHook_Engine_ListHooksForRepo(t *testing.T) { + // setup types + _hookOne := testHook() + _hookOne.SetID(1) + _hookOne.SetRepoID(1) + _hookOne.SetBuildID(1) + _hookOne.SetNumber(1) + _hookOne.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hookOne.SetWebhookID(1) + + _hookTwo := testHook() + _hookTwo.SetID(2) + _hookTwo.SetRepoID(1) + _hookTwo.SetBuildID(2) + _hookTwo.SetNumber(2) + _hookTwo.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hookTwo.SetWebhookID(1) + + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "hooks" WHERE repo_id = $1`).WithArgs(1).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "source_id", "created", "host", "event", "event_action", "branch", "error", "status", "link", "webhook_id"}). + AddRow(2, 1, 2, 2, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", "", "", 1). + AddRow(1, 1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", "", "", 1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "hooks" WHERE repo_id = $1 ORDER BY id DESC LIMIT 10`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateHook(_hookOne) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + _, err = _sqlite.CreateHook(_hookTwo) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Hook + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Hook{_hookTwo, _hookOne}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Hook{_hookTwo, _hookOne}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListHooksForRepo(_repo, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListHooksForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListHooksForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListHooksForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/hook/list_test.go b/database/hook/list_test.go new file mode 100644 index 000000000..e2fb33772 --- /dev/null +++ b/database/hook/list_test.go @@ -0,0 +1,107 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestHook_Engine_ListHooks(t *testing.T) { + // setup types + _hookOne := testHook() + _hookOne.SetID(1) + _hookOne.SetRepoID(1) + _hookOne.SetBuildID(1) + _hookOne.SetNumber(1) + _hookOne.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hookOne.SetWebhookID(1) + + _hookTwo := testHook() + _hookTwo.SetID(2) + _hookTwo.SetRepoID(1) + _hookTwo.SetBuildID(2) + _hookTwo.SetNumber(2) + _hookTwo.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hookTwo.SetWebhookID(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "hooks"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "source_id", "created", "host", "event", "event_action", "branch", "error", "status", "link", "webhook_id"}). + AddRow(1, 1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", "", "", 1). + AddRow(2, 1, 2, 2, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", "", "", 1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "hooks"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateHook(_hookOne) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + _, err = _sqlite.CreateHook(_hookTwo) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Hook + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Hook{_hookOne, _hookTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Hook{_hookOne, _hookTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListHooks() + + if test.failure { + if err == nil { + t.Errorf("ListHooks for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListHooks for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListHooks for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/hook/opts.go b/database/hook/opts.go new file mode 100644 index 000000000..e88e92da7 --- /dev/null +++ b/database/hook/opts.go @@ -0,0 +1,44 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Hooks. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Hooks. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the hook engine + e.client = client + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Hooks. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the hook engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Hooks. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the hook engine + e.config.SkipCreation = skipCreation + + return nil + } +} diff --git a/database/hook/opts_test.go b/database/hook/opts_test.go new file mode 100644 index 000000000..81946c8f4 --- /dev/null +++ b/database/hook/opts_test.go @@ -0,0 +1,161 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestHook_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestHook_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestHook_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} diff --git a/database/hook/table.go b/database/hook/table.go new file mode 100644 index 000000000..90419508f --- /dev/null +++ b/database/hook/table.go @@ -0,0 +1,74 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres hooks table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +hooks ( + id SERIAL PRIMARY KEY, + repo_id INTEGER, + build_id INTEGER, + number INTEGER, + source_id VARCHAR(250), + created INTEGER, + host VARCHAR(250), + event VARCHAR(250), + event_action VARCHAR(250), + branch VARCHAR(500), + error VARCHAR(500), + status VARCHAR(250), + link VARCHAR(1000), + webhook_id INTEGER, + UNIQUE(repo_id, number) +); +` + + // CreateSqliteTable represents a query to create the Sqlite hooks table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +hooks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_id INTEGER, + build_id INTEGER, + number INTEGER, + source_id TEXT, + created INTEGER, + host TEXT, + event TEXT, + event_action TEXT, + branch TEXT, + error TEXT, + status TEXT, + link TEXT, + webhook_id INTEGER, + UNIQUE(repo_id, number) +); +` +) + +// CreateHookTable creates the hooks table in the database. +func (e *engine) CreateHookTable(driver string) error { + e.logger.Tracef("creating hooks table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the hooks table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the hooks table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/hook/table_test.go b/database/hook/table_test.go new file mode 100644 index 000000000..915621718 --- /dev/null +++ b/database/hook/table_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestHook_Engine_CreateHookTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateHookTable(test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateHookTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateHookTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/hook/update.go b/database/hook/update.go new file mode 100644 index 000000000..d7741f249 --- /dev/null +++ b/database/hook/update.go @@ -0,0 +1,37 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// UpdateHook updates an existing hook in the database. +func (e *engine) UpdateHook(h *library.Hook) (*library.Hook, error) { + e.logger.WithFields(logrus.Fields{ + "hook": h.GetNumber(), + }).Tracef("updating hook %d in the database", h.GetNumber()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#HookFromLibrary + hook := database.HookFromLibrary(h) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Hook.Validate + err := hook.Validate() + if err != nil { + return nil, err + } + + result := e.client.Table(constants.TableHook).Save(hook) + + // send query to the database + return hook.ToLibrary(), result.Error +} diff --git a/database/hook/update_test.go b/database/hook/update_test.go new file mode 100644 index 000000000..cbb309897 --- /dev/null +++ b/database/hook/update_test.go @@ -0,0 +1,77 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package hook + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestHook_Engine_UpdateHook(t *testing.T) { + // setup types + _hook := testHook() + _hook.SetID(1) + _hook.SetRepoID(1) + _hook.SetBuildID(1) + _hook.SetNumber(1) + _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + _hook.SetWebhookID(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`UPDATE "hooks" +SET "repo_id"=$1,"build_id"=$2,"number"=$3,"source_id"=$4,"created"=$5,"host"=$6,"event"=$7,"event_action"=$8,"branch"=$9,"error"=$10,"status"=$11,"link"=$12,"webhook_id"=$13 +WHERE "id" = $14`). + WithArgs(1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", nil, nil, nil, nil, nil, nil, nil, nil, 1, 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateHook(_hook) + if err != nil { + t.Errorf("unable to create test hook for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err = test.database.UpdateHook(_hook) + + if test.failure { + if err == nil { + t.Errorf("UpdateHook for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateHook for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/integration_test.go b/database/integration_test.go new file mode 100644 index 000000000..30d6ff696 --- /dev/null +++ b/database/integration_test.go @@ -0,0 +1,2302 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +import ( + "context" + "os" + "reflect" + "strings" + "testing" + "time" + + "github.com/go-vela/server/database/build" + "github.com/go-vela/server/database/executable" + "github.com/go-vela/server/database/hook" + "github.com/go-vela/server/database/log" + "github.com/go-vela/server/database/pipeline" + "github.com/go-vela/server/database/repo" + "github.com/go-vela/server/database/schedule" + "github.com/go-vela/server/database/secret" + "github.com/go-vela/server/database/service" + "github.com/go-vela/server/database/step" + "github.com/go-vela/server/database/user" + "github.com/go-vela/server/database/worker" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/go-vela/types/raw" +) + +// Resources represents the object containing test resources. +type Resources struct { + Builds []*library.Build + Deployments []*library.Deployment + Executables []*library.BuildExecutable + Hooks []*library.Hook + Logs []*library.Log + Pipelines []*library.Pipeline + Repos []*library.Repo + Schedules []*library.Schedule + Secrets []*library.Secret + Services []*library.Service + Steps []*library.Step + Users []*library.User + Workers []*library.Worker +} + +func TestDatabase_Integration(t *testing.T) { + // check if we should skip the integration test + // + // https://konradreiche.com/blog/how-to-separate-integration-tests-in-go + if os.Getenv("INTEGRATION") == "" { + t.Skipf("skipping %s integration test due to environment variable constraint", t.Name()) + } + + // setup tests + tests := []struct { + name string + config *config + }{ + { + name: "postgres", + config: &config{ + Driver: "postgres", + Address: os.Getenv("POSTGRES_ADDR"), + CompressionLevel: 3, + ConnectionLife: 10 * time.Second, + ConnectionIdle: 5, + ConnectionOpen: 20, + EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + SkipCreation: false, + }, + }, + { + name: "sqlite3", + config: &config{ + Driver: "sqlite3", + Address: os.Getenv("SQLITE_ADDR"), + CompressionLevel: 3, + ConnectionLife: 10 * time.Second, + ConnectionIdle: 5, + ConnectionOpen: 20, + EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + SkipCreation: false, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create resources for testing + resources := newResources() + + db, err := New( + WithAddress(test.config.Address), + WithCompressionLevel(test.config.CompressionLevel), + WithConnectionLife(test.config.ConnectionLife), + WithConnectionIdle(test.config.ConnectionIdle), + WithConnectionOpen(test.config.ConnectionOpen), + WithDriver(test.config.Driver), + WithEncryptionKey(test.config.EncryptionKey), + WithSkipCreation(test.config.SkipCreation), + ) + if err != nil { + t.Errorf("unable to create new database engine for %s: %v", test.name, err) + } + + driver := db.Driver() + if !strings.EqualFold(driver, test.config.Driver) { + t.Errorf("Driver() is %v, want %v", driver, test.config.Driver) + } + + err = db.Ping() + if err != nil { + t.Errorf("unable to ping database engine for %s: %v", test.name, err) + } + + t.Run("test_builds", func(t *testing.T) { testBuilds(t, db, resources) }) + + t.Run("test_executables", func(t *testing.T) { testExecutables(t, db, resources) }) + + t.Run("test_hooks", func(t *testing.T) { testHooks(t, db, resources) }) + + t.Run("test_logs", func(t *testing.T) { testLogs(t, db, resources) }) + + t.Run("test_pipelines", func(t *testing.T) { testPipelines(t, db, resources) }) + + t.Run("test_repos", func(t *testing.T) { testRepos(t, db, resources) }) + + t.Run("test_schedules", func(t *testing.T) { testSchedules(t, db, resources) }) + + t.Run("test_secrets", func(t *testing.T) { testSecrets(t, db, resources) }) + + t.Run("test_services", func(t *testing.T) { testServices(t, db, resources) }) + + t.Run("test_steps", func(t *testing.T) { testSteps(t, db, resources) }) + + t.Run("test_users", func(t *testing.T) { testUsers(t, db, resources) }) + + t.Run("test_workers", func(t *testing.T) { testWorkers(t, db, resources) }) + + err = db.Close() + if err != nil { + t.Errorf("unable to close database engine for %s: %v", test.name, err) + } + }) + } +} + +func testBuilds(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for builds + methods := make(map[string]bool) + // capture the element type of the build interface + element := reflect.TypeOf(new(build.BuildInterface)).Elem() + // iterate through all methods found in the build interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for builds + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + // create the repos for build related functions + for _, repo := range resources.Repos { + _, err := db.CreateRepo(context.TODO(), repo) + if err != nil { + t.Errorf("unable to create repo %d: %v", repo.GetID(), err) + } + } + + buildOne := new(library.BuildQueue) + buildOne.SetCreated(1563474076) + buildOne.SetFullName("github/octocat") + buildOne.SetNumber(1) + buildOne.SetStatus("running") + + buildTwo := new(library.BuildQueue) + buildTwo.SetCreated(1563474076) + buildTwo.SetFullName("github/octocat") + buildTwo.SetNumber(2) + buildTwo.SetStatus("running") + + queueBuilds := []*library.BuildQueue{buildOne, buildTwo} + + // create the builds + for _, build := range resources.Builds { + _, err := db.CreateBuild(context.TODO(), build) + if err != nil { + t.Errorf("unable to create build %d: %v", build.GetID(), err) + } + } + methods["CreateBuild"] = true + + // count the builds + count, err := db.CountBuilds(context.TODO()) + if err != nil { + t.Errorf("unable to count builds: %v", err) + } + if int(count) != len(resources.Builds) { + t.Errorf("CountBuilds() is %v, want %v", count, len(resources.Builds)) + } + methods["CountBuilds"] = true + + // count the builds for a deployment + count, err = db.CountBuildsForDeployment(context.TODO(), resources.Deployments[0], nil) + if err != nil { + t.Errorf("unable to count builds for deployment %d: %v", resources.Deployments[0].GetID(), err) + } + if int(count) != len(resources.Builds) { + t.Errorf("CountBuildsForDeployment() is %v, want %v", count, len(resources.Builds)) + } + methods["CountBuildsForDeployment"] = true + + // count the builds for an org + count, err = db.CountBuildsForOrg(context.TODO(), resources.Repos[0].GetOrg(), nil) + if err != nil { + t.Errorf("unable to count builds for org %s: %v", resources.Repos[0].GetOrg(), err) + } + if int(count) != len(resources.Builds) { + t.Errorf("CountBuildsForOrg() is %v, want %v", count, len(resources.Builds)) + } + methods["CountBuildsForOrg"] = true + + // count the builds for a repo + count, err = db.CountBuildsForRepo(context.TODO(), resources.Repos[0], nil) + if err != nil { + t.Errorf("unable to count builds for repo %d: %v", resources.Repos[0].GetID(), err) + } + if int(count) != len(resources.Builds) { + t.Errorf("CountBuildsForRepo() is %v, want %v", count, len(resources.Builds)) + } + methods["CountBuildsForRepo"] = true + + // count the builds for a status + count, err = db.CountBuildsForStatus(context.TODO(), "running", nil) + if err != nil { + t.Errorf("unable to count builds for status %s: %v", "running", err) + } + if int(count) != len(resources.Builds) { + t.Errorf("CountBuildsForStatus() is %v, want %v", count, len(resources.Builds)) + } + methods["CountBuildsForStatus"] = true + + // list the builds + list, err := db.ListBuilds(context.TODO()) + if err != nil { + t.Errorf("unable to list builds: %v", err) + } + if !reflect.DeepEqual(list, resources.Builds) { + t.Errorf("ListBuilds() is %v, want %v", list, resources.Builds) + } + methods["ListBuilds"] = true + + // list the builds for a deployment + list, count, err = db.ListBuildsForDeployment(context.TODO(), resources.Deployments[0], nil, 1, 10) + if err != nil { + t.Errorf("unable to list builds for deployment %d: %v", resources.Deployments[0].GetID(), err) + } + if int(count) != len(resources.Builds) { + t.Errorf("ListBuildsForDeployment() is %v, want %v", count, len(resources.Builds)) + } + if !reflect.DeepEqual(list, []*library.Build{resources.Builds[1], resources.Builds[0]}) { + t.Errorf("ListBuildsForDeployment() is %v, want %v", list, []*library.Build{resources.Builds[1], resources.Builds[0]}) + } + methods["ListBuildsForDeployment"] = true + + // list the builds for an org + list, count, err = db.ListBuildsForOrg(context.TODO(), resources.Repos[0].GetOrg(), nil, 1, 10) + if err != nil { + t.Errorf("unable to list builds for org %s: %v", resources.Repos[0].GetOrg(), err) + } + if int(count) != len(resources.Builds) { + t.Errorf("ListBuildsForOrg() is %v, want %v", count, len(resources.Builds)) + } + if !reflect.DeepEqual(list, resources.Builds) { + t.Errorf("ListBuildsForOrg() is %v, want %v", list, resources.Builds) + } + methods["ListBuildsForOrg"] = true + + // list the builds for a repo + list, count, err = db.ListBuildsForRepo(context.TODO(), resources.Repos[0], nil, time.Now().UTC().Unix(), 0, 1, 10) + if err != nil { + t.Errorf("unable to list builds for repo %d: %v", resources.Repos[0].GetID(), err) + } + if int(count) != len(resources.Builds) { + t.Errorf("ListBuildsForRepo() is %v, want %v", count, len(resources.Builds)) + } + if !reflect.DeepEqual(list, []*library.Build{resources.Builds[1], resources.Builds[0]}) { + t.Errorf("ListBuildsForRepo() is %v, want %v", list, []*library.Build{resources.Builds[1], resources.Builds[0]}) + } + methods["ListBuildsForRepo"] = true + + // list the pending and running builds + queueList, err := db.ListPendingAndRunningBuilds(context.TODO(), "0") + if err != nil { + t.Errorf("unable to list pending and running builds: %v", err) + } + if !reflect.DeepEqual(queueList, queueBuilds) { + t.Errorf("ListPendingAndRunningBuilds() is %v, want %v", queueList, queueBuilds) + } + methods["ListPendingAndRunningBuilds"] = true + + // lookup the last build by repo + got, err := db.LastBuildForRepo(context.TODO(), resources.Repos[0], "main") + if err != nil { + t.Errorf("unable to get last build for repo %d: %v", resources.Repos[0].GetID(), err) + } + if !reflect.DeepEqual(got, resources.Builds[1]) { + t.Errorf("LastBuildForRepo() is %v, want %v", got, resources.Builds[1]) + } + methods["LastBuildForRepo"] = true + + // lookup the builds by repo and number + for _, build := range resources.Builds { + repo := resources.Repos[build.GetRepoID()-1] + got, err = db.GetBuildForRepo(context.TODO(), repo, build.GetNumber()) + if err != nil { + t.Errorf("unable to get build %d for repo %d: %v", build.GetID(), repo.GetID(), err) + } + if !reflect.DeepEqual(got, build) { + t.Errorf("GetBuildForRepo() is %v, want %v", got, build) + } + } + methods["GetBuildForRepo"] = true + + // clean the builds + count, err = db.CleanBuilds(context.TODO(), "integration testing", 1563474090) + if err != nil { + t.Errorf("unable to clean builds: %v", err) + } + if int(count) != len(resources.Builds) { + t.Errorf("CleanBuilds() is %v, want %v", count, len(resources.Builds)) + } + methods["CleanBuilds"] = true + + // update the builds + for _, build := range resources.Builds { + build.SetStatus("success") + _, err = db.UpdateBuild(context.TODO(), build) + if err != nil { + t.Errorf("unable to update build %d: %v", build.GetID(), err) + } + + // lookup the build by ID + got, err = db.GetBuild(context.TODO(), build.GetID()) + if err != nil { + t.Errorf("unable to get build %d by ID: %v", build.GetID(), err) + } + if !reflect.DeepEqual(got, build) { + t.Errorf("GetBuild() is %v, want %v", got, build) + } + } + methods["UpdateBuild"] = true + methods["GetBuild"] = true + + // delete the builds + for _, build := range resources.Builds { + err = db.DeleteBuild(context.TODO(), build) + if err != nil { + t.Errorf("unable to delete build %d: %v", build.GetID(), err) + } + } + methods["DeleteBuild"] = true + + // delete the repos for build related functions + for _, repo := range resources.Repos { + err = db.DeleteRepo(context.TODO(), repo) + if err != nil { + t.Errorf("unable to delete repo %d: %v", repo.GetID(), err) + } + } + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for builds", method) + } + } +} + +func testExecutables(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for pipelines + methods := make(map[string]bool) + // capture the element type of the pipeline interface + element := reflect.TypeOf(new(executable.BuildExecutableInterface)).Elem() + // iterate through all methods found in the pipeline interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for pipelines + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + // create the pipelines + for _, executable := range resources.Executables { + err := db.CreateBuildExecutable(context.TODO(), executable) + if err != nil { + t.Errorf("unable to create executable %d: %v", executable.GetID(), err) + } + } + methods["CreateBuildExecutable"] = true + + // pop executables for builds + for _, executable := range resources.Executables { + got, err := db.PopBuildExecutable(context.TODO(), executable.GetBuildID()) + if err != nil { + t.Errorf("unable to get executable %d for build %d: %v", executable.GetID(), executable.GetBuildID(), err) + } + if !reflect.DeepEqual(got, executable) { + t.Errorf("PopBuildExecutable() is %v, want %v", got, executable) + } + } + methods["PopBuildExecutable"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for pipelines", method) + } + } +} + +func testHooks(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for hooks + methods := make(map[string]bool) + // capture the element type of the hook interface + element := reflect.TypeOf(new(hook.HookInterface)).Elem() + // iterate through all methods found in the hook interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for hooks + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + // create the hooks + for _, hook := range resources.Hooks { + _, err := db.CreateHook(hook) + if err != nil { + t.Errorf("unable to create hook %d: %v", hook.GetID(), err) + } + } + methods["CreateHook"] = true + + // count the hooks + count, err := db.CountHooks() + if err != nil { + t.Errorf("unable to count hooks: %v", err) + } + if int(count) != len(resources.Hooks) { + t.Errorf("CountHooks() is %v, want %v", count, len(resources.Hooks)) + } + methods["CountHooks"] = true + + // count the hooks for a repo + count, err = db.CountHooksForRepo(resources.Repos[0]) + if err != nil { + t.Errorf("unable to count hooks for repo %d: %v", resources.Repos[0].GetID(), err) + } + if int(count) != len(resources.Builds) { + t.Errorf("CountHooksForRepo() is %v, want %v", count, len(resources.Builds)) + } + methods["CountHooksForRepo"] = true + + // list the hooks + list, err := db.ListHooks() + if err != nil { + t.Errorf("unable to list hooks: %v", err) + } + if !reflect.DeepEqual(list, resources.Hooks) { + t.Errorf("ListHooks() is %v, want %v", list, resources.Hooks) + } + methods["ListHooks"] = true + + // list the hooks for a repo + list, count, err = db.ListHooksForRepo(resources.Repos[0], 1, 10) + if err != nil { + t.Errorf("unable to list hooks for repo %d: %v", resources.Repos[0].GetID(), err) + } + if int(count) != len(resources.Hooks) { + t.Errorf("ListHooksForRepo() is %v, want %v", count, len(resources.Hooks)) + } + if !reflect.DeepEqual(list, []*library.Hook{resources.Hooks[1], resources.Hooks[0]}) { + t.Errorf("ListHooksForRepo() is %v, want %v", list, []*library.Hook{resources.Hooks[1], resources.Hooks[0]}) + } + methods["ListHooksForRepo"] = true + + // lookup the last build by repo + got, err := db.LastHookForRepo(resources.Repos[0]) + if err != nil { + t.Errorf("unable to get last hook for repo %d: %v", resources.Repos[0].GetID(), err) + } + if !reflect.DeepEqual(got, resources.Hooks[1]) { + t.Errorf("LastHookForRepo() is %v, want %v", got, resources.Hooks[1]) + } + methods["LastHookForRepo"] = true + + // lookup the hooks by name + for _, hook := range resources.Hooks { + repo := resources.Repos[hook.GetRepoID()-1] + got, err = db.GetHookForRepo(repo, hook.GetNumber()) + if err != nil { + t.Errorf("unable to get hook %d for repo %d: %v", hook.GetID(), repo.GetID(), err) + } + if !reflect.DeepEqual(got, hook) { + t.Errorf("GetHookForRepo() is %v, want %v", got, hook) + } + } + methods["GetHookForRepo"] = true + + // update the hooks + for _, hook := range resources.Hooks { + hook.SetStatus("success") + _, err = db.UpdateHook(hook) + if err != nil { + t.Errorf("unable to update hook %d: %v", hook.GetID(), err) + } + + // lookup the hook by ID + got, err = db.GetHook(hook.GetID()) + if err != nil { + t.Errorf("unable to get hook %d by ID: %v", hook.GetID(), err) + } + if !reflect.DeepEqual(got, hook) { + t.Errorf("GetHook() is %v, want %v", got, hook) + } + } + methods["UpdateHook"] = true + methods["GetHook"] = true + + // delete the hooks + for _, hook := range resources.Hooks { + err = db.DeleteHook(hook) + if err != nil { + t.Errorf("unable to delete hook %d: %v", hook.GetID(), err) + } + } + methods["DeleteHook"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for hooks", method) + } + } +} + +func testLogs(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for logs + methods := make(map[string]bool) + // capture the element type of the log interface + element := reflect.TypeOf(new(log.LogInterface)).Elem() + // iterate through all methods found in the log interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for logs + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + // create the logs + for _, log := range resources.Logs { + err := db.CreateLog(log) + if err != nil { + t.Errorf("unable to create log %d: %v", log.GetID(), err) + } + } + methods["CreateLog"] = true + + // count the logs + count, err := db.CountLogs() + if err != nil { + t.Errorf("unable to count logs: %v", err) + } + if int(count) != len(resources.Logs) { + t.Errorf("CountLogs() is %v, want %v", count, len(resources.Logs)) + } + methods["CountLogs"] = true + + // count the logs for a build + count, err = db.CountLogsForBuild(resources.Builds[0]) + if err != nil { + t.Errorf("unable to count logs for build %d: %v", resources.Builds[0].GetID(), err) + } + if int(count) != len(resources.Logs) { + t.Errorf("CountLogs() is %v, want %v", count, len(resources.Logs)) + } + methods["CountLogsForBuild"] = true + + // list the logs + list, err := db.ListLogs() + if err != nil { + t.Errorf("unable to list logs: %v", err) + } + if !reflect.DeepEqual(list, resources.Logs) { + t.Errorf("ListLogs() is %v, want %v", list, resources.Logs) + } + methods["ListLogs"] = true + + // list the logs for a build + list, count, err = db.ListLogsForBuild(resources.Builds[0], 1, 10) + if err != nil { + t.Errorf("unable to list logs for build %d: %v", resources.Builds[0].GetID(), err) + } + if int(count) != len(resources.Logs) { + t.Errorf("ListLogsForBuild() is %v, want %v", count, len(resources.Logs)) + } + if !reflect.DeepEqual(list, resources.Logs) { + t.Errorf("ListLogsForBuild() is %v, want %v", list, resources.Logs) + } + methods["ListLogsForBuild"] = true + + // lookup the logs by service + for _, log := range []*library.Log{resources.Logs[0], resources.Logs[1]} { + service := resources.Services[log.GetServiceID()-1] + got, err := db.GetLogForService(service) + if err != nil { + t.Errorf("unable to get log %d for service %d: %v", log.GetID(), service.GetID(), err) + } + if !reflect.DeepEqual(got, log) { + t.Errorf("GetLogForService() is %v, want %v", got, log) + } + } + methods["GetLogForService"] = true + + // lookup the logs by service + for _, log := range []*library.Log{resources.Logs[2], resources.Logs[3]} { + step := resources.Steps[log.GetStepID()-1] + got, err := db.GetLogForStep(step) + if err != nil { + t.Errorf("unable to get log %d for step %d: %v", log.GetID(), step.GetID(), err) + } + if !reflect.DeepEqual(got, log) { + t.Errorf("GetLogForStep() is %v, want %v", got, log) + } + } + methods["GetLogForStep"] = true + + // update the logs + for _, log := range resources.Logs { + log.SetData([]byte("bar")) + err = db.UpdateLog(log) + if err != nil { + t.Errorf("unable to update log %d: %v", log.GetID(), err) + } + + // lookup the log by ID + got, err := db.GetLog(log.GetID()) + if err != nil { + t.Errorf("unable to get log %d by ID: %v", log.GetID(), err) + } + if !reflect.DeepEqual(got, log) { + t.Errorf("GetLog() is %v, want %v", got, log) + } + } + methods["UpdateLog"] = true + methods["GetLog"] = true + + // delete the logs + for _, log := range resources.Logs { + err = db.DeleteLog(log) + if err != nil { + t.Errorf("unable to delete log %d: %v", log.GetID(), err) + } + } + methods["DeleteLog"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for logs", method) + } + } +} + +func testPipelines(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for pipelines + methods := make(map[string]bool) + // capture the element type of the pipeline interface + element := reflect.TypeOf(new(pipeline.PipelineInterface)).Elem() + // iterate through all methods found in the pipeline interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for pipelines + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + // create the pipelines + for _, pipeline := range resources.Pipelines { + _, err := db.CreatePipeline(context.TODO(), pipeline) + if err != nil { + t.Errorf("unable to create pipeline %d: %v", pipeline.GetID(), err) + } + } + methods["CreatePipeline"] = true + + // count the pipelines + count, err := db.CountPipelines(context.TODO()) + if err != nil { + t.Errorf("unable to count pipelines: %v", err) + } + if int(count) != len(resources.Pipelines) { + t.Errorf("CountPipelines() is %v, want %v", count, len(resources.Pipelines)) + } + methods["CountPipelines"] = true + + // count the pipelines for a repo + count, err = db.CountPipelinesForRepo(context.TODO(), resources.Repos[0]) + if err != nil { + t.Errorf("unable to count pipelines for repo %d: %v", resources.Repos[0].GetID(), err) + } + if int(count) != len(resources.Pipelines) { + t.Errorf("CountPipelinesForRepo() is %v, want %v", count, len(resources.Pipelines)) + } + methods["CountPipelinesForRepo"] = true + + // list the pipelines + list, err := db.ListPipelines(context.TODO()) + if err != nil { + t.Errorf("unable to list pipelines: %v", err) + } + if !reflect.DeepEqual(list, resources.Pipelines) { + t.Errorf("ListPipelines() is %v, want %v", list, resources.Pipelines) + } + methods["ListPipelines"] = true + + // list the pipelines for a repo + list, count, err = db.ListPipelinesForRepo(context.TODO(), resources.Repos[0], 1, 10) + if err != nil { + t.Errorf("unable to list pipelines for repo %d: %v", resources.Repos[0].GetID(), err) + } + if int(count) != len(resources.Pipelines) { + t.Errorf("ListPipelinesForRepo() is %v, want %v", count, len(resources.Pipelines)) + } + if !reflect.DeepEqual(list, resources.Pipelines) { + t.Errorf("ListPipelines() is %v, want %v", list, resources.Pipelines) + } + methods["ListPipelinesForRepo"] = true + + // lookup the pipelines by name + for _, pipeline := range resources.Pipelines { + repo := resources.Repos[pipeline.GetRepoID()-1] + got, err := db.GetPipelineForRepo(context.TODO(), pipeline.GetCommit(), repo) + if err != nil { + t.Errorf("unable to get pipeline %d for repo %d: %v", pipeline.GetID(), repo.GetID(), err) + } + if !reflect.DeepEqual(got, pipeline) { + t.Errorf("GetPipelineForRepo() is %v, want %v", got, pipeline) + } + } + methods["GetPipelineForRepo"] = true + + // update the pipelines + for _, pipeline := range resources.Pipelines { + pipeline.SetVersion("2") + _, err = db.UpdatePipeline(context.TODO(), pipeline) + if err != nil { + t.Errorf("unable to update pipeline %d: %v", pipeline.GetID(), err) + } + + // lookup the pipeline by ID + got, err := db.GetPipeline(context.TODO(), pipeline.GetID()) + if err != nil { + t.Errorf("unable to get pipeline %d by ID: %v", pipeline.GetID(), err) + } + if !reflect.DeepEqual(got, pipeline) { + t.Errorf("GetPipeline() is %v, want %v", got, pipeline) + } + } + methods["UpdatePipeline"] = true + methods["GetPipeline"] = true + + // delete the pipelines + for _, pipeline := range resources.Pipelines { + err = db.DeletePipeline(context.TODO(), pipeline) + if err != nil { + t.Errorf("unable to delete pipeline %d: %v", pipeline.GetID(), err) + } + } + methods["DeletePipeline"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for pipelines", method) + } + } +} + +func testRepos(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for repos + methods := make(map[string]bool) + // capture the element type of the repo interface + element := reflect.TypeOf(new(repo.RepoInterface)).Elem() + // iterate through all methods found in the repo interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for repos + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + // create the repos + for _, repo := range resources.Repos { + _, err := db.CreateRepo(context.TODO(), repo) + if err != nil { + t.Errorf("unable to create repo %d: %v", repo.GetID(), err) + } + } + methods["CreateRepo"] = true + + // count the repos + count, err := db.CountRepos(context.TODO()) + if err != nil { + t.Errorf("unable to count repos: %v", err) + } + if int(count) != len(resources.Repos) { + t.Errorf("CountRepos() is %v, want %v", count, len(resources.Repos)) + } + methods["CountRepos"] = true + + // count the repos for an org + count, err = db.CountReposForOrg(context.TODO(), resources.Repos[0].GetOrg(), nil) + if err != nil { + t.Errorf("unable to count repos for org %s: %v", resources.Repos[0].GetOrg(), err) + } + if int(count) != len(resources.Repos) { + t.Errorf("CountReposForOrg() is %v, want %v", count, len(resources.Repos)) + } + methods["CountReposForOrg"] = true + + // count the repos for a user + count, err = db.CountReposForUser(context.TODO(), resources.Users[0], nil) + if err != nil { + t.Errorf("unable to count repos for user %d: %v", resources.Users[0].GetID(), err) + } + if int(count) != len(resources.Repos) { + t.Errorf("CountReposForUser() is %v, want %v", count, len(resources.Repos)) + } + methods["CountReposForUser"] = true + + // list the repos + list, err := db.ListRepos(context.TODO()) + if err != nil { + t.Errorf("unable to list repos: %v", err) + } + if !reflect.DeepEqual(list, resources.Repos) { + t.Errorf("ListRepos() is %v, want %v", list, resources.Repos) + } + methods["ListRepos"] = true + + // list the repos for an org + list, count, err = db.ListReposForOrg(context.TODO(), resources.Repos[0].GetOrg(), "name", nil, 1, 10) + if err != nil { + t.Errorf("unable to list repos for org %s: %v", resources.Repos[0].GetOrg(), err) + } + if int(count) != len(resources.Repos) { + t.Errorf("ListReposForOrg() is %v, want %v", count, len(resources.Repos)) + } + if !reflect.DeepEqual(list, resources.Repos) { + t.Errorf("ListReposForOrg() is %v, want %v", list, resources.Repos) + } + methods["ListReposForOrg"] = true + + // list the repos for a user + list, count, err = db.ListReposForUser(context.TODO(), resources.Users[0], "name", nil, 1, 10) + if err != nil { + t.Errorf("unable to list repos for user %d: %v", resources.Users[0].GetID(), err) + } + if int(count) != len(resources.Repos) { + t.Errorf("ListReposForUser() is %v, want %v", count, len(resources.Repos)) + } + if !reflect.DeepEqual(list, resources.Repos) { + t.Errorf("ListReposForUser() is %v, want %v", list, resources.Repos) + } + methods["ListReposForUser"] = true + + // lookup the repos by name + for _, repo := range resources.Repos { + got, err := db.GetRepoForOrg(context.TODO(), repo.GetOrg(), repo.GetName()) + if err != nil { + t.Errorf("unable to get repo %d by org: %v", repo.GetID(), err) + } + if !reflect.DeepEqual(got, repo) { + t.Errorf("GetRepoForOrg() is %v, want %v", got, repo) + } + } + methods["GetRepoForOrg"] = true + + // update the repos + for _, repo := range resources.Repos { + repo.SetActive(false) + _, err = db.UpdateRepo(context.TODO(), repo) + if err != nil { + t.Errorf("unable to update repo %d: %v", repo.GetID(), err) + } + + // lookup the repo by ID + got, err := db.GetRepo(context.TODO(), repo.GetID()) + if err != nil { + t.Errorf("unable to get repo %d by ID: %v", repo.GetID(), err) + } + if !reflect.DeepEqual(got, repo) { + t.Errorf("GetRepo() is %v, want %v", got, repo) + } + } + methods["UpdateRepo"] = true + methods["GetRepo"] = true + + // delete the repos + for _, repo := range resources.Repos { + err = db.DeleteRepo(context.TODO(), repo) + if err != nil { + t.Errorf("unable to delete repo %d: %v", repo.GetID(), err) + } + } + methods["DeleteRepo"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for repos", method) + } + } +} + +func testSchedules(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for schedules + methods := make(map[string]bool) + // capture the element type of the schedule interface + element := reflect.TypeOf(new(schedule.ScheduleInterface)).Elem() + // iterate through all methods found in the schedule interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for schedules + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + ctx := context.TODO() + + // create the schedules + for _, schedule := range resources.Schedules { + _, err := db.CreateSchedule(ctx, schedule) + if err != nil { + t.Errorf("unable to create schedule %d: %v", schedule.GetID(), err) + } + } + methods["CreateSchedule"] = true + + // count the schedules + count, err := db.CountSchedules(ctx) + if err != nil { + t.Errorf("unable to count schedules: %v", err) + } + if int(count) != len(resources.Schedules) { + t.Errorf("CountSchedules() is %v, want %v", count, len(resources.Schedules)) + } + methods["CountSchedules"] = true + + // count the schedules for a repo + count, err = db.CountSchedulesForRepo(ctx, resources.Repos[0]) + if err != nil { + t.Errorf("unable to count schedules for repo %d: %v", resources.Repos[0].GetID(), err) + } + if int(count) != len(resources.Schedules) { + t.Errorf("CountSchedulesForRepo() is %v, want %v", count, len(resources.Schedules)) + } + methods["CountSchedulesForRepo"] = true + + // list the schedules + list, err := db.ListSchedules(ctx) + if err != nil { + t.Errorf("unable to list schedules: %v", err) + } + if !reflect.DeepEqual(list, resources.Schedules) { + t.Errorf("ListSchedules() is %v, want %v", list, resources.Schedules) + } + methods["ListSchedules"] = true + + // list the active schedules + list, err = db.ListActiveSchedules(ctx) + if err != nil { + t.Errorf("unable to list schedules: %v", err) + } + if !reflect.DeepEqual(list, resources.Schedules) { + t.Errorf("ListActiveSchedules() is %v, want %v", list, resources.Schedules) + } + methods["ListActiveSchedules"] = true + + // list the schedules for a repo + list, count, err = db.ListSchedulesForRepo(ctx, resources.Repos[0], 1, 10) + if err != nil { + t.Errorf("unable to count schedules for repo %d: %v", resources.Repos[0].GetID(), err) + } + if int(count) != len(resources.Schedules) { + t.Errorf("ListSchedulesForRepo() is %v, want %v", count, len(resources.Schedules)) + } + if !reflect.DeepEqual(list, []*library.Schedule{resources.Schedules[1], resources.Schedules[0]}) { + t.Errorf("ListSchedulesForRepo() is %v, want %v", list, []*library.Schedule{resources.Schedules[1], resources.Schedules[0]}) + } + methods["ListSchedulesForRepo"] = true + + // lookup the schedules by name + for _, schedule := range resources.Schedules { + repo := resources.Repos[schedule.GetRepoID()-1] + got, err := db.GetScheduleForRepo(ctx, repo, schedule.GetName()) + if err != nil { + t.Errorf("unable to get schedule %d for repo %d: %v", schedule.GetID(), repo.GetID(), err) + } + if !reflect.DeepEqual(got, schedule) { + t.Errorf("GetScheduleForRepo() is %v, want %v", got, schedule) + } + } + methods["GetScheduleForRepo"] = true + + // update the schedules + for _, schedule := range resources.Schedules { + schedule.SetUpdatedAt(time.Now().UTC().Unix()) + got, err := db.UpdateSchedule(ctx, schedule, true) + if err != nil { + t.Errorf("unable to update schedule %d: %v", schedule.GetID(), err) + } + + if !reflect.DeepEqual(got, schedule) { + t.Errorf("GetSchedule() is %v, want %v", got, schedule) + } + } + methods["UpdateSchedule"] = true + methods["GetSchedule"] = true + + // delete the schedules + for _, schedule := range resources.Schedules { + err = db.DeleteSchedule(ctx, schedule) + if err != nil { + t.Errorf("unable to delete schedule %d: %v", schedule.GetID(), err) + } + } + methods["DeleteSchedule"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for schedules", method) + } + } +} + +func testSecrets(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for secrets + methods := make(map[string]bool) + // capture the element type of the secret interface + element := reflect.TypeOf(new(secret.SecretInterface)).Elem() + // iterate through all methods found in the secret interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for secrets + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + // create the secrets + for _, secret := range resources.Secrets { + _, err := db.CreateSecret(secret) + if err != nil { + t.Errorf("unable to create secret %d: %v", secret.GetID(), err) + } + } + methods["CreateSecret"] = true + + // count the secrets + count, err := db.CountSecrets() + if err != nil { + t.Errorf("unable to count secrets: %v", err) + } + if int(count) != len(resources.Secrets) { + t.Errorf("CountSecrets() is %v, want %v", count, len(resources.Secrets)) + } + methods["CountSecrets"] = true + + for _, secret := range resources.Secrets { + switch secret.GetType() { + case constants.SecretOrg: + // count the secrets for an org + count, err = db.CountSecretsForOrg(secret.GetOrg(), nil) + if err != nil { + t.Errorf("unable to count secrets for org %s: %v", secret.GetOrg(), err) + } + if int(count) != 1 { + t.Errorf("CountSecretsForOrg() is %v, want %v", count, 1) + } + methods["CountSecretsForOrg"] = true + case constants.SecretRepo: + // count the secrets for a repo + count, err = db.CountSecretsForRepo(resources.Repos[0], nil) + if err != nil { + t.Errorf("unable to count secrets for repo %d: %v", resources.Repos[0].GetID(), err) + } + if int(count) != 1 { + t.Errorf("CountSecretsForRepo() is %v, want %v", count, 1) + } + methods["CountSecretsForRepo"] = true + case constants.SecretShared: + // count the secrets for a team + count, err = db.CountSecretsForTeam(secret.GetOrg(), secret.GetTeam(), nil) + if err != nil { + t.Errorf("unable to count secrets for team %s: %v", secret.GetTeam(), err) + } + if int(count) != 1 { + t.Errorf("CountSecretsForTeam() is %v, want %v", count, 1) + } + methods["CountSecretsForTeam"] = true + + // count the secrets for a list of teams + count, err = db.CountSecretsForTeams(secret.GetOrg(), []string{secret.GetTeam()}, nil) + if err != nil { + t.Errorf("unable to count secrets for teams %s: %v", []string{secret.GetTeam()}, err) + } + if int(count) != 1 { + t.Errorf("CountSecretsForTeams() is %v, want %v", count, 1) + } + methods["CountSecretsForTeams"] = true + default: + t.Errorf("unsupported type %s for secret %d", secret.GetType(), secret.GetID()) + } + } + + // list the secrets + list, err := db.ListSecrets() + if err != nil { + t.Errorf("unable to list secrets: %v", err) + } + if !reflect.DeepEqual(list, resources.Secrets) { + t.Errorf("ListSecrets() is %v, want %v", list, resources.Secrets) + } + methods["ListSecrets"] = true + + for _, secret := range resources.Secrets { + switch secret.GetType() { + case constants.SecretOrg: + // list the secrets for an org + list, count, err = db.ListSecretsForOrg(secret.GetOrg(), nil, 1, 10) + if err != nil { + t.Errorf("unable to list secrets for org %s: %v", secret.GetOrg(), err) + } + if int(count) != 1 { + t.Errorf("ListSecretsForOrg() is %v, want %v", count, 1) + } + if !reflect.DeepEqual(list, []*library.Secret{secret}) { + t.Errorf("ListSecretsForOrg() is %v, want %v", list, []*library.Secret{secret}) + } + methods["ListSecretsForOrg"] = true + case constants.SecretRepo: + // list the secrets for a repo + list, count, err = db.ListSecretsForRepo(resources.Repos[0], nil, 1, 10) + if err != nil { + t.Errorf("unable to list secrets for repo %d: %v", resources.Repos[0].GetID(), err) + } + if int(count) != 1 { + t.Errorf("ListSecretsForRepo() is %v, want %v", count, 1) + } + if !reflect.DeepEqual(list, []*library.Secret{secret}) { + t.Errorf("ListSecretsForRepo() is %v, want %v", list, []*library.Secret{secret}) + } + methods["ListSecretsForRepo"] = true + case constants.SecretShared: + // list the secrets for a team + list, count, err = db.ListSecretsForTeam(secret.GetOrg(), secret.GetTeam(), nil, 1, 10) + if err != nil { + t.Errorf("unable to list secrets for team %s: %v", secret.GetTeam(), err) + } + if int(count) != 1 { + t.Errorf("ListSecretsForTeam() is %v, want %v", count, 1) + } + if !reflect.DeepEqual(list, []*library.Secret{secret}) { + t.Errorf("ListSecretsForTeam() is %v, want %v", list, []*library.Secret{secret}) + } + methods["ListSecretsForTeam"] = true + + // list the secrets for a list of teams + list, count, err = db.ListSecretsForTeams(secret.GetOrg(), []string{secret.GetTeam()}, nil, 1, 10) + if err != nil { + t.Errorf("unable to list secrets for teams %s: %v", []string{secret.GetTeam()}, err) + } + if int(count) != 1 { + t.Errorf("ListSecretsForTeams() is %v, want %v", count, 1) + } + if !reflect.DeepEqual(list, []*library.Secret{secret}) { + t.Errorf("ListSecretsForTeams() is %v, want %v", list, []*library.Secret{secret}) + } + methods["ListSecretsForTeams"] = true + default: + t.Errorf("unsupported type %s for secret %d", secret.GetType(), secret.GetID()) + } + } + + for _, secret := range resources.Secrets { + switch secret.GetType() { + case constants.SecretOrg: + // lookup the secret by org + got, err := db.GetSecretForOrg(secret.GetOrg(), secret.GetName()) + if err != nil { + t.Errorf("unable to get secret %d for org %s: %v", secret.GetID(), secret.GetOrg(), err) + } + if !reflect.DeepEqual(got, secret) { + t.Errorf("GetSecretForOrg() is %v, want %v", got, secret) + } + methods["GetSecretForOrg"] = true + case constants.SecretRepo: + // lookup the secret by repo + got, err := db.GetSecretForRepo(secret.GetName(), resources.Repos[0]) + if err != nil { + t.Errorf("unable to get secret %d for repo %d: %v", secret.GetID(), resources.Repos[0].GetID(), err) + } + if !reflect.DeepEqual(got, secret) { + t.Errorf("GetSecretForRepo() is %v, want %v", got, secret) + } + methods["GetSecretForRepo"] = true + case constants.SecretShared: + // lookup the secret by team + got, err := db.GetSecretForTeam(secret.GetOrg(), secret.GetTeam(), secret.GetName()) + if err != nil { + t.Errorf("unable to get secret %d for team %s: %v", secret.GetID(), secret.GetTeam(), err) + } + if !reflect.DeepEqual(got, secret) { + t.Errorf("GetSecretForTeam() is %v, want %v", got, secret) + } + methods["GetSecretForTeam"] = true + default: + t.Errorf("unsupported type %s for secret %d", secret.GetType(), secret.GetID()) + } + } + + // update the secrets + for _, secret := range resources.Secrets { + secret.SetUpdatedAt(time.Now().UTC().Unix()) + got, err := db.UpdateSecret(secret) + if err != nil { + t.Errorf("unable to update secret %d: %v", secret.GetID(), err) + } + + if !reflect.DeepEqual(got, secret) { + t.Errorf("GetSecret() is %v, want %v", got, secret) + } + } + methods["UpdateSecret"] = true + methods["GetSecret"] = true + + // delete the secrets + for _, secret := range resources.Secrets { + err = db.DeleteSecret(secret) + if err != nil { + t.Errorf("unable to delete secret %d: %v", secret.GetID(), err) + } + } + methods["DeleteSecret"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for secrets", method) + } + } +} + +func testServices(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for services + methods := make(map[string]bool) + // capture the element type of the service interface + element := reflect.TypeOf(new(service.ServiceInterface)).Elem() + // iterate through all methods found in the service interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for services + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + // create the services + for _, service := range resources.Services { + _, err := db.CreateService(service) + if err != nil { + t.Errorf("unable to create service %d: %v", service.GetID(), err) + } + } + methods["CreateService"] = true + + // count the services + count, err := db.CountServices() + if err != nil { + t.Errorf("unable to count services: %v", err) + } + if int(count) != len(resources.Services) { + t.Errorf("CountServices() is %v, want %v", count, len(resources.Services)) + } + methods["CountServices"] = true + + // count the services for a build + count, err = db.CountServicesForBuild(resources.Builds[0], nil) + if err != nil { + t.Errorf("unable to count services for build %d: %v", resources.Builds[0].GetID(), err) + } + if int(count) != len(resources.Services) { + t.Errorf("CountServicesForBuild() is %v, want %v", count, len(resources.Services)) + } + methods["CountServicesForBuild"] = true + + // list the services + list, err := db.ListServices() + if err != nil { + t.Errorf("unable to list services: %v", err) + } + if !reflect.DeepEqual(list, resources.Services) { + t.Errorf("ListServices() is %v, want %v", list, resources.Services) + } + methods["ListServices"] = true + + // list the services for a build + list, count, err = db.ListServicesForBuild(resources.Builds[0], nil, 1, 10) + if err != nil { + t.Errorf("unable to list services for build %d: %v", resources.Builds[0].GetID(), err) + } + if !reflect.DeepEqual(list, []*library.Service{resources.Services[1], resources.Services[0]}) { + t.Errorf("ListServicesForBuild() is %v, want %v", list, []*library.Service{resources.Services[1], resources.Services[0]}) + } + if int(count) != len(resources.Services) { + t.Errorf("ListServicesForBuild() is %v, want %v", count, len(resources.Services)) + } + methods["ListServicesForBuild"] = true + + expected := map[string]float64{ + "#init": 1, + "target/vela-git:v0.3.0": 1, + } + images, err := db.ListServiceImageCount() + if err != nil { + t.Errorf("unable to list service image count: %v", err) + } + if !reflect.DeepEqual(images, expected) { + t.Errorf("ListServiceImageCount() is %v, want %v", images, expected) + } + methods["ListServiceImageCount"] = true + + expected = map[string]float64{ + "pending": 1, + "failure": 0, + "killed": 0, + "running": 1, + "success": 0, + } + statuses, err := db.ListServiceStatusCount() + if err != nil { + t.Errorf("unable to list service status count: %v", err) + } + if !reflect.DeepEqual(statuses, expected) { + t.Errorf("ListServiceStatusCount() is %v, want %v", statuses, expected) + } + methods["ListServiceStatusCount"] = true + + // lookup the services by name + for _, service := range resources.Services { + build := resources.Builds[service.GetBuildID()-1] + got, err := db.GetServiceForBuild(build, service.GetNumber()) + if err != nil { + t.Errorf("unable to get service %d for build %d: %v", service.GetID(), build.GetID(), err) + } + if !reflect.DeepEqual(got, service) { + t.Errorf("GetServiceForBuild() is %v, want %v", got, service) + } + } + methods["GetServiceForBuild"] = true + + // clean the services + count, err = db.CleanServices("integration testing", 1563474090) + if err != nil { + t.Errorf("unable to clean services: %v", err) + } + if int(count) != len(resources.Services) { + t.Errorf("CleanServices() is %v, want %v", count, len(resources.Services)) + } + methods["CleanServices"] = true + + // update the services + for _, service := range resources.Services { + service.SetStatus("success") + got, err := db.UpdateService(service) + if err != nil { + t.Errorf("unable to update service %d: %v", service.GetID(), err) + } + + if !reflect.DeepEqual(got, service) { + t.Errorf("UpdateService() is %v, want %v", got, service) + } + } + methods["UpdateService"] = true + methods["GetService"] = true + + // delete the services + for _, service := range resources.Services { + err = db.DeleteService(service) + if err != nil { + t.Errorf("unable to delete service %d: %v", service.GetID(), err) + } + } + methods["DeleteService"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for services", method) + } + } +} + +func testSteps(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for steps + methods := make(map[string]bool) + // capture the element type of the step interface + element := reflect.TypeOf(new(step.StepInterface)).Elem() + // iterate through all methods found in the step interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for steps + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + // create the steps + for _, step := range resources.Steps { + _, err := db.CreateStep(step) + if err != nil { + t.Errorf("unable to create step %d: %v", step.GetID(), err) + } + } + methods["CreateStep"] = true + + // count the steps + count, err := db.CountSteps() + if err != nil { + t.Errorf("unable to count steps: %v", err) + } + if int(count) != len(resources.Steps) { + t.Errorf("CountSteps() is %v, want %v", count, len(resources.Steps)) + } + methods["CountSteps"] = true + + // count the steps for a build + count, err = db.CountStepsForBuild(resources.Builds[0], nil) + if err != nil { + t.Errorf("unable to count steps for build %d: %v", resources.Builds[0].GetID(), err) + } + if int(count) != len(resources.Steps) { + t.Errorf("CountStepsForBuild() is %v, want %v", count, len(resources.Steps)) + } + methods["CountStepsForBuild"] = true + + // list the steps + list, err := db.ListSteps() + if err != nil { + t.Errorf("unable to list steps: %v", err) + } + if !reflect.DeepEqual(list, resources.Steps) { + t.Errorf("ListSteps() is %v, want %v", list, resources.Steps) + } + methods["ListSteps"] = true + + // list the steps for a build + list, count, err = db.ListStepsForBuild(resources.Builds[0], nil, 1, 10) + if err != nil { + t.Errorf("unable to list steps for build %d: %v", resources.Builds[0].GetID(), err) + } + if !reflect.DeepEqual(list, []*library.Step{resources.Steps[1], resources.Steps[0]}) { + t.Errorf("ListStepsForBuild() is %v, want %v", list, []*library.Step{resources.Steps[1], resources.Steps[0]}) + } + if int(count) != len(resources.Steps) { + t.Errorf("ListStepsForBuild() is %v, want %v", count, len(resources.Steps)) + } + methods["ListStepsForBuild"] = true + + expected := map[string]float64{ + "#init": 1, + "target/vela-git:v0.3.0": 1, + } + images, err := db.ListStepImageCount() + if err != nil { + t.Errorf("unable to list step image count: %v", err) + } + if !reflect.DeepEqual(images, expected) { + t.Errorf("ListStepImageCount() is %v, want %v", images, expected) + } + methods["ListStepImageCount"] = true + + expected = map[string]float64{ + "pending": 1, + "failure": 0, + "killed": 0, + "running": 1, + "success": 0, + } + statuses, err := db.ListStepStatusCount() + if err != nil { + t.Errorf("unable to list step status count: %v", err) + } + if !reflect.DeepEqual(statuses, expected) { + t.Errorf("ListStepStatusCount() is %v, want %v", statuses, expected) + } + methods["ListStepStatusCount"] = true + + // lookup the steps by name + for _, step := range resources.Steps { + build := resources.Builds[step.GetBuildID()-1] + got, err := db.GetStepForBuild(build, step.GetNumber()) + if err != nil { + t.Errorf("unable to get step %d for build %d: %v", step.GetID(), build.GetID(), err) + } + if !reflect.DeepEqual(got, step) { + t.Errorf("GetStepForBuild() is %v, want %v", got, step) + } + } + methods["GetStepForBuild"] = true + + // clean the steps + count, err = db.CleanSteps("integration testing", 1563474090) + if err != nil { + t.Errorf("unable to clean steps: %v", err) + } + if int(count) != len(resources.Steps) { + t.Errorf("CleanSteps() is %v, want %v", count, len(resources.Steps)) + } + methods["CleanSteps"] = true + + // update the steps + for _, step := range resources.Steps { + step.SetStatus("success") + got, err := db.UpdateStep(step) + if err != nil { + t.Errorf("unable to update step %d: %v", step.GetID(), err) + } + + if !reflect.DeepEqual(got, step) { + t.Errorf("GetStep() is %v, want %v", got, step) + } + } + methods["UpdateStep"] = true + methods["GetStep"] = true + + // delete the steps + for _, step := range resources.Steps { + err = db.DeleteStep(step) + if err != nil { + t.Errorf("unable to delete step %d: %v", step.GetID(), err) + } + } + methods["DeleteStep"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for steps", method) + } + } +} + +func testUsers(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for users + methods := make(map[string]bool) + // capture the element type of the user interface + element := reflect.TypeOf(new(user.UserInterface)).Elem() + // iterate through all methods found in the user interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for users + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + userOne := new(library.User) + userOne.SetID(1) + userOne.SetName("octocat") + userOne.SetToken("") + userOne.SetRefreshToken("") + userOne.SetHash("") + userOne.SetFavorites(nil) + userOne.SetActive(false) + userOne.SetAdmin(false) + + userTwo := new(library.User) + userTwo.SetID(2) + userTwo.SetName("octokitty") + userTwo.SetToken("") + userTwo.SetRefreshToken("") + userTwo.SetHash("") + userTwo.SetFavorites(nil) + userTwo.SetActive(false) + userTwo.SetAdmin(false) + + liteUsers := []*library.User{userOne, userTwo} + + // create the users + for _, user := range resources.Users { + err := db.CreateUser(user) + if err != nil { + t.Errorf("unable to create user %d: %v", user.GetID(), err) + } + } + methods["CreateUser"] = true + + // count the users + count, err := db.CountUsers() + if err != nil { + t.Errorf("unable to count users: %v", err) + } + if int(count) != len(resources.Users) { + t.Errorf("CountUsers() is %v, want %v", count, len(resources.Users)) + } + methods["CountUsers"] = true + + // list the users + list, err := db.ListUsers() + if err != nil { + t.Errorf("unable to list users: %v", err) + } + if !reflect.DeepEqual(list, resources.Users) { + t.Errorf("ListUsers() is %v, want %v", list, resources.Users) + } + methods["ListUsers"] = true + + // lite list the users + list, count, err = db.ListLiteUsers(1, 10) + if err != nil { + t.Errorf("unable to list lite users: %v", err) + } + if !reflect.DeepEqual(list, liteUsers) { + t.Errorf("ListLiteUsers() is %v, want %v", list, liteUsers) + } + if int(count) != len(liteUsers) { + t.Errorf("ListLiteUsers() is %v, want %v", count, len(liteUsers)) + } + methods["ListLiteUsers"] = true + + // lookup the users by name + for _, user := range resources.Users { + got, err := db.GetUserForName(user.GetName()) + if err != nil { + t.Errorf("unable to get user %d by name: %v", user.GetID(), err) + } + if !reflect.DeepEqual(got, user) { + t.Errorf("GetUserForName() is %v, want %v", got, user) + } + } + methods["GetUserForName"] = true + + // update the users + for _, user := range resources.Users { + user.SetActive(false) + err = db.UpdateUser(user) + if err != nil { + t.Errorf("unable to update user %d: %v", user.GetID(), err) + } + + // lookup the user by ID + got, err := db.GetUser(user.GetID()) + if err != nil { + t.Errorf("unable to get user %d by ID: %v", user.GetID(), err) + } + if !reflect.DeepEqual(got, user) { + t.Errorf("GetUser() is %v, want %v", got, user) + } + } + methods["UpdateUser"] = true + methods["GetUser"] = true + + // delete the users + for _, user := range resources.Users { + err = db.DeleteUser(user) + if err != nil { + t.Errorf("unable to delete user %d: %v", user.GetID(), err) + } + } + methods["DeleteUser"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for users", method) + } + } +} + +func testWorkers(t *testing.T, db Interface, resources *Resources) { + // create a variable to track the number of methods called for workers + methods := make(map[string]bool) + // capture the element type of the worker interface + element := reflect.TypeOf(new(worker.WorkerInterface)).Elem() + // iterate through all methods found in the worker interface + for i := 0; i < element.NumMethod(); i++ { + // skip tracking the methods to create indexes and tables for workers + // since those are already called when the database engine starts + if strings.Contains(element.Method(i).Name, "Index") || + strings.Contains(element.Method(i).Name, "Table") { + continue + } + + // add the method name to the list of functions + methods[element.Method(i).Name] = false + } + + // create the workers + for _, worker := range resources.Workers { + err := db.CreateWorker(worker) + if err != nil { + t.Errorf("unable to create worker %d: %v", worker.GetID(), err) + } + } + methods["CreateWorker"] = true + + // count the workers + count, err := db.CountWorkers() + if err != nil { + t.Errorf("unable to count workers: %v", err) + } + if int(count) != len(resources.Workers) { + t.Errorf("CountWorkers() is %v, want %v", count, len(resources.Workers)) + } + methods["CountWorkers"] = true + + // list the workers + list, err := db.ListWorkers() + if err != nil { + t.Errorf("unable to list workers: %v", err) + } + if !reflect.DeepEqual(list, resources.Workers) { + t.Errorf("ListWorkers() is %v, want %v", list, resources.Workers) + } + methods["ListWorkers"] = true + + // lookup the workers by hostname + for _, worker := range resources.Workers { + got, err := db.GetWorkerForHostname(worker.GetHostname()) + if err != nil { + t.Errorf("unable to get worker %d by hostname: %v", worker.GetID(), err) + } + if !reflect.DeepEqual(got, worker) { + t.Errorf("GetWorkerForHostname() is %v, want %v", got, worker) + } + } + methods["GetWorkerForHostname"] = true + + // update the workers + for _, worker := range resources.Workers { + worker.SetActive(false) + err = db.UpdateWorker(worker) + if err != nil { + t.Errorf("unable to update worker %d: %v", worker.GetID(), err) + } + + // lookup the worker by ID + got, err := db.GetWorker(worker.GetID()) + if err != nil { + t.Errorf("unable to get worker %d by ID: %v", worker.GetID(), err) + } + if !reflect.DeepEqual(got, worker) { + t.Errorf("GetWorker() is %v, want %v", got, worker) + } + } + methods["UpdateWorker"] = true + methods["GetWorker"] = true + + // delete the workers + for _, worker := range resources.Workers { + err = db.DeleteWorker(worker) + if err != nil { + t.Errorf("unable to delete worker %d: %v", worker.GetID(), err) + } + } + methods["DeleteWorker"] = true + + // ensure we called all the methods we expected to + for method, called := range methods { + if !called { + t.Errorf("method %s was not called for workers", method) + } + } +} + +func newResources() *Resources { + buildOne := new(library.Build) + buildOne.SetID(1) + buildOne.SetRepoID(1) + buildOne.SetPipelineID(1) + buildOne.SetNumber(1) + buildOne.SetParent(1) + buildOne.SetEvent("push") + buildOne.SetEventAction("") + buildOne.SetStatus("running") + buildOne.SetError("") + buildOne.SetEnqueued(1563474077) + buildOne.SetCreated(1563474076) + buildOne.SetStarted(1563474078) + buildOne.SetFinished(1563474079) + buildOne.SetDeploy("") + buildOne.SetDeployPayload(raw.StringSliceMap{"foo": "test1"}) + buildOne.SetClone("https://github.com/github/octocat.git") + buildOne.SetSource("https://github.com/github/octocat/deployments/1") + buildOne.SetTitle("push received from https://github.com/github/octocat") + buildOne.SetMessage("First commit...") + buildOne.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + buildOne.SetSender("OctoKitty") + buildOne.SetAuthor("OctoKitty") + buildOne.SetEmail("OctoKitty@github.com") + buildOne.SetLink("https://example.company.com/github/octocat/1") + buildOne.SetBranch("main") + buildOne.SetRef("refs/heads/main") + buildOne.SetBaseRef("") + buildOne.SetHeadRef("changes") + buildOne.SetHost("example.company.com") + buildOne.SetRuntime("docker") + buildOne.SetDistribution("linux") + + buildTwo := new(library.Build) + buildTwo.SetID(2) + buildTwo.SetRepoID(1) + buildTwo.SetPipelineID(1) + buildTwo.SetNumber(2) + buildTwo.SetParent(1) + buildTwo.SetEvent("pull_request") + buildTwo.SetEventAction("") + buildTwo.SetStatus("running") + buildTwo.SetError("") + buildTwo.SetEnqueued(1563474077) + buildTwo.SetCreated(1563474076) + buildTwo.SetStarted(1563474078) + buildTwo.SetFinished(1563474079) + buildTwo.SetDeploy("") + buildTwo.SetDeployPayload(raw.StringSliceMap{"foo": "test1"}) + buildTwo.SetClone("https://github.com/github/octocat.git") + buildTwo.SetSource("https://github.com/github/octocat/deployments/1") + buildTwo.SetTitle("pull_request received from https://github.com/github/octocat") + buildTwo.SetMessage("Second commit...") + buildTwo.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135164") + buildTwo.SetSender("OctoKitty") + buildTwo.SetAuthor("OctoKitty") + buildTwo.SetEmail("OctoKitty@github.com") + buildTwo.SetLink("https://example.company.com/github/octocat/2") + buildTwo.SetBranch("main") + buildTwo.SetRef("refs/heads/main") + buildTwo.SetBaseRef("") + buildTwo.SetHeadRef("changes") + buildTwo.SetHost("example.company.com") + buildTwo.SetRuntime("docker") + buildTwo.SetDistribution("linux") + + executableOne := new(library.BuildExecutable) + executableOne.SetID(1) + executableOne.SetBuildID(1) + executableOne.SetData([]byte("foo")) + + executableTwo := new(library.BuildExecutable) + executableTwo.SetID(2) + executableTwo.SetBuildID(2) + executableTwo.SetData([]byte("foo")) + + deploymentOne := new(library.Deployment) + deploymentOne.SetID(1) + deploymentOne.SetRepoID(1) + deploymentOne.SetURL("https://github.com/github/octocat/deployments/1") + deploymentOne.SetUser("octocat") + deploymentOne.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + deploymentOne.SetRef("refs/heads/master") + deploymentOne.SetTask("vela-deploy") + deploymentOne.SetTarget("production") + deploymentOne.SetDescription("Deployment request from Vela") + deploymentOne.SetPayload(map[string]string{"foo": "test1"}) + + deploymentTwo := new(library.Deployment) + deploymentTwo.SetID(1) + deploymentTwo.SetRepoID(1) + deploymentTwo.SetURL("https://github.com/github/octocat/deployments/2") + deploymentTwo.SetUser("octocat") + deploymentTwo.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135164") + deploymentTwo.SetRef("refs/heads/master") + deploymentTwo.SetTask("vela-deploy") + deploymentTwo.SetTarget("production") + deploymentTwo.SetDescription("Deployment request from Vela") + deploymentTwo.SetPayload(map[string]string{"foo": "test1"}) + + hookOne := new(library.Hook) + hookOne.SetID(1) + hookOne.SetRepoID(1) + hookOne.SetBuildID(1) + hookOne.SetNumber(1) + hookOne.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + hookOne.SetCreated(time.Now().UTC().Unix()) + hookOne.SetHost("github.com") + hookOne.SetEvent("push") + hookOne.SetEventAction("") + hookOne.SetBranch("main") + hookOne.SetError("") + hookOne.SetStatus("success") + hookOne.SetLink("https://github.com/github/octocat/settings/hooks/1") + hookOne.SetWebhookID(123456) + + hookTwo := new(library.Hook) + hookTwo.SetID(2) + hookTwo.SetRepoID(1) + hookTwo.SetBuildID(1) + hookTwo.SetNumber(2) + hookTwo.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") + hookTwo.SetCreated(time.Now().UTC().Unix()) + hookTwo.SetHost("github.com") + hookTwo.SetEvent("push") + hookTwo.SetEventAction("") + hookTwo.SetBranch("main") + hookTwo.SetError("") + hookTwo.SetStatus("success") + hookTwo.SetLink("https://github.com/github/octocat/settings/hooks/1") + hookTwo.SetWebhookID(123456) + + logServiceOne := new(library.Log) + logServiceOne.SetID(1) + logServiceOne.SetBuildID(1) + logServiceOne.SetRepoID(1) + logServiceOne.SetServiceID(1) + logServiceOne.SetStepID(0) + logServiceOne.SetData([]byte("foo")) + + logServiceTwo := new(library.Log) + logServiceTwo.SetID(2) + logServiceTwo.SetBuildID(1) + logServiceTwo.SetRepoID(1) + logServiceTwo.SetServiceID(2) + logServiceTwo.SetStepID(0) + logServiceTwo.SetData([]byte("foo")) + + logStepOne := new(library.Log) + logStepOne.SetID(3) + logStepOne.SetBuildID(1) + logStepOne.SetRepoID(1) + logStepOne.SetServiceID(0) + logStepOne.SetStepID(1) + logStepOne.SetData([]byte("foo")) + + logStepTwo := new(library.Log) + logStepTwo.SetID(4) + logStepTwo.SetBuildID(1) + logStepTwo.SetRepoID(1) + logStepTwo.SetServiceID(0) + logStepTwo.SetStepID(2) + logStepTwo.SetData([]byte("foo")) + + pipelineOne := new(library.Pipeline) + pipelineOne.SetID(1) + pipelineOne.SetRepoID(1) + pipelineOne.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + pipelineOne.SetFlavor("large") + pipelineOne.SetPlatform("docker") + pipelineOne.SetRef("refs/heads/main") + pipelineOne.SetType("yaml") + pipelineOne.SetVersion("1") + pipelineOne.SetExternalSecrets(false) + pipelineOne.SetInternalSecrets(false) + pipelineOne.SetServices(true) + pipelineOne.SetStages(false) + pipelineOne.SetSteps(true) + pipelineOne.SetTemplates(false) + pipelineOne.SetData([]byte("version: 1")) + + pipelineTwo := new(library.Pipeline) + pipelineTwo.SetID(2) + pipelineTwo.SetRepoID(1) + pipelineTwo.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135164") + pipelineTwo.SetFlavor("large") + pipelineTwo.SetPlatform("docker") + pipelineTwo.SetRef("refs/heads/main") + pipelineTwo.SetType("yaml") + pipelineTwo.SetVersion("1") + pipelineTwo.SetExternalSecrets(false) + pipelineTwo.SetInternalSecrets(false) + pipelineTwo.SetServices(true) + pipelineTwo.SetStages(false) + pipelineTwo.SetSteps(true) + pipelineTwo.SetTemplates(false) + pipelineTwo.SetData([]byte("version: 1")) + + repoOne := new(library.Repo) + repoOne.SetID(1) + repoOne.SetUserID(1) + repoOne.SetHash("MzM4N2MzMDAtNmY4Mi00OTA5LWFhZDAtNWIzMTlkNTJkODMy") + repoOne.SetOrg("github") + repoOne.SetName("octocat") + repoOne.SetFullName("github/octocat") + repoOne.SetLink("https://github.com/github/octocat") + repoOne.SetClone("https://github.com/github/octocat.git") + repoOne.SetBranch("main") + repoOne.SetTopics([]string{"cloud", "security"}) + repoOne.SetBuildLimit(10) + repoOne.SetTimeout(30) + repoOne.SetCounter(0) + repoOne.SetVisibility("public") + repoOne.SetPrivate(false) + repoOne.SetTrusted(false) + repoOne.SetActive(true) + repoOne.SetAllowPull(false) + repoOne.SetAllowPush(true) + repoOne.SetAllowDeploy(false) + repoOne.SetAllowTag(false) + repoOne.SetAllowComment(false) + repoOne.SetPipelineType("") + repoOne.SetPreviousName("") + + repoTwo := new(library.Repo) + repoTwo.SetID(2) + repoTwo.SetUserID(1) + repoTwo.SetHash("MzM4N2MzMDAtNmY4Mi00OTA5LWFhZDAtNWIzMTlkNTJkODMy") + repoTwo.SetOrg("github") + repoTwo.SetName("octokitty") + repoTwo.SetFullName("github/octokitty") + repoTwo.SetLink("https://github.com/github/octokitty") + repoTwo.SetClone("https://github.com/github/octokitty.git") + repoTwo.SetBranch("main") + repoTwo.SetTopics([]string{"cloud", "security"}) + repoTwo.SetBuildLimit(10) + repoTwo.SetTimeout(30) + repoTwo.SetCounter(0) + repoTwo.SetVisibility("public") + repoTwo.SetPrivate(false) + repoTwo.SetTrusted(false) + repoTwo.SetActive(true) + repoTwo.SetAllowPull(false) + repoTwo.SetAllowPush(true) + repoTwo.SetAllowDeploy(false) + repoTwo.SetAllowTag(false) + repoTwo.SetAllowComment(false) + repoTwo.SetPipelineType("") + repoTwo.SetPreviousName("") + + scheduleOne := new(library.Schedule) + scheduleOne.SetID(1) + scheduleOne.SetRepoID(1) + scheduleOne.SetActive(true) + scheduleOne.SetName("nightly") + scheduleOne.SetEntry("0 0 * * *") + scheduleOne.SetCreatedAt(time.Now().UTC().Unix()) + scheduleOne.SetCreatedBy("octocat") + scheduleOne.SetUpdatedAt(time.Now().Add(time.Hour * 1).UTC().Unix()) + scheduleOne.SetUpdatedBy("octokitty") + scheduleOne.SetScheduledAt(time.Now().Add(time.Hour * 2).UTC().Unix()) + scheduleOne.SetBranch("main") + + scheduleTwo := new(library.Schedule) + scheduleTwo.SetID(2) + scheduleTwo.SetRepoID(1) + scheduleTwo.SetActive(true) + scheduleTwo.SetName("hourly") + scheduleTwo.SetEntry("0 * * * *") + scheduleTwo.SetCreatedAt(time.Now().UTC().Unix()) + scheduleTwo.SetCreatedBy("octocat") + scheduleTwo.SetUpdatedAt(time.Now().Add(time.Hour * 1).UTC().Unix()) + scheduleTwo.SetUpdatedBy("octokitty") + scheduleTwo.SetScheduledAt(time.Now().Add(time.Hour * 2).UTC().Unix()) + scheduleTwo.SetBranch("main") + + secretOrg := new(library.Secret) + secretOrg.SetID(1) + secretOrg.SetOrg("github") + secretOrg.SetRepo("*") + secretOrg.SetTeam("") + secretOrg.SetName("foo") + secretOrg.SetValue("bar") + secretOrg.SetType("org") + secretOrg.SetImages([]string{"alpine"}) + secretOrg.SetEvents([]string{"push", "tag", "deployment"}) + secretOrg.SetAllowCommand(true) + secretOrg.SetCreatedAt(time.Now().UTC().Unix()) + secretOrg.SetCreatedBy("octocat") + secretOrg.SetUpdatedAt(time.Now().Add(time.Hour * 1).UTC().Unix()) + secretOrg.SetUpdatedBy("octokitty") + + secretRepo := new(library.Secret) + secretRepo.SetID(2) + secretRepo.SetOrg("github") + secretRepo.SetRepo("octocat") + secretRepo.SetTeam("") + secretRepo.SetName("foo") + secretRepo.SetValue("bar") + secretRepo.SetType("repo") + secretRepo.SetImages([]string{"alpine"}) + secretRepo.SetEvents([]string{"push", "tag", "deployment"}) + secretRepo.SetAllowCommand(true) + secretRepo.SetCreatedAt(time.Now().UTC().Unix()) + secretRepo.SetCreatedBy("octocat") + secretRepo.SetUpdatedAt(time.Now().Add(time.Hour * 1).UTC().Unix()) + secretRepo.SetUpdatedBy("octokitty") + + secretShared := new(library.Secret) + secretShared.SetID(3) + secretShared.SetOrg("github") + secretShared.SetRepo("") + secretShared.SetTeam("octocat") + secretShared.SetName("foo") + secretShared.SetValue("bar") + secretShared.SetType("shared") + secretShared.SetImages([]string{"alpine"}) + secretShared.SetEvents([]string{"push", "tag", "deployment"}) + secretShared.SetAllowCommand(true) + secretShared.SetCreatedAt(time.Now().UTC().Unix()) + secretShared.SetCreatedBy("octocat") + secretShared.SetUpdatedAt(time.Now().Add(time.Hour * 1).UTC().Unix()) + secretShared.SetUpdatedBy("octokitty") + + serviceOne := new(library.Service) + serviceOne.SetID(1) + serviceOne.SetBuildID(1) + serviceOne.SetRepoID(1) + serviceOne.SetNumber(1) + serviceOne.SetName("init") + serviceOne.SetImage("#init") + serviceOne.SetStatus("running") + serviceOne.SetError("") + serviceOne.SetExitCode(0) + serviceOne.SetCreated(1563474076) + serviceOne.SetStarted(1563474078) + serviceOne.SetFinished(1563474079) + serviceOne.SetHost("example.company.com") + serviceOne.SetRuntime("docker") + serviceOne.SetDistribution("linux") + + serviceTwo := new(library.Service) + serviceTwo.SetID(2) + serviceTwo.SetBuildID(1) + serviceTwo.SetRepoID(1) + serviceTwo.SetNumber(2) + serviceTwo.SetName("clone") + serviceTwo.SetImage("target/vela-git:v0.3.0") + serviceTwo.SetStatus("pending") + serviceTwo.SetError("") + serviceTwo.SetExitCode(0) + serviceTwo.SetCreated(1563474086) + serviceTwo.SetStarted(1563474088) + serviceTwo.SetFinished(1563474089) + serviceTwo.SetHost("example.company.com") + serviceTwo.SetRuntime("docker") + serviceTwo.SetDistribution("linux") + + stepOne := new(library.Step) + stepOne.SetID(1) + stepOne.SetBuildID(1) + stepOne.SetRepoID(1) + stepOne.SetNumber(1) + stepOne.SetName("init") + stepOne.SetImage("#init") + stepOne.SetStage("init") + stepOne.SetStatus("running") + stepOne.SetError("") + stepOne.SetExitCode(0) + stepOne.SetCreated(1563474076) + stepOne.SetStarted(1563474078) + stepOne.SetFinished(1563474079) + stepOne.SetHost("example.company.com") + stepOne.SetRuntime("docker") + stepOne.SetDistribution("linux") + + stepTwo := new(library.Step) + stepTwo.SetID(2) + stepTwo.SetBuildID(1) + stepTwo.SetRepoID(1) + stepTwo.SetNumber(2) + stepTwo.SetName("clone") + stepTwo.SetImage("target/vela-git:v0.3.0") + stepTwo.SetStage("init") + stepTwo.SetStatus("pending") + stepTwo.SetError("") + stepTwo.SetExitCode(0) + stepTwo.SetCreated(1563474086) + stepTwo.SetStarted(1563474088) + stepTwo.SetFinished(1563474089) + stepTwo.SetHost("example.company.com") + stepTwo.SetRuntime("docker") + stepTwo.SetDistribution("linux") + + userOne := new(library.User) + userOne.SetID(1) + userOne.SetName("octocat") + userOne.SetToken("superSecretToken") + userOne.SetRefreshToken("superSecretRefreshToken") + userOne.SetHash("MzM4N2MzMDAtNmY4Mi00OTA5LWFhZDAtNWIzMTlkNTJkODMy") + userOne.SetFavorites([]string{"github/octocat"}) + userOne.SetActive(true) + userOne.SetAdmin(false) + + userTwo := new(library.User) + userTwo.SetID(2) + userTwo.SetName("octokitty") + userTwo.SetToken("superSecretToken") + userTwo.SetRefreshToken("superSecretRefreshToken") + userTwo.SetHash("MzM4N2MzMDAtNmY4Mi00OTA5LWFhZDAtNWIzMTlkNTJkODMy") + userTwo.SetFavorites([]string{"github/octocat"}) + userTwo.SetActive(true) + userTwo.SetAdmin(false) + + workerOne := new(library.Worker) + workerOne.SetID(1) + workerOne.SetHostname("worker-1.example.com") + workerOne.SetAddress("https://worker-1.example.com") + workerOne.SetRoutes([]string{"vela"}) + workerOne.SetActive(true) + workerOne.SetStatus("available") + workerOne.SetLastStatusUpdateAt(time.Now().UTC().Unix()) + workerOne.SetRunningBuildIDs([]string{"12345"}) + workerOne.SetLastBuildStartedAt(time.Now().UTC().Unix()) + workerOne.SetLastBuildFinishedAt(time.Now().UTC().Unix()) + workerOne.SetLastCheckedIn(time.Now().UTC().Unix()) + workerOne.SetBuildLimit(1) + + workerTwo := new(library.Worker) + workerTwo.SetID(2) + workerTwo.SetHostname("worker-2.example.com") + workerTwo.SetAddress("https://worker-2.example.com") + workerTwo.SetRoutes([]string{"vela"}) + workerTwo.SetActive(true) + workerTwo.SetStatus("available") + workerTwo.SetLastStatusUpdateAt(time.Now().UTC().Unix()) + workerTwo.SetRunningBuildIDs([]string{"12345"}) + workerTwo.SetLastBuildStartedAt(time.Now().UTC().Unix()) + workerTwo.SetLastBuildFinishedAt(time.Now().UTC().Unix()) + workerTwo.SetLastCheckedIn(time.Now().UTC().Unix()) + workerTwo.SetBuildLimit(1) + + return &Resources{ + Builds: []*library.Build{buildOne, buildTwo}, + Deployments: []*library.Deployment{deploymentOne, deploymentTwo}, + Executables: []*library.BuildExecutable{executableOne, executableTwo}, + Hooks: []*library.Hook{hookOne, hookTwo}, + Logs: []*library.Log{logServiceOne, logServiceTwo, logStepOne, logStepTwo}, + Pipelines: []*library.Pipeline{pipelineOne, pipelineTwo}, + Repos: []*library.Repo{repoOne, repoTwo}, + Schedules: []*library.Schedule{scheduleOne, scheduleTwo}, + Secrets: []*library.Secret{secretOrg, secretRepo, secretShared}, + Services: []*library.Service{serviceOne, serviceTwo}, + Steps: []*library.Step{stepOne, stepTwo}, + Users: []*library.User{userOne, userTwo}, + Workers: []*library.Worker{workerOne, workerTwo}, + } +} diff --git a/database/interface.go b/database/interface.go new file mode 100644 index 000000000..cc7428378 --- /dev/null +++ b/database/interface.go @@ -0,0 +1,72 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +import ( + "github.com/go-vela/server/database/build" + "github.com/go-vela/server/database/executable" + "github.com/go-vela/server/database/hook" + "github.com/go-vela/server/database/log" + "github.com/go-vela/server/database/pipeline" + "github.com/go-vela/server/database/repo" + "github.com/go-vela/server/database/schedule" + "github.com/go-vela/server/database/secret" + "github.com/go-vela/server/database/service" + "github.com/go-vela/server/database/step" + "github.com/go-vela/server/database/user" + "github.com/go-vela/server/database/worker" +) + +// Interface represents the interface for integrating with the supported database providers. +type Interface interface { + // Generic Interface Functions + + // Close defines a function that stops and terminates the connection to the database. + Close() error + + // Driver defines a function that outputs the configured database driver. + Driver() string + + // Ping defines a function that sends a "ping" request to the configured database. + Ping() error + + // Resource Interface Functions + + // BuildInterface defines the interface for builds stored in the database. + build.BuildInterface + + // BuildExecutableInterface defines the interface for build executables stored in the database. + executable.BuildExecutableInterface + + // HookInterface defines the interface for hooks stored in the database. + hook.HookInterface + + // LogInterface defines the interface for logs stored in the database. + log.LogInterface + + // PipelineInterface defines the interface for pipelines stored in the database. + pipeline.PipelineInterface + + // RepoInterface defines the interface for repos stored in the database. + repo.RepoInterface + + // ScheduleInterface defines the interface for schedules stored in the database. + schedule.ScheduleInterface + + // SecretInterface defines the interface for secrets stored in the database. + secret.SecretInterface + + // ServiceInterface defines the interface for services stored in the database. + service.ServiceInterface + + // StepInterface defines the interface for steps stored in the database. + step.StepInterface + + // UserInterface defines the interface for users stored in the database. + user.UserInterface + + // WorkerInterface defines the interface for workers stored in the database. + worker.WorkerInterface +} diff --git a/database/log/count.go b/database/log/count.go new file mode 100644 index 000000000..e4ec570fe --- /dev/null +++ b/database/log/count.go @@ -0,0 +1,25 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "github.com/go-vela/types/constants" +) + +// CountLogs gets the count of all logs from the database. +func (e *engine) CountLogs() (int64, error) { + e.logger.Tracef("getting count of all logs from the database") + + // variable to store query results + var l int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableLog). + Count(&l). + Error + + return l, err +} diff --git a/database/log/count_build.go b/database/log/count_build.go new file mode 100644 index 000000000..6a7fb74fb --- /dev/null +++ b/database/log/count_build.go @@ -0,0 +1,27 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" +) + +// CountLogsForBuild gets the count of logs by build ID from the database. +func (e *engine) CountLogsForBuild(b *library.Build) (int64, error) { + e.logger.Tracef("getting count of logs for build %d from the database", b.GetID()) + + // variable to store query results + var l int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableLog). + Where("build_id = ?", b.GetID()). + Count(&l). + Error + + return l, err +} diff --git a/database/log/count_build_test.go b/database/log/count_build_test.go new file mode 100644 index 000000000..d462eedf0 --- /dev/null +++ b/database/log/count_build_test.go @@ -0,0 +1,99 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestLog_Engine_CountLogsForBuild(t *testing.T) { + // setup types + _service := testLog() + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetServiceID(1) + + _step := testLog() + _step.SetID(2) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetStepID(1) + + _build := testBuild() + _build.SetID(1) + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "logs" WHERE build_id = $1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateLog(_service) + if err != nil { + t.Errorf("unable to create test service log for sqlite: %v", err) + } + + err = _sqlite.CreateLog(_step) + if err != nil { + t.Errorf("unable to create test step log for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountLogsForBuild(_build) + + if test.failure { + if err == nil { + t.Errorf("CountLogsForBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountLogsForBuild for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountLogsForBuild for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/log/count_test.go b/database/log/count_test.go new file mode 100644 index 000000000..99e75a767 --- /dev/null +++ b/database/log/count_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestLog_Engine_CountLogs(t *testing.T) { + // setup types + _service := testLog() + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetServiceID(1) + + _step := testLog() + _step.SetID(2) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetStepID(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "logs"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateLog(_service) + if err != nil { + t.Errorf("unable to create test service log for sqlite: %v", err) + } + + err = _sqlite.CreateLog(_step) + if err != nil { + t.Errorf("unable to create test step log for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountLogs() + + if test.failure { + if err == nil { + t.Errorf("CountLogs for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountLogs for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountLogs for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/log/create.go b/database/log/create.go new file mode 100644 index 000000000..978da9a1f --- /dev/null +++ b/database/log/create.go @@ -0,0 +1,57 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with create.go +package log + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// CreateLog creates a new log in the database. +func (e *engine) CreateLog(l *library.Log) error { + // check what the log entry is for + switch { + case l.GetServiceID() > 0: + e.logger.Tracef("creating log for service %d for build %d in the database", l.GetServiceID(), l.GetBuildID()) + case l.GetStepID() > 0: + e.logger.Tracef("creating log for step %d for build %d in the database", l.GetStepID(), l.GetBuildID()) + } + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#LogFromLibrary + log := database.LogFromLibrary(l) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.Validate + err := log.Validate() + if err != nil { + return err + } + + // compress log data for the resource + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.Compress + err = log.Compress(e.config.CompressionLevel) + if err != nil { + switch { + case l.GetServiceID() > 0: + return fmt.Errorf("unable to compress log for service %d for build %d: %w", l.GetServiceID(), l.GetBuildID(), err) + case l.GetStepID() > 0: + return fmt.Errorf("unable to compress log for step %d for build %d: %w", l.GetStepID(), l.GetBuildID(), err) + } + } + + // send query to the database + return e.client. + Table(constants.TableLog). + Create(log). + Error +} diff --git a/database/log/create_test.go b/database/log/create_test.go new file mode 100644 index 000000000..1574e88a8 --- /dev/null +++ b/database/log/create_test.go @@ -0,0 +1,92 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestLog_Engine_CreateLog(t *testing.T) { + // setup types + _service := testLog() + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetServiceID(1) + + _step := testLog() + _step.SetID(2) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetStepID(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the service query + _mock.ExpectQuery(`INSERT INTO "logs" +("build_id","repo_id","service_id","step_id","data","id") +VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id"`). + WithArgs(1, 1, 1, nil, AnyArgument{}, 1). + WillReturnRows(_rows) + + // ensure the mock expects the step query + _mock.ExpectQuery(`INSERT INTO "logs" +("build_id","repo_id","service_id","step_id","data","id") +VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id"`). + WithArgs(1, 1, nil, 1, AnyArgument{}, 2). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + logs []*library.Log + }{ + { + failure: false, + name: "postgres", + database: _postgres, + logs: []*library.Log{_service, _step}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + logs: []*library.Log{_service, _step}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for _, log := range test.logs { + err := test.database.CreateLog(log) + + if test.failure { + if err == nil { + t.Errorf("CreateLog for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateLog for %s returned err: %v", test.name, err) + } + } + }) + } +} diff --git a/database/log/delete.go b/database/log/delete.go new file mode 100644 index 000000000..255e6213b --- /dev/null +++ b/database/log/delete.go @@ -0,0 +1,33 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// DeleteLog deletes an existing log from the database. +func (e *engine) DeleteLog(l *library.Log) error { + // check what the log entry is for + switch { + case l.GetServiceID() > 0: + e.logger.Tracef("deleting log for service %d for build %d in the database", l.GetServiceID(), l.GetBuildID()) + case l.GetStepID() > 0: + e.logger.Tracef("deleting log for step %d for build %d in the database", l.GetStepID(), l.GetBuildID()) + } + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#LogFromLibrary + log := database.LogFromLibrary(l) + + // send query to the database + return e.client. + Table(constants.TableLog). + Delete(log). + Error +} diff --git a/database/log/delete_test.go b/database/log/delete_test.go new file mode 100644 index 000000000..15329a0af --- /dev/null +++ b/database/log/delete_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestLog_Engine_DeleteLog(t *testing.T) { + // setup types + _log := testLog() + _log.SetID(1) + _log.SetRepoID(1) + _log.SetBuildID(1) + _log.SetStepID(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`DELETE FROM "logs" WHERE "logs"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateLog(_log) + if err != nil { + t.Errorf("unable to create test log for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeleteLog(_log) + + if test.failure { + if err == nil { + t.Errorf("DeleteLog for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeleteLog for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/log/get.go b/database/log/get.go new file mode 100644 index 000000000..d31c6e1ef --- /dev/null +++ b/database/log/get.go @@ -0,0 +1,48 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetLog gets a log by ID from the database. +func (e *engine) GetLog(id int64) (*library.Log, error) { + e.logger.Tracef("getting log %d from the database", id) + + // variable to store query results + l := new(database.Log) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableLog). + Where("id = ?", id). + Take(l). + Error + if err != nil { + return nil, err + } + + // decompress log data + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.Decompress + err = l.Decompress() + if err != nil { + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch uncompressed logs + e.logger.Errorf("unable to decompress log %d: %v", id, err) + + // return the uncompressed log + return l.ToLibrary(), nil + } + + // return the log + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.ToLibrary + return l.ToLibrary(), nil +} diff --git a/database/log/get_service.go b/database/log/get_service.go new file mode 100644 index 000000000..38cf38a45 --- /dev/null +++ b/database/log/get_service.go @@ -0,0 +1,49 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with get_step.go +package log + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetLogForService gets a log by service ID from the database. +func (e *engine) GetLogForService(s *library.Service) (*library.Log, error) { + e.logger.Tracef("getting log for service %d for build %d from the database", s.GetID(), s.GetBuildID()) + + // variable to store query results + l := new(database.Log) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableLog). + Where("service_id = ?", s.GetID()). + Take(l). + Error + if err != nil { + return nil, err + } + + // decompress log data for the service + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.Decompress + err = l.Decompress() + if err != nil { + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch uncompressed logs + e.logger.Errorf("unable to decompress log for service %d for build %d: %v", s.GetID(), s.GetBuildID(), err) + + // return the uncompressed log + return l.ToLibrary(), nil + } + + // return the log + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.ToLibrary + return l.ToLibrary(), nil +} diff --git a/database/log/get_service_test.go b/database/log/get_service_test.go new file mode 100644 index 000000000..26f42813c --- /dev/null +++ b/database/log/get_service_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestLog_Engine_GetLogForService(t *testing.T) { + // setup types + _log := testLog() + _log.SetID(1) + _log.SetRepoID(1) + _log.SetBuildID(1) + _log.SetServiceID(1) + _log.SetData([]byte{}) + + _service := testService() + _service.SetID(1) + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetNumber(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "build_id", "repo_id", "service_id", "step_id", "data"}). + AddRow(1, 1, 1, 1, 0, []byte{}) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "logs" WHERE service_id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateLog(_log) + if err != nil { + t.Errorf("unable to create test log for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Log + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _log, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _log, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetLogForService(_service) + + if test.failure { + if err == nil { + t.Errorf("GetLogForService for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetLogForService for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetLogForService for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/log/get_step.go b/database/log/get_step.go new file mode 100644 index 000000000..92e503852 --- /dev/null +++ b/database/log/get_step.go @@ -0,0 +1,49 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with get_service.go +package log + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetLogForStep gets a log by step ID from the database. +func (e *engine) GetLogForStep(s *library.Step) (*library.Log, error) { + e.logger.Tracef("getting log for step %d for build %d from the database", s.GetID(), s.GetBuildID()) + + // variable to store query results + l := new(database.Log) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableLog). + Where("step_id = ?", s.GetID()). + Take(l). + Error + if err != nil { + return nil, err + } + + // decompress log data for the step + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.Decompress + err = l.Decompress() + if err != nil { + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch uncompressed logs + e.logger.Errorf("unable to decompress log for step %d for build %d: %v", s.GetID(), s.GetBuildID(), err) + + // return the uncompressed log + return l.ToLibrary(), nil + } + + // return the log + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.ToLibrary + return l.ToLibrary(), nil +} diff --git a/database/log/get_step_test.go b/database/log/get_step_test.go new file mode 100644 index 000000000..39f019039 --- /dev/null +++ b/database/log/get_step_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestLog_Engine_GetLogForStep(t *testing.T) { + // setup types + _log := testLog() + _log.SetID(1) + _log.SetRepoID(1) + _log.SetBuildID(1) + _log.SetStepID(1) + _log.SetData([]byte{}) + + _step := testStep() + _step.SetID(1) + _step.SetID(1) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetNumber(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "build_id", "repo_id", "step_id", "step_id", "data"}). + AddRow(1, 1, 1, 0, 1, []byte{}) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "logs" WHERE step_id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateLog(_log) + if err != nil { + t.Errorf("unable to create test log for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Log + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _log, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _log, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetLogForStep(_step) + + if test.failure { + if err == nil { + t.Errorf("GetLogForStep for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetLogForStep for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetLogForStep for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/log/get_test.go b/database/log/get_test.go new file mode 100644 index 000000000..31325e3ac --- /dev/null +++ b/database/log/get_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestLog_Engine_GetLog(t *testing.T) { + // setup types + _log := testLog() + _log.SetID(1) + _log.SetRepoID(1) + _log.SetBuildID(1) + _log.SetStepID(1) + _log.SetData([]byte{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "build_id", "repo_id", "service_id", "step_id", "data"}). + AddRow(1, 1, 1, 0, 1, []byte{}) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "logs" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateLog(_log) + if err != nil { + t.Errorf("unable to create test log for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Log + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _log, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _log, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetLog(1) + + if test.failure { + if err == nil { + t.Errorf("GetLog for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetLog for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetLog for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/log/index.go b/database/log/index.go new file mode 100644 index 000000000..2ad50b642 --- /dev/null +++ b/database/log/index.go @@ -0,0 +1,24 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +const ( + // CreateBuildIDIndex represents a query to create an + // index on the logs table for the build_id column. + CreateBuildIDIndex = ` +CREATE INDEX +IF NOT EXISTS +logs_build_id +ON logs (build_id); +` +) + +// CreateLogIndexes creates the indexes for the logs table in the database. +func (e *engine) CreateLogIndexes() error { + e.logger.Tracef("creating indexes for logs table in the database") + + // create the build_id column index for the logs table + return e.client.Exec(CreateBuildIDIndex).Error +} diff --git a/database/log/index_test.go b/database/log/index_test.go new file mode 100644 index 000000000..26c0045db --- /dev/null +++ b/database/log/index_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestLog_Engine_CreateLogIndexes(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreateBuildIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateLogIndexes() + + if test.failure { + if err == nil { + t.Errorf("CreateLogIndexes for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateLogIndexes for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/log/interface.go b/database/log/interface.go new file mode 100644 index 000000000..8c72a5098 --- /dev/null +++ b/database/log/interface.go @@ -0,0 +1,49 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "github.com/go-vela/types/library" +) + +// LogInterface represents the Vela interface for log +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type LogInterface interface { + // Log Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreateLogIndexes defines a function that creates the indexes for the logs table. + CreateLogIndexes() error + // CreateLogTable defines a function that creates the logs table. + CreateLogTable(string) error + + // Log Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CountLogs defines a function that gets the count of all logs. + CountLogs() (int64, error) + // CountLogsForBuild defines a function that gets the count of logs by build ID. + CountLogsForBuild(*library.Build) (int64, error) + // CreateLog defines a function that creates a new log. + CreateLog(*library.Log) error + // DeleteLog defines a function that deletes an existing log. + DeleteLog(*library.Log) error + // GetLog defines a function that gets a log by ID. + GetLog(int64) (*library.Log, error) + // GetLogForService defines a function that gets a log by service ID. + GetLogForService(*library.Service) (*library.Log, error) + // GetLogForStep defines a function that gets a log by step ID. + GetLogForStep(*library.Step) (*library.Log, error) + // ListLogs defines a function that gets a list of all logs. + ListLogs() ([]*library.Log, error) + // ListLogsForBuild defines a function that gets a list of logs by build ID. + ListLogsForBuild(*library.Build, int, int) ([]*library.Log, int64, error) + // UpdateLog defines a function that updates an existing log. + UpdateLog(*library.Log) error +} diff --git a/database/log/list.go b/database/log/list.go new file mode 100644 index 000000000..6a5381278 --- /dev/null +++ b/database/log/list.go @@ -0,0 +1,65 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListLogs gets a list of all logs from the database. +func (e *engine) ListLogs() ([]*library.Log, error) { + e.logger.Trace("listing all logs from the database") + + // variables to store query results and return value + count := int64(0) + l := new([]database.Log) + logs := []*library.Log{} + + // count the results + count, err := e.CountLogs() + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return logs, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableLog). + Find(&l). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, log := range *l { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := log + + // decompress log data + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.Decompress + err = tmp.Decompress() + if err != nil { + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch uncompressed logs + e.logger.Errorf("unable to decompress logs: %v", err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.ToLibrary + logs = append(logs, tmp.ToLibrary()) + } + + return logs, nil +} diff --git a/database/log/list_build.go b/database/log/list_build.go new file mode 100644 index 000000000..ab083a706 --- /dev/null +++ b/database/log/list_build.go @@ -0,0 +1,73 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListLogsForBuild gets a list of logs by build ID from the database. +func (e *engine) ListLogsForBuild(b *library.Build, page, perPage int) ([]*library.Log, int64, error) { + e.logger.Tracef("listing logs for build %d from the database", b.GetID()) + + // variables to store query results and return value + count := int64(0) + l := new([]database.Log) + logs := []*library.Log{} + + // count the results + count, err := e.CountLogsForBuild(b) + if err != nil { + return nil, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return logs, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableLog). + Where("build_id = ?", b.GetID()). + Order("service_id ASC NULLS LAST"). + Order("step_id ASC"). + Limit(perPage). + Offset(offset). + Find(&l). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, log := range *l { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := log + + // decompress log data for the build + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.Decompress + err = tmp.Decompress() + if err != nil { + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch uncompressed logs + e.logger.Errorf("unable to decompress logs for build %d: %v", b.GetID(), err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.ToLibrary + logs = append(logs, tmp.ToLibrary()) + } + + return logs, count, nil +} diff --git a/database/log/list_build_test.go b/database/log/list_build_test.go new file mode 100644 index 000000000..cf20fb50e --- /dev/null +++ b/database/log/list_build_test.go @@ -0,0 +1,110 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestLog_Engine_ListLogsForBuild(t *testing.T) { + // setup types + _service := testLog() + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetServiceID(1) + _service.SetData([]byte{}) + + _step := testLog() + _step.SetID(2) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetStepID(1) + _step.SetData([]byte{}) + + _build := testBuild() + _build.SetID(1) + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "logs" WHERE build_id = $1`).WithArgs(1).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "build_id", "repo_id", "service_id", "step_id", "data"}). + AddRow(1, 1, 1, 1, 0, []byte{}).AddRow(2, 1, 1, 0, 1, []byte{}) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "logs" WHERE build_id = $1 ORDER BY service_id ASC NULLS LAST,step_id ASC LIMIT 10`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateLog(_service) + if err != nil { + t.Errorf("unable to create test service log for sqlite: %v", err) + } + + err = _sqlite.CreateLog(_step) + if err != nil { + t.Errorf("unable to create test step log for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Log + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Log{_service, _step}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Log{_service, _step}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListLogsForBuild(_build, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListLogsForBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListLogsForBuild for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListLogsForBuild for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/log/list_test.go b/database/log/list_test.go new file mode 100644 index 000000000..0cf420255 --- /dev/null +++ b/database/log/list_test.go @@ -0,0 +1,104 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestLog_Engine_ListLogs(t *testing.T) { + // setup types + _service := testLog() + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetServiceID(1) + _service.SetData([]byte{}) + + _step := testLog() + _step.SetID(2) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetStepID(1) + _step.SetData([]byte{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "logs"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "build_id", "repo_id", "service_id", "step_id", "data"}). + AddRow(1, 1, 1, 1, 0, []byte{}).AddRow(2, 1, 1, 0, 1, []byte{}) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "logs"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateLog(_service) + if err != nil { + t.Errorf("unable to create test service log for sqlite: %v", err) + } + + err = _sqlite.CreateLog(_step) + if err != nil { + t.Errorf("unable to create test step log for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Log + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Log{_service, _step}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Log{_service, _step}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListLogs() + + if test.failure { + if err == nil { + t.Errorf("ListLogs for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListLogs for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListLogs for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/log/log.go b/database/log/log.go new file mode 100644 index 000000000..35d25400f --- /dev/null +++ b/database/log/log.go @@ -0,0 +1,82 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the LogInterface interface. + config struct { + // specifies the level of compression to use for the Log engine + CompressionLevel int + // specifies to skip creating tables and indexes for the Log engine + SkipCreation bool + } + + // engine represents the log functionality that implements the LogInterface interface. + engine struct { + // engine configuration settings used in log functions + config *config + + // gorm.io/gorm database client used in log functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in log functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with logs in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new Log engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating log database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of logs table and indexes in the database") + + return e, nil + } + + // create the logs table + err := e.CreateLogTable(e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableLog, err) + } + + // create the indexes for the logs table + err = e.CreateLogIndexes() + if err != nil { + return nil, fmt.Errorf("unable to create indexes for %s table: %w", constants.TableLog, err) + } + + return e, nil +} diff --git a/database/log/log_test.go b/database/log/log_test.go new file mode 100644 index 000000000..69e3835d5 --- /dev/null +++ b/database/log/log_test.go @@ -0,0 +1,276 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "database/sql/driver" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestLog_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateBuildIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithClient(test.client), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateBuildIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres log engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite log engine: %v", err) + } + + return _engine +} + +// This will be used with the github.com/DATA-DOG/go-sqlmock +// library to compare values that are otherwise not easily +// compared. These typically would be values generated before +// adding or updating them in the database. +// +// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime +type AnyArgument struct{} + +// Match satisfies sqlmock.Argument interface. +func (a AnyArgument) Match(v driver.Value) bool { + return true +} + +// testBuild is a test helper function to create a library +// Build type with all fields set to their zero values. +func testBuild() *library.Build { + return &library.Build{ + ID: new(int64), + RepoID: new(int64), + PipelineID: new(int64), + Number: new(int), + Parent: new(int), + Event: new(string), + EventAction: new(string), + Status: new(string), + Error: new(string), + Enqueued: new(int64), + Created: new(int64), + Started: new(int64), + Finished: new(int64), + Deploy: new(string), + Clone: new(string), + Source: new(string), + Title: new(string), + Message: new(string), + Commit: new(string), + Sender: new(string), + Author: new(string), + Email: new(string), + Link: new(string), + Branch: new(string), + Ref: new(string), + BaseRef: new(string), + HeadRef: new(string), + Host: new(string), + Runtime: new(string), + Distribution: new(string), + } +} + +// testLog is a test helper function to create a library +// Log type with all fields set to their zero values. +func testLog() *library.Log { + return &library.Log{ + ID: new(int64), + RepoID: new(int64), + BuildID: new(int64), + ServiceID: new(int64), + StepID: new(int64), + Data: new([]byte), + } +} + +// testService is a test helper function to create a library +// Service type with all fields set to their zero values. +func testService() *library.Service { + return &library.Service{ + ID: new(int64), + BuildID: new(int64), + RepoID: new(int64), + Number: new(int), + Name: new(string), + Image: new(string), + Status: new(string), + Error: new(string), + ExitCode: new(int), + Created: new(int64), + Started: new(int64), + Finished: new(int64), + Host: new(string), + Runtime: new(string), + Distribution: new(string), + } +} + +// testStep is a test helper function to create a library +// Step type with all fields set to their zero values. +func testStep() *library.Step { + return &library.Step{ + ID: new(int64), + BuildID: new(int64), + RepoID: new(int64), + Number: new(int), + Name: new(string), + Image: new(string), + Stage: new(string), + Status: new(string), + Error: new(string), + ExitCode: new(int), + Created: new(int64), + Started: new(int64), + Finished: new(int64), + Host: new(string), + Runtime: new(string), + Distribution: new(string), + } +} diff --git a/database/log/opts.go b/database/log/opts.go new file mode 100644 index 000000000..ea1897ba9 --- /dev/null +++ b/database/log/opts.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Logs. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Logs. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the log engine + e.client = client + + return nil + } +} + +// WithCompressionLevel sets the compression level in the database engine for Logs. +func WithCompressionLevel(level int) EngineOpt { + return func(e *engine) error { + // set the compression level in the log engine + e.config.CompressionLevel = level + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Logs. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the log engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Logs. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the log engine + e.config.SkipCreation = skipCreation + + return nil + } +} diff --git a/database/log/opts_test.go b/database/log/opts_test.go new file mode 100644 index 000000000..c35dbaa48 --- /dev/null +++ b/database/log/opts_test.go @@ -0,0 +1,216 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestLog_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestLog_EngineOpt_WithCompressionLevel(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + level int + want int + }{ + { + failure: false, + name: "compression level set to -1", + level: -1, + want: -1, + }, + { + failure: false, + name: "compression level set to 0", + level: 0, + want: 0, + }, + { + failure: false, + name: "compression level set to 1", + level: 1, + want: 1, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithCompressionLevel(test.level)(e) + + if test.failure { + if err == nil { + t.Errorf("WithCompressionLevel for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithCompressionLevel returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.CompressionLevel, test.want) { + t.Errorf("WithCompressionLevel is %v, want %v", e.config.CompressionLevel, test.want) + } + }) + } +} + +func TestLog_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestLog_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} diff --git a/database/log/table.go b/database/log/table.go new file mode 100644 index 000000000..2d5e5e0c5 --- /dev/null +++ b/database/log/table.go @@ -0,0 +1,60 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres logs table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +logs ( + id SERIAL PRIMARY KEY, + build_id INTEGER, + repo_id INTEGER, + service_id INTEGER, + step_id INTEGER, + data BYTEA, + UNIQUE(step_id), + UNIQUE(service_id) +); +` + + // CreateSqliteTable represents a query to create the Sqlite logs table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + build_id INTEGER, + repo_id INTEGER, + service_id INTEGER, + step_id INTEGER, + data BLOB, + UNIQUE(step_id), + UNIQUE(service_id) +); +` +) + +// CreateLogTable creates the logs table in the database. +func (e *engine) CreateLogTable(driver string) error { + e.logger.Tracef("creating logs table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the logs table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the logs table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/log/table_test.go b/database/log/table_test.go new file mode 100644 index 000000000..066d9f38a --- /dev/null +++ b/database/log/table_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestLog_Engine_CreateLogTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateLogTable(test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateLogTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateLogTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/log/update.go b/database/log/update.go new file mode 100644 index 000000000..fb7165004 --- /dev/null +++ b/database/log/update.go @@ -0,0 +1,57 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with update.go +package log + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// UpdateLog updates an existing log in the database. +func (e *engine) UpdateLog(l *library.Log) error { + // check what the log entry is for + switch { + case l.GetServiceID() > 0: + e.logger.Tracef("updating log for service %d for build %d in the database", l.GetServiceID(), l.GetBuildID()) + case l.GetStepID() > 0: + e.logger.Tracef("updating log for step %d for build %d in the database", l.GetStepID(), l.GetBuildID()) + } + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#LogFromLibrary + log := database.LogFromLibrary(l) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.Validate + err := log.Validate() + if err != nil { + return err + } + + // compress log data for the resource + // + // https://pkg.go.dev/github.com/go-vela/types/database#Log.Compress + err = log.Compress(e.config.CompressionLevel) + if err != nil { + switch { + case l.GetServiceID() > 0: + return fmt.Errorf("unable to compress log for service %d for build %d: %w", l.GetServiceID(), l.GetBuildID(), err) + case l.GetStepID() > 0: + return fmt.Errorf("unable to compress log for step %d for build %d: %w", l.GetStepID(), l.GetBuildID(), err) + } + } + + // send query to the database + return e.client. + Table(constants.TableLog). + Save(log). + Error +} diff --git a/database/log/update_test.go b/database/log/update_test.go new file mode 100644 index 000000000..0b4e8e127 --- /dev/null +++ b/database/log/update_test.go @@ -0,0 +1,101 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package log + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestLog_Engine_UpdateLog(t *testing.T) { + // setup types + _service := testLog() + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetServiceID(1) + _service.SetData([]byte{}) + + _step := testLog() + _step.SetID(2) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetStepID(1) + _step.SetData([]byte{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the service query + _mock.ExpectExec(`UPDATE "logs" +SET "build_id"=$1,"repo_id"=$2,"service_id"=$3,"step_id"=$4,"data"=$5 +WHERE "id" = $6`). + WithArgs(1, 1, 1, nil, AnyArgument{}, 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // ensure the mock expects the step query + _mock.ExpectExec(`UPDATE "logs" +SET "build_id"=$1,"repo_id"=$2,"service_id"=$3,"step_id"=$4,"data"=$5 +WHERE "id" = $6`). + WithArgs(1, 1, nil, 1, AnyArgument{}, 2). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateLog(_service) + if err != nil { + t.Errorf("unable to create test service log for sqlite: %v", err) + } + + err = _sqlite.CreateLog(_step) + if err != nil { + t.Errorf("unable to create test step log for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + logs []*library.Log + }{ + { + failure: false, + name: "postgres", + database: _postgres, + logs: []*library.Log{_service, _step}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + logs: []*library.Log{_service, _step}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + for _, log := range test.logs { + err = test.database.UpdateLog(log) + + if test.failure { + if err == nil { + t.Errorf("UpdateLog for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateLog for %s returned err: %v", test.name, err) + } + } + }) + } +} diff --git a/database/opts.go b/database/opts.go new file mode 100644 index 000000000..42ab3d033 --- /dev/null +++ b/database/opts.go @@ -0,0 +1,102 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +import ( + "context" + "time" +) + +// EngineOpt represents a configuration option to initialize the database engine. +type EngineOpt func(*engine) error + +// WithAddress sets the address in the database engine. +func WithAddress(address string) EngineOpt { + return func(e *engine) error { + // set the fully qualified connection string in the database engine + e.config.Address = address + + return nil + } +} + +// WithCompressionLevel sets the compression level in the database engine. +func WithCompressionLevel(level int) EngineOpt { + return func(e *engine) error { + // set the level of compression for resources in the database engine + e.config.CompressionLevel = level + + return nil + } +} + +// WithConnectionLife sets the life of connections in the database engine. +func WithConnectionLife(connectionLife time.Duration) EngineOpt { + return func(e *engine) error { + // set the maximum duration of time for connection in the database engine + e.config.ConnectionLife = connectionLife + + return nil + } +} + +// WithConnectionIdle sets the idle connections in the database engine. +func WithConnectionIdle(connectionIdle int) EngineOpt { + return func(e *engine) error { + // set the maximum allowed idle connections in the database engine + e.config.ConnectionIdle = connectionIdle + + return nil + } +} + +// WithConnectionOpen sets the open connections in the database engine. +func WithConnectionOpen(connectionOpen int) EngineOpt { + return func(e *engine) error { + // set the maximum allowed open connections in the database engine + e.config.ConnectionOpen = connectionOpen + + return nil + } +} + +// WithDriver sets the driver in the database engine. +func WithDriver(driver string) EngineOpt { + return func(e *engine) error { + // set the database type to interact with in the database engine + e.config.Driver = driver + + return nil + } +} + +// WithEncryptionKey sets the encryption key in the database engine. +func WithEncryptionKey(encryptionKey string) EngineOpt { + return func(e *engine) error { + // set the key for encrypting resources in the database engine + e.config.EncryptionKey = encryptionKey + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the database engine + e.config.SkipCreation = skipCreation + + return nil + } +} + +// WithContext sets the context in the database engine. +func WithContext(ctx context.Context) EngineOpt { + return func(e *engine) error { + e.ctx = ctx + + return nil + } +} diff --git a/database/opts_test.go b/database/opts_test.go new file mode 100644 index 000000000..446fd5903 --- /dev/null +++ b/database/opts_test.go @@ -0,0 +1,409 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +import ( + "reflect" + "testing" + "time" +) + +func TestDatabase_EngineOpt_WithAddress(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + address string + want string + }{ + { + failure: false, + name: "address set", + address: "file::memory:?cache=shared", + want: "file::memory:?cache=shared", + }, + { + failure: false, + name: "address not set", + address: "", + want: "", + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithAddress(test.address)(e) + + if test.failure { + if err == nil { + t.Errorf("WithAddress for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithAddress for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(e.config.Address, test.want) { + t.Errorf("WithAddress for %s is %v, want %v", test.name, e.config.Address, test.want) + } + }) + } +} + +func TestDatabase_EngineOpt_WithCompressionLevel(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + level int + want int + }{ + { + failure: false, + name: "compression level set to -1", + level: -1, + want: -1, + }, + { + failure: false, + name: "compression level set to 0", + level: 0, + want: 0, + }, + { + failure: false, + name: "compression level set to 1", + level: 1, + want: 1, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithCompressionLevel(test.level)(e) + + if test.failure { + if err == nil { + t.Errorf("WithCompressionLevel for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithCompressionLevel for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(e.config.CompressionLevel, test.want) { + t.Errorf("WithCompressionLevel for %s is %v, want %v", test.name, e.config.CompressionLevel, test.want) + } + }) + } +} + +func TestDatabase_EngineOpt_WithConnectionLife(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + life time.Duration + want time.Duration + }{ + { + failure: false, + name: "life of connections set", + life: 30 * time.Minute, + want: 30 * time.Minute, + }, + { + failure: false, + name: "life of connections not set", + life: 0, + want: 0, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithConnectionLife(test.life)(e) + + if test.failure { + if err == nil { + t.Errorf("WithConnectionLife for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithConnectionLife for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(e.config.ConnectionLife, test.want) { + t.Errorf("WithConnectionLife for %s is %v, want %v", test.name, e.config.ConnectionLife, test.want) + } + }) + } +} + +func TestDatabase_EngineOpt_WithConnectionIdle(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + open int + want int + }{ + { + failure: false, + name: "idle connections set", + open: 2, + want: 2, + }, + { + failure: false, + name: "idle connections not set", + open: 0, + want: 0, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithConnectionIdle(test.open)(e) + + if test.failure { + if err == nil { + t.Errorf("WithConnectionIdle for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithConnectionIdle for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(e.config.ConnectionIdle, test.want) { + t.Errorf("WithConnectionIdle for %s is %v, want %v", test.name, e.config.ConnectionIdle, test.want) + } + }) + } +} + +func TestDatabase_EngineOpt_WithConnectionOpen(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + open int + want int + }{ + { + failure: false, + name: "open connections set", + open: 2, + want: 2, + }, + { + failure: false, + name: "open connections not set", + open: 0, + want: 0, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithConnectionOpen(test.open)(e) + + if test.failure { + if err == nil { + t.Errorf("WithConnectionOpen for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithConnectionOpen for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(e.config.ConnectionOpen, test.want) { + t.Errorf("WithConnectionOpen for %s is %v, want %v", test.name, e.config.ConnectionOpen, test.want) + } + }) + } +} + +func TestDatabase_EngineOpt_WithDriver(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + driver string + want string + }{ + { + failure: false, + name: "driver set", + driver: "sqlite3", + want: "sqlite3", + }, + { + failure: false, + name: "driver not set", + driver: "", + want: "", + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithDriver(test.driver)(e) + + if test.failure { + if err == nil { + t.Errorf("WithDriver for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithDriver for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(e.config.Driver, test.want) { + t.Errorf("WithDriver for %s is %v, want %v", test.name, e.config.Driver, test.want) + } + }) + } +} + +func TestDatabase_EngineOpt_WithEncryptionKey(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + key string + want string + }{ + { + failure: false, + name: "encryption key set", + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + want: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + }, + { + failure: false, + name: "encryption key not set", + key: "", + want: "", + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithEncryptionKey(test.key)(e) + + if test.failure { + if err == nil { + t.Errorf("WithEncryptionKey for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithEncryptionKey for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(e.config.EncryptionKey, test.want) { + t.Errorf("WithEncryptionKey for %s is %v, want %v", test.name, e.config.EncryptionKey, test.want) + } + }) + } +} + +func TestDatabase_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skip bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skip: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skip: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skip)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} diff --git a/database/ping.go b/database/ping.go new file mode 100644 index 000000000..80968c152 --- /dev/null +++ b/database/ping.go @@ -0,0 +1,42 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +import ( + "fmt" + "time" +) + +// Ping sends a "ping" request with backoff to the database. +func (e *engine) Ping() error { + e.logger.Tracef("sending ping request to the %s database", e.Driver()) + + // create a loop to attempt ping requests 5 times + for i := 0; i < 5; i++ { + // capture database/sql database from gorm.io/gorm database + _sql, err := e.client.DB() + if err != nil { + return err + } + + // send ping request to database + err = _sql.Ping() + if err != nil { + // create the duration of time to sleep for before attempting to retry + duration := time.Duration(i+1) * time.Second + + e.logger.Warnf("unable to ping %s database - retrying in %v", e.Driver(), duration) + + // sleep for loop iteration in seconds + time.Sleep(duration) + + continue + } + + return nil + } + + return fmt.Errorf("unable to successfully ping %s database", e.Driver()) +} diff --git a/database/ping_test.go b/database/ping_test.go new file mode 100644 index 000000000..5b2fdb0c9 --- /dev/null +++ b/database/ping_test.go @@ -0,0 +1,82 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +import ( + "testing" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +func TestDatabase_Engine_Ping(t *testing.T) { + _postgres, _mock := testPostgres(t) + defer _postgres.Close() + // ensure the mock expects the ping + _mock.ExpectPing() + + // create a test database without mocking the call + _unmocked, _ := testPostgres(t) + + _sqlite := testSqlite(t) + defer _sqlite.Close() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + name: "success with postgres", + failure: false, + database: _postgres, + }, + { + name: "success with sqlite", + failure: false, + database: _sqlite, + }, + { + name: "failure without mocked call", + failure: true, + database: _unmocked, + }, + { + name: "failure with invalid gorm database", + failure: true, + database: &engine{ + config: &config{ + Driver: "invalid", + }, + client: &gorm.DB{ + Config: &gorm.Config{ + ConnPool: nil, + }, + }, + logger: logrus.NewEntry(logrus.StandardLogger()), + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.Ping() + + if test.failure { + if err == nil { + t.Errorf("Ping for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("Ping for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/pipeline/count.go b/database/pipeline/count.go new file mode 100644 index 000000000..33377d105 --- /dev/null +++ b/database/pipeline/count.go @@ -0,0 +1,27 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/constants" +) + +// CountPipelines gets the count of all pipelines from the database. +func (e *engine) CountPipelines(ctx context.Context) (int64, error) { + e.logger.Tracef("getting count of all pipelines from the database") + + // variable to store query results + var p int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TablePipeline). + Count(&p). + Error + + return p, err +} diff --git a/database/pipeline/count_repo.go b/database/pipeline/count_repo.go new file mode 100644 index 000000000..a3318a4e6 --- /dev/null +++ b/database/pipeline/count_repo.go @@ -0,0 +1,33 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CountPipelinesForRepo gets the count of pipelines by repo ID from the database. +func (e *engine) CountPipelinesForRepo(ctx context.Context, r *library.Repo) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("getting count of pipelines for repo %s from the database", r.GetFullName()) + + // variable to store query results + var p int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TablePipeline). + Where("repo_id = ?", r.GetID()). + Count(&p). + Error + + return p, err +} diff --git a/database/pipeline/count_repo_test.go b/database/pipeline/count_repo_test.go new file mode 100644 index 000000000..0d2469e77 --- /dev/null +++ b/database/pipeline/count_repo_test.go @@ -0,0 +1,99 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestPipeline_Engine_CountPipelinesForRepo(t *testing.T) { + // setup types + _pipelineOne := testPipeline() + _pipelineOne.SetID(1) + _pipelineOne.SetRepoID(1) + _pipelineOne.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + _pipelineOne.SetRef("refs/heads/master") + _pipelineOne.SetType("yaml") + _pipelineOne.SetVersion("1") + + _pipelineTwo := testPipeline() + _pipelineTwo.SetID(2) + _pipelineTwo.SetRepoID(1) + _pipelineTwo.SetCommit("a49aaf4afae6431a79239c95247a2b169fd9f067") + _pipelineTwo.SetRef("refs/heads/main") + _pipelineTwo.SetType("yaml") + _pipelineTwo.SetVersion("1") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "pipelines" WHERE repo_id = $1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreatePipeline(context.TODO(), _pipelineOne) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + _, err = _sqlite.CreatePipeline(context.TODO(), _pipelineTwo) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountPipelinesForRepo(context.TODO(), &library.Repo{ID: _pipelineOne.RepoID}) + + if test.failure { + if err == nil { + t.Errorf("CountPipelinesForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountPipelinesForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountPipelinesForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/pipeline/count_test.go b/database/pipeline/count_test.go new file mode 100644 index 000000000..6b638420a --- /dev/null +++ b/database/pipeline/count_test.go @@ -0,0 +1,98 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestPipeline_Engine_CountPipelines(t *testing.T) { + // setup types + _pipelineOne := testPipeline() + _pipelineOne.SetID(1) + _pipelineOne.SetRepoID(1) + _pipelineOne.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + _pipelineOne.SetRef("refs/heads/master") + _pipelineOne.SetType("yaml") + _pipelineOne.SetVersion("1") + + _pipelineTwo := testPipeline() + _pipelineTwo.SetID(2) + _pipelineTwo.SetRepoID(2) + _pipelineTwo.SetCommit("a49aaf4afae6431a79239c95247a2b169fd9f067") + _pipelineTwo.SetRef("refs/heads/main") + _pipelineTwo.SetType("yaml") + _pipelineTwo.SetVersion("1") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "pipelines"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreatePipeline(context.TODO(), _pipelineOne) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + _, err = _sqlite.CreatePipeline(context.TODO(), _pipelineTwo) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountPipelines(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("CountPipelines for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountPipelines for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountPipelines for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/pipeline/create.go b/database/pipeline/create.go new file mode 100644 index 000000000..04b344c95 --- /dev/null +++ b/database/pipeline/create.go @@ -0,0 +1,55 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreatePipeline creates a new pipeline in the database. +func (e *engine) CreatePipeline(ctx context.Context, p *library.Pipeline) (*library.Pipeline, error) { + e.logger.WithFields(logrus.Fields{ + "pipeline": p.GetCommit(), + }).Tracef("creating pipeline %s in the database", p.GetCommit()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#PipelineFromLibrary + pipeline := database.PipelineFromLibrary(p) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.Validate + err := pipeline.Validate() + if err != nil { + return nil, err + } + + // compress data for the pipeline + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.Compress + err = pipeline.Compress(e.config.CompressionLevel) + if err != nil { + return nil, err + } + + // send query to the database + err = e.client.Table(constants.TablePipeline).Create(pipeline).Error + if err != nil { + return nil, err + } + + err = pipeline.Decompress() + if err != nil { + return nil, err + } + + return pipeline.ToLibrary(), nil +} diff --git a/database/pipeline/create_test.go b/database/pipeline/create_test.go new file mode 100644 index 000000000..9077dad5e --- /dev/null +++ b/database/pipeline/create_test.go @@ -0,0 +1,82 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestPipeline_Engine_CreatePipeline(t *testing.T) { + // setup types + _pipeline := testPipeline() + _pipeline.SetID(1) + _pipeline.SetRepoID(1) + _pipeline.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + _pipeline.SetRef("refs/heads/master") + _pipeline.SetType("yaml") + _pipeline.SetVersion("1") + _pipeline.SetData([]byte{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`INSERT INTO "pipelines" +("repo_id","commit","flavor","platform","ref","type","version","external_secrets","internal_secrets","services","stages","steps","templates","data","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING "id"`). + WithArgs(1, "48afb5bdc41ad69bf22588491333f7cf71135163", nil, nil, "refs/heads/master", "yaml", "1", false, false, false, false, false, false, AnyArgument{}, 1). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CreatePipeline(context.TODO(), _pipeline) + + if test.failure { + if err == nil { + t.Errorf("CreatePipeline for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreatePipeline for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _pipeline) { + t.Errorf("CreatePipeline for %s returned %s, want %s", test.name, got, _pipeline) + } + }) + } +} diff --git a/database/pipeline/delete.go b/database/pipeline/delete.go new file mode 100644 index 000000000..d473d5d4f --- /dev/null +++ b/database/pipeline/delete.go @@ -0,0 +1,32 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// DeletePipeline deletes an existing pipeline from the database. +func (e *engine) DeletePipeline(ctx context.Context, p *library.Pipeline) error { + e.logger.WithFields(logrus.Fields{ + "pipeline": p.GetCommit(), + }).Tracef("deleting pipeline %s from the database", p.GetCommit()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#PipelineFromLibrary + pipeline := database.PipelineFromLibrary(p) + + // send query to the database + return e.client. + Table(constants.TablePipeline). + Delete(pipeline). + Error +} diff --git a/database/pipeline/delete_test.go b/database/pipeline/delete_test.go new file mode 100644 index 000000000..39e47a7dd --- /dev/null +++ b/database/pipeline/delete_test.go @@ -0,0 +1,76 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestPipeline_Engine_DeletePipeline(t *testing.T) { + // setup types + _pipeline := testPipeline() + _pipeline.SetID(1) + _pipeline.SetRepoID(1) + _pipeline.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + _pipeline.SetRef("refs/heads/master") + _pipeline.SetType("yaml") + _pipeline.SetVersion("1") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`DELETE FROM "pipelines" WHERE "pipelines"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreatePipeline(context.TODO(), _pipeline) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeletePipeline(context.TODO(), _pipeline) + + if test.failure { + if err == nil { + t.Errorf("DeletePipeline for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeletePipeline for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/pipeline/get.go b/database/pipeline/get.go new file mode 100644 index 000000000..17ec6ab78 --- /dev/null +++ b/database/pipeline/get.go @@ -0,0 +1,44 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetPipeline gets a pipeline by ID from the database. +func (e *engine) GetPipeline(ctx context.Context, id int64) (*library.Pipeline, error) { + e.logger.Tracef("getting pipeline %d from the database", id) + + // variable to store query results + p := new(database.Pipeline) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TablePipeline). + Where("id = ?", id). + Take(p). + Error + if err != nil { + return nil, err + } + + // decompress data for the pipeline + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.Decompress + err = p.Decompress() + if err != nil { + return nil, err + } + + // return the decompressed pipeline + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.ToLibrary + return p.ToLibrary(), nil +} diff --git a/database/pipeline/get_repo.go b/database/pipeline/get_repo.go new file mode 100644 index 000000000..f165c18eb --- /dev/null +++ b/database/pipeline/get_repo.go @@ -0,0 +1,50 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetPipelineForRepo gets a pipeline by number and repo ID from the database. +func (e *engine) GetPipelineForRepo(ctx context.Context, commit string, r *library.Repo) (*library.Pipeline, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "pipeline": commit, + "repo": r.GetName(), + }).Tracef("getting pipeline %s/%s from the database", r.GetFullName(), commit) + + // variable to store query results + p := new(database.Pipeline) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TablePipeline). + Where("repo_id = ?", r.GetID()). + Where("\"commit\" = ?", commit). + Take(p). + Error + if err != nil { + return nil, err + } + + // decompress data for the pipeline + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.Decompress + err = p.Decompress() + if err != nil { + return nil, err + } + + // return the decompressed pipeline + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.ToLibrary + return p.ToLibrary(), nil +} diff --git a/database/pipeline/get_repo_test.go b/database/pipeline/get_repo_test.go new file mode 100644 index 000000000..67161931d --- /dev/null +++ b/database/pipeline/get_repo_test.go @@ -0,0 +1,89 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestPipeline_Engine_GetPipelineForRepo(t *testing.T) { + // setup types + _pipeline := testPipeline() + _pipeline.SetID(1) + _pipeline.SetRepoID(1) + _pipeline.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + _pipeline.SetRef("refs/heads/master") + _pipeline.SetType("yaml") + _pipeline.SetVersion("1") + _pipeline.SetData([]byte("foo")) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "commit", "flavor", "platform", "ref", "type", "version", "services", "stages", "steps", "templates", "data"}). + AddRow(1, 1, "48afb5bdc41ad69bf22588491333f7cf71135163", "", "", "refs/heads/master", "yaml", "1", false, false, false, false, []byte{120, 94, 74, 203, 207, 7, 4, 0, 0, 255, 255, 2, 130, 1, 69}) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "pipelines" WHERE repo_id = $1 AND "commit" = $2 LIMIT 1`).WithArgs(1, "48afb5bdc41ad69bf22588491333f7cf71135163").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreatePipeline(context.TODO(), _pipeline) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Pipeline + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _pipeline, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _pipeline, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetPipelineForRepo(context.TODO(), "48afb5bdc41ad69bf22588491333f7cf71135163", &library.Repo{ID: _pipeline.RepoID}) + + if test.failure { + if err == nil { + t.Errorf("GetPipelineForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetPipelineForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetPipelineForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/pipeline/get_test.go b/database/pipeline/get_test.go new file mode 100644 index 000000000..ca6d789f2 --- /dev/null +++ b/database/pipeline/get_test.go @@ -0,0 +1,89 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestPipeline_Engine_GetPipeline(t *testing.T) { + // setup types + _pipeline := testPipeline() + _pipeline.SetID(1) + _pipeline.SetRepoID(1) + _pipeline.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + _pipeline.SetRef("refs/heads/master") + _pipeline.SetType("yaml") + _pipeline.SetVersion("1") + _pipeline.SetData([]byte("foo")) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "commit", "flavor", "platform", "ref", "type", "version", "services", "stages", "steps", "templates", "data"}). + AddRow(1, 1, "48afb5bdc41ad69bf22588491333f7cf71135163", "", "", "refs/heads/master", "yaml", "1", false, false, false, false, []byte{120, 94, 74, 203, 207, 7, 4, 0, 0, 255, 255, 2, 130, 1, 69}) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "pipelines" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreatePipeline(context.TODO(), _pipeline) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Pipeline + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _pipeline, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _pipeline, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetPipeline(context.TODO(), 1) + + if test.failure { + if err == nil { + t.Errorf("GetPipeline for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetPipeline for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetPipeline for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/pipeline/index.go b/database/pipeline/index.go new file mode 100644 index 000000000..3189b728d --- /dev/null +++ b/database/pipeline/index.go @@ -0,0 +1,26 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import "context" + +const ( + // CreateRepoIDIndex represents a query to create an + // index on the pipelines table for the repo_id column. + CreateRepoIDIndex = ` +CREATE INDEX +IF NOT EXISTS +pipelines_repo_id +ON pipelines (repo_id); +` +) + +// CreatePipelineIndexes creates the indexes for the pipelines table in the database. +func (e *engine) CreatePipelineIndexes(ctx context.Context) error { + e.logger.Tracef("creating indexes for pipelines table in the database") + + // create the repo_id column index for the pipelines table + return e.client.Exec(CreateRepoIDIndex).Error +} diff --git a/database/pipeline/index_test.go b/database/pipeline/index_test.go new file mode 100644 index 000000000..e72b5a593 --- /dev/null +++ b/database/pipeline/index_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestPipeline_Engine_CreatePipelineIndexes(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreatePipelineIndexes(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("CreatePipelineIndexes for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreatePipelineIndexes for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/pipeline/interface.go b/database/pipeline/interface.go new file mode 100644 index 000000000..ae75ebd54 --- /dev/null +++ b/database/pipeline/interface.go @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/library" +) + +// PipelineInterface represents the Vela interface for pipeline +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type PipelineInterface interface { + // Pipeline Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreatePipelineIndexes defines a function that creates the indexes for the pipelines table. + CreatePipelineIndexes(context.Context) error + // CreatePipelineTable defines a function that creates the pipelines table. + CreatePipelineTable(context.Context, string) error + + // Pipeline Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CountPipelines defines a function that gets the count of all pipelines. + CountPipelines(context.Context) (int64, error) + // CountPipelinesForRepo defines a function that gets the count of pipelines by repo ID. + CountPipelinesForRepo(context.Context, *library.Repo) (int64, error) + // CreatePipeline defines a function that creates a new pipeline. + CreatePipeline(context.Context, *library.Pipeline) (*library.Pipeline, error) + // DeletePipeline defines a function that deletes an existing pipeline. + DeletePipeline(context.Context, *library.Pipeline) error + // GetPipeline defines a function that gets a pipeline by ID. + GetPipeline(context.Context, int64) (*library.Pipeline, error) + // GetPipelineForRepo defines a function that gets a pipeline by commit SHA and repo ID. + GetPipelineForRepo(context.Context, string, *library.Repo) (*library.Pipeline, error) + // ListPipelines defines a function that gets a list of all pipelines. + ListPipelines(context.Context) ([]*library.Pipeline, error) + // ListPipelinesForRepo defines a function that gets a list of pipelines by repo ID. + ListPipelinesForRepo(context.Context, *library.Repo, int, int) ([]*library.Pipeline, int64, error) + // UpdatePipeline defines a function that updates an existing pipeline. + UpdatePipeline(context.Context, *library.Pipeline) (*library.Pipeline, error) +} diff --git a/database/pipeline/list.go b/database/pipeline/list.go new file mode 100644 index 000000000..54f8015c4 --- /dev/null +++ b/database/pipeline/list.go @@ -0,0 +1,64 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListPipelines gets a list of all pipelines from the database. +func (e *engine) ListPipelines(ctx context.Context) ([]*library.Pipeline, error) { + e.logger.Trace("listing all pipelines from the database") + + // variables to store query results and return value + count := int64(0) + p := new([]database.Pipeline) + pipelines := []*library.Pipeline{} + + // count the results + count, err := e.CountPipelines(ctx) + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return pipelines, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TablePipeline). + Find(&p). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, pipeline := range *p { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := pipeline + + // decompress data for the pipeline + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.Decompress + err = tmp.Decompress() + if err != nil { + return nil, err + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.ToLibrary + pipelines = append(pipelines, tmp.ToLibrary()) + } + + return pipelines, nil +} diff --git a/database/pipeline/list_repo.go b/database/pipeline/list_repo.go new file mode 100644 index 000000000..e8c38afab --- /dev/null +++ b/database/pipeline/list_repo.go @@ -0,0 +1,75 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListPipelinesForRepo gets a list of pipelines by repo ID from the database. +// +//nolint:lll // ignore long line length due to variable names +func (e *engine) ListPipelinesForRepo(ctx context.Context, r *library.Repo, page, perPage int) ([]*library.Pipeline, int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("listing pipelines for repo %s from the database", r.GetFullName()) + + // variables to store query results and return values + count := int64(0) + p := new([]database.Pipeline) + pipelines := []*library.Pipeline{} + + // count the results + count, err := e.CountPipelinesForRepo(context.TODO(), r) + if err != nil { + return pipelines, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return pipelines, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + err = e.client. + Table(constants.TablePipeline). + Where("repo_id = ?", r.GetID()). + Limit(perPage). + Offset(offset). + Find(&p). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, pipeline := range *p { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := pipeline + + // decompress data for the pipeline + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.Decompress + err = tmp.Decompress() + if err != nil { + return nil, count, err + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.ToLibrary + pipelines = append(pipelines, tmp.ToLibrary()) + } + + return pipelines, count, nil +} diff --git a/database/pipeline/list_repo_test.go b/database/pipeline/list_repo_test.go new file mode 100644 index 000000000..35cc2e769 --- /dev/null +++ b/database/pipeline/list_repo_test.go @@ -0,0 +1,110 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestPipeline_Engine_ListPipelinesForRepo(t *testing.T) { + // setup types + _pipelineOne := testPipeline() + _pipelineOne.SetID(1) + _pipelineOne.SetRepoID(1) + _pipelineOne.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + _pipelineOne.SetRef("refs/heads/master") + _pipelineOne.SetType("yaml") + _pipelineOne.SetVersion("1") + _pipelineOne.SetData([]byte("foo")) + + _pipelineTwo := testPipeline() + _pipelineTwo.SetID(2) + _pipelineTwo.SetRepoID(1) + _pipelineTwo.SetCommit("a49aaf4afae6431a79239c95247a2b169fd9f067") + _pipelineTwo.SetRef("refs/heads/main") + _pipelineTwo.SetType("yaml") + _pipelineTwo.SetVersion("1") + _pipelineTwo.SetData([]byte("foo")) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "pipelines" WHERE repo_id = $1`).WithArgs(1).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "commit", "flavor", "platform", "ref", "type", "version", "services", "stages", "steps", "templates", "data"}). + AddRow(1, 1, "48afb5bdc41ad69bf22588491333f7cf71135163", "", "", "refs/heads/master", "yaml", "1", false, false, false, false, []byte{120, 94, 74, 203, 207, 7, 4, 0, 0, 255, 255, 2, 130, 1, 69}). + AddRow(2, 1, "a49aaf4afae6431a79239c95247a2b169fd9f067", "", "", "refs/heads/main", "yaml", "1", false, false, false, false, []byte{120, 94, 74, 203, 207, 7, 4, 0, 0, 255, 255, 2, 130, 1, 69}) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "pipelines" WHERE repo_id = $1 LIMIT 10`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreatePipeline(context.TODO(), _pipelineOne) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + _, err = _sqlite.CreatePipeline(context.TODO(), _pipelineTwo) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Pipeline + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Pipeline{_pipelineOne, _pipelineTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Pipeline{_pipelineOne, _pipelineTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListPipelinesForRepo(context.TODO(), &library.Repo{ID: _pipelineOne.RepoID}, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListPipelinesForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListPipelinesForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListPipelinesForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/pipeline/list_test.go b/database/pipeline/list_test.go new file mode 100644 index 000000000..e1d14166e --- /dev/null +++ b/database/pipeline/list_test.go @@ -0,0 +1,110 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestPipeline_Engine_ListPipelines(t *testing.T) { + // setup types + _pipelineOne := testPipeline() + _pipelineOne.SetID(1) + _pipelineOne.SetRepoID(1) + _pipelineOne.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + _pipelineOne.SetRef("refs/heads/master") + _pipelineOne.SetType("yaml") + _pipelineOne.SetVersion("1") + _pipelineOne.SetData([]byte("foo")) + + _pipelineTwo := testPipeline() + _pipelineTwo.SetID(2) + _pipelineTwo.SetRepoID(2) + _pipelineTwo.SetCommit("a49aaf4afae6431a79239c95247a2b169fd9f067") + _pipelineTwo.SetRef("refs/heads/main") + _pipelineTwo.SetType("yaml") + _pipelineTwo.SetVersion("1") + _pipelineTwo.SetData([]byte("foo")) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "pipelines"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "commit", "flavor", "platform", "ref", "type", "version", "services", "stages", "steps", "templates", "data"}). + AddRow(1, 1, "48afb5bdc41ad69bf22588491333f7cf71135163", "", "", "refs/heads/master", "yaml", "1", false, false, false, false, []byte{120, 94, 74, 203, 207, 7, 4, 0, 0, 255, 255, 2, 130, 1, 69}). + AddRow(2, 2, "a49aaf4afae6431a79239c95247a2b169fd9f067", "", "", "refs/heads/main", "yaml", "1", false, false, false, false, []byte{120, 94, 74, 203, 207, 7, 4, 0, 0, 255, 255, 2, 130, 1, 69}) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "pipelines"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreatePipeline(context.TODO(), _pipelineOne) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + _, err = _sqlite.CreatePipeline(context.TODO(), _pipelineTwo) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Pipeline + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Pipeline{_pipelineOne, _pipelineTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Pipeline{_pipelineOne, _pipelineTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListPipelines(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("ListPipelines for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListPipelines for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListPipelines for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/pipeline/opts.go b/database/pipeline/opts.go new file mode 100644 index 000000000..e4d1e6d46 --- /dev/null +++ b/database/pipeline/opts.go @@ -0,0 +1,65 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Pipelines. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Pipelines. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the pipeline engine + e.client = client + + return nil + } +} + +// WithCompressionLevel sets the compression level in the database engine for Pipelines. +func WithCompressionLevel(level int) EngineOpt { + return func(e *engine) error { + // set the compression level in the pipeline engine + e.config.CompressionLevel = level + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Pipelines. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the pipeline engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Pipelines. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the pipeline engine + e.config.SkipCreation = skipCreation + + return nil + } +} + +// WithContext sets the context in the database engine for Pipelines. +func WithContext(ctx context.Context) EngineOpt { + return func(e *engine) error { + e.ctx = ctx + + return nil + } +} diff --git a/database/pipeline/opts_test.go b/database/pipeline/opts_test.go new file mode 100644 index 000000000..2380bdda7 --- /dev/null +++ b/database/pipeline/opts_test.go @@ -0,0 +1,266 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestPipeline_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestPipeline_EngineOpt_WithCompressionLevel(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + level int + want int + }{ + { + failure: false, + name: "compression level set to -1", + level: -1, + want: -1, + }, + { + failure: false, + name: "compression level set to 0", + level: 0, + want: 0, + }, + { + failure: false, + name: "compression level set to 1", + level: 1, + want: 1, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithCompressionLevel(test.level)(e) + + if test.failure { + if err == nil { + t.Errorf("WithCompressionLevel for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithCompressionLevel returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.CompressionLevel, test.want) { + t.Errorf("WithCompressionLevel is %v, want %v", e.config.CompressionLevel, test.want) + } + }) + } +} + +func TestPipeline_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestPipeline_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} + +func TestPipeline_EngineOpt_WithContext(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + ctx context.Context + want context.Context + }{ + { + failure: false, + name: "context set to TODO", + ctx: context.TODO(), + want: context.TODO(), + }, + { + failure: false, + name: "context set to nil", + ctx: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithContext(test.ctx)(e) + + if test.failure { + if err == nil { + t.Errorf("WithContext for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithContext returned err: %v", err) + } + + if !reflect.DeepEqual(e.ctx, test.want) { + t.Errorf("WithContext is %v, want %v", e.ctx, test.want) + } + }) + } +} diff --git a/database/pipeline/pipeline.go b/database/pipeline/pipeline.go new file mode 100644 index 000000000..7c9f29d8f --- /dev/null +++ b/database/pipeline/pipeline.go @@ -0,0 +1,86 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the PipelineInterface interface. + config struct { + // specifies the level of compression to use for the Pipeline engine + CompressionLevel int + // specifies to skip creating tables and indexes for the Pipeline engine + SkipCreation bool + } + + // engine represents the pipeline functionality that implements the PipelineInterface interface. + engine struct { + // engine configuration settings used in pipeline functions + config *config + + ctx context.Context + + // gorm.io/gorm database client used in pipeline functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in pipeline functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with pipelines in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new Pipeline engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + e.ctx = context.TODO() + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating pipeline database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of pipelines table and indexes in the database") + + return e, nil + } + + // create the pipelines table + err := e.CreatePipelineTable(e.ctx, e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TablePipeline, err) + } + + // create the indexes for the pipelines table + err = e.CreatePipelineIndexes(e.ctx) + if err != nil { + return nil, fmt.Errorf("unable to create indexes for %s table: %w", constants.TablePipeline, err) + } + + return e, nil +} diff --git a/database/pipeline/pipeline_test.go b/database/pipeline/pipeline_test.go new file mode 100644 index 000000000..13d01393a --- /dev/null +++ b/database/pipeline/pipeline_test.go @@ -0,0 +1,211 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "database/sql/driver" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestPipeline_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + level int + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + level: 1, + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{CompressionLevel: 1, SkipCreation: false}, + ctx: context.TODO(), + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + level: 1, + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{CompressionLevel: 1, SkipCreation: false}, + ctx: context.TODO(), + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithContext(context.TODO()), + WithClient(test.client), + WithCompressionLevel(test.level), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithCompressionLevel(0), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres pipeline engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithCompressionLevel(0), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite pipeline engine: %v", err) + } + + return _engine +} + +// testPipeline is a test helper function to create a library +// Pipeline type with all fields set to their zero values. +func testPipeline() *library.Pipeline { + return &library.Pipeline{ + ID: new(int64), + RepoID: new(int64), + Commit: new(string), + Flavor: new(string), + Platform: new(string), + Ref: new(string), + Type: new(string), + Version: new(string), + ExternalSecrets: new(bool), + InternalSecrets: new(bool), + Services: new(bool), + Stages: new(bool), + Steps: new(bool), + Templates: new(bool), + Data: new([]byte), + } +} + +// This will be used with the github.com/DATA-DOG/go-sqlmock library to compare values +// that are otherwise not easily compared. These typically would be values generated +// before adding or updating them in the database. +// +// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime +type AnyArgument struct{} + +// Match satisfies sqlmock.Argument interface. +func (a AnyArgument) Match(v driver.Value) bool { + return true +} diff --git a/database/pipeline/table.go b/database/pipeline/table.go new file mode 100644 index 000000000..eef8b885a --- /dev/null +++ b/database/pipeline/table.go @@ -0,0 +1,78 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres pipelines table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +pipelines ( + id SERIAL PRIMARY KEY, + repo_id INTEGER, + commit VARCHAR(500), + flavor VARCHAR(100), + platform VARCHAR(100), + ref VARCHAR(500), + type VARCHAR(100), + version VARCHAR(50), + external_secrets BOOLEAN, + internal_secrets BOOLEAN, + services BOOLEAN, + stages BOOLEAN, + steps BOOLEAN, + templates BOOLEAN, + data BYTEA, + UNIQUE(repo_id, commit) +); +` + + // CreateSqliteTable represents a query to create the Sqlite pipelines table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +pipelines ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_id INTEGER, + 'commit' TEXT, + flavor TEXT, + platform TEXT, + ref TEXT, + type TEXT, + version TEXT, + external_secrets BOOLEAN, + internal_secrets BOOLEAN, + services BOOLEAN, + stages BOOLEAN, + steps BOOLEAN, + templates BOOLEAN, + data BLOB, + UNIQUE(repo_id, 'commit') +); +` +) + +// CreatePipelineTable creates the pipelines table in the database. +func (e *engine) CreatePipelineTable(ctx context.Context, driver string) error { + e.logger.Tracef("creating pipelines table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the pipelines table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the pipelines table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/pipeline/table_test.go b/database/pipeline/table_test.go new file mode 100644 index 000000000..72add7aee --- /dev/null +++ b/database/pipeline/table_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestPipeline_Engine_CreatePipelineTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreatePipelineTable(context.TODO(), test.name) + + if test.failure { + if err == nil { + t.Errorf("CreatePipelineTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreatePipelineTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/pipeline/update.go b/database/pipeline/update.go new file mode 100644 index 000000000..64d9dd561 --- /dev/null +++ b/database/pipeline/update.go @@ -0,0 +1,56 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// UpdatePipeline updates an existing pipeline in the database. +func (e *engine) UpdatePipeline(ctx context.Context, p *library.Pipeline) (*library.Pipeline, error) { + e.logger.WithFields(logrus.Fields{ + "pipeline": p.GetCommit(), + }).Tracef("updating pipeline %s in the database", p.GetCommit()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#PipelineFromLibrary + pipeline := database.PipelineFromLibrary(p) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.Validate + err := pipeline.Validate() + if err != nil { + return nil, err + } + + // compress data for the pipeline + // + // https://pkg.go.dev/github.com/go-vela/types/database#Pipeline.Compress + err = pipeline.Compress(e.config.CompressionLevel) + if err != nil { + return nil, err + } + + // send query to the database + err = e.client.Table(constants.TablePipeline).Save(pipeline).Error + if err != nil { + return nil, err + } + + // decompress pipeline to return + err = pipeline.Decompress() + if err != nil { + return nil, err + } + + return pipeline.ToLibrary(), nil +} diff --git a/database/pipeline/update_test.go b/database/pipeline/update_test.go new file mode 100644 index 000000000..f8ab6b3d3 --- /dev/null +++ b/database/pipeline/update_test.go @@ -0,0 +1,84 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestPipeline_Engine_UpdatePipeline(t *testing.T) { + // setup types + _pipeline := testPipeline() + _pipeline.SetID(1) + _pipeline.SetRepoID(1) + _pipeline.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + _pipeline.SetRef("refs/heads/master") + _pipeline.SetType("yaml") + _pipeline.SetVersion("1") + _pipeline.SetData([]byte{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`UPDATE "pipelines" +SET "repo_id"=$1,"commit"=$2,"flavor"=$3,"platform"=$4,"ref"=$5,"type"=$6,"version"=$7,"external_secrets"=$8,"internal_secrets"=$9,"services"=$10,"stages"=$11,"steps"=$12,"templates"=$13,"data"=$14 +WHERE "id" = $15`). + WithArgs(1, "48afb5bdc41ad69bf22588491333f7cf71135163", nil, nil, "refs/heads/master", "yaml", "1", false, false, false, false, false, false, AnyArgument{}, 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreatePipeline(context.TODO(), _pipeline) + if err != nil { + t.Errorf("unable to create test pipeline for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.UpdatePipeline(context.TODO(), _pipeline) + + if test.failure { + if err == nil { + t.Errorf("UpdatePipeline for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdatePipeline for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _pipeline) { + t.Errorf("UpdatePipeline for %s returned %s, want %s", test.name, got, _pipeline) + } + }) + } +} diff --git a/database/postgres/build.go b/database/postgres/build.go deleted file mode 100644 index 84e4d2140..000000000 --- a/database/postgres/build.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "errors" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetBuild gets a build by number and repo ID from the database. -// -// nolint: dupl // ignore similar code with hook -func (c *client) GetBuild(number int, r *library.Repo) (*library.Build, error) { - c.Logger.WithFields(logrus.Fields{ - "build": number, - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("getting build %s/%d from the database", r.GetFullName(), number) - - // variable to store query results - b := new(database.Build) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableBuild). - Raw(dml.SelectRepoBuild, r.GetID(), number). - Scan(b) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return b.ToLibrary(), result.Error -} - -// GetLastBuild gets the last build by repo ID from the database. -func (c *client) GetLastBuild(r *library.Repo) (*library.Build, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("getting last build for repo %s from the database", r.GetFullName()) - - // variable to store query results - b := new(database.Build) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableBuild). - Raw(dml.SelectLastRepoBuild, r.GetID()). - Scan(b) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - // the record will not exist if it's a new repo - return nil, nil - } - - return b.ToLibrary(), result.Error -} - -// GetLastBuildByBranch gets the last build by repo ID and branch from the database. -func (c *client) GetLastBuildByBranch(r *library.Repo, branch string) (*library.Build, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("getting last build for repo %s on branch %s from the database", r.GetFullName(), branch) - - // variable to store query results - b := new(database.Build) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableBuild). - Raw(dml.SelectLastRepoBuildByBranch, r.GetID(), branch). - Scan(b) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - // the record will not exist if it's a new repo - return nil, nil - } - - return b.ToLibrary(), result.Error -} - -// GetPendingAndRunningBuilds returns the list of pending -// and running builds within the given timeframe. -func (c *client) GetPendingAndRunningBuilds(after string) ([]*library.BuildQueue, error) { - c.Logger.Trace("getting pending and running builds from the database") - - // variable to store query results - b := new([]database.BuildQueue) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableBuild). - Raw(dml.SelectPendingAndRunningBuilds, after). - Scan(b) - - // variable we want to return - builds := []*library.BuildQueue{} - - // iterate through all query results - for _, build := range *b { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := build - - // convert query result to library type - builds = append(builds, tmp.ToLibrary()) - } - - return builds, result.Error -} - -// CreateBuild creates a new build in the database. -func (c *client) CreateBuild(b *library.Build) error { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("creating build %d in the database", b.GetNumber()) - - // cast to database type - build := database.BuildFromLibrary(b) - - // validate the necessary fields are populated - err := build.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Postgres. - Table(constants.TableBuild). - Create(build.Crop()).Error -} - -// UpdateBuild updates a build in the database. -func (c *client) UpdateBuild(b *library.Build) error { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("updating build %d in the database", b.GetNumber()) - - // cast to database type - build := database.BuildFromLibrary(b) - - // validate the necessary fields are populated - err := build.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Postgres. - Table(constants.TableBuild). - Save(build.Crop()).Error -} - -// DeleteBuild deletes a build by unique ID from the database. -func (c *client) DeleteBuild(id int64) error { - c.Logger.Tracef("deleting build %d in the database", id) - - // send query to the database - return c.Postgres. - Table(constants.TableBuild). - Exec(dml.DeleteBuild, id).Error -} diff --git a/database/postgres/build_count.go b/database/postgres/build_count.go deleted file mode 100644 index 0d7af8f5f..000000000 --- a/database/postgres/build_count.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetBuildCount gets a count of all builds from the database. -func (c *client) GetBuildCount() (int64, error) { - c.Logger.Trace("getting count of builds from the database") - - // variable to store query results - var b int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableBuild). - Raw(dml.SelectBuildsCount). - Pluck("count", &b).Error - - return b, err -} - -// GetBuildCountByStatus gets a count of all builds by status from the database. -func (c *client) GetBuildCountByStatus(status string) (int64, error) { - c.Logger.Tracef("getting count of builds by status %s from the database", status) - - // variable to store query results - var b int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableBuild). - Raw(dml.SelectBuildsCountByStatus, status). - Pluck("count", &b).Error - - return b, err -} - -// GetOrgBuildCount gets the count of all builds by repo ID from the database. -func (c *client) GetOrgBuildCount(org string, filters map[string]interface{}) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "org": org, - }).Tracef("getting count of builds for org %s from the database", org) - - // variable to store query results - var b int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableBuild). - Joins("JOIN repos ON builds.repo_id = repos.id and repos.org = ?", org). - Where(filters). - Count(&b).Error - - return b, err -} - -// GetRepoBuildCount gets the count of all builds by repo ID from the database. -func (c *client) GetRepoBuildCount(r *library.Repo, filters map[string]interface{}) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "name": r.GetName(), - }).Tracef("getting count of builds for repo %s from the database", r.GetFullName()) - - // variable to store query results - var b int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableBuild). - Where("repo_id = ?", r.GetID()). - Where(filters). - Count(&b).Error - - return b, err -} diff --git a/database/postgres/build_count_test.go b/database/postgres/build_count_test.go deleted file mode 100644 index ba5a795ca..000000000 --- a/database/postgres/build_count_test.go +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetBuildCount(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectBuildsCount).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetBuildCount() - - if test.failure { - if err == nil { - t.Errorf("GetBuildCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetBuildCountByStatus(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectBuildsCountByStatus, "running").Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetBuildCountByStatus("running") - - if test.failure { - if err == nil { - t.Errorf("GetBuildCountByStatus should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildCountByStatus returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildCountByStatus is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetOrgBuildCount(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT count(*) FROM \"builds\" JOIN repos ON builds.repo_id = repos.id and repos.org = $1").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - filters := map[string]interface{}{} - // run tests - for _, test := range tests { - got, err := _database.GetOrgBuildCount("foo", filters) - - if test.failure { - if err == nil { - t.Errorf("GetOrgBuildCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgBuildCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgBuildCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetOrgBuildCountByEvent(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT count(*) FROM \"builds\" JOIN repos ON builds.repo_id = repos.id and repos.org = $1 WHERE \"event\" = $2").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - filters := map[string]interface{}{ - "event": "push", - } - - // run tests - for _, test := range tests { - got, err := _database.GetOrgBuildCount("foo", filters) - - if test.failure { - if err == nil { - t.Errorf("GetOrgBuildCountByEvent should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgBuildCountByEvent returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgBuildCountByEvent is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetRepoBuildCount(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(`SELECT count(*) FROM "builds" WHERE repo_id = $1`).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - filters := map[string]interface{}{} - - // run tests - for _, test := range tests { - got, err := _database.GetRepoBuildCount(_repo, filters) - - if test.failure { - if err == nil { - t.Errorf("GetRepoBuildCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoBuildCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoBuildCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/build_list.go b/database/postgres/build_list.go deleted file mode 100644 index a40cb93c3..000000000 --- a/database/postgres/build_list.go +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetBuildList gets a list of all builds from the database. -func (c *client) GetBuildList() ([]*library.Build, error) { - c.Logger.Trace("listing builds from the database") - - // variable to store query results - b := new([]database.Build) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableBuild). - Raw(dml.ListBuilds). - Scan(b).Error - - // variable we want to return - builds := []*library.Build{} - // iterate through all query results - for _, build := range *b { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := build - - // convert query result to library type - builds = append(builds, tmp.ToLibrary()) - } - - return builds, err -} - -// GetDeploymentBuildList gets a list of all builds from the database. -func (c *client) GetDeploymentBuildList(deployment string) ([]*library.Build, error) { - c.Logger.WithFields(logrus.Fields{ - "deployment": deployment, - }).Tracef("listing builds for deployment %s from the database", deployment) - - // variable to store query results - b := new([]database.Build) - - filters := map[string]string{} - if len(deployment) > 0 { - filters["source"] = deployment - } - // send query to the database and store result in variable - // - // nolint: gomnd // ignore magic number - err := c.Postgres. - Table(constants.TableBuild). - Select("*"). - Where(filters). - Limit(3). - Order("number DESC"). - Scan(b).Error - - // variable we want to return - builds := []*library.Build{} - // iterate through all query results - for _, build := range *b { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := build - - // convert query result to library type - builds = append(builds, tmp.ToLibrary()) - } - - return builds, err -} - -// GetOrgBuildList gets a list of all builds by org name and allows filters from the database. -// -// nolint: lll // ignore long line length due to variable names -func (c *client) GetOrgBuildList(org string, filters map[string]interface{}, page, perPage int) ([]*library.Build, int64, error) { - c.Logger.WithFields(logrus.Fields{ - "org": org, - }).Tracef("listing builds for org %s from the database", org) - - // variables to store query results - b := new([]database.Build) - builds := []*library.Build{} - count := int64(0) - - // count the results - count, err := c.GetOrgBuildCount(org, filters) - if err != nil { - return builds, 0, err - } - - // short-circuit if there are no results - if count == 0 { - return builds, 0, nil - } - - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err = c.Postgres. - Table(constants.TableBuild). - Select("builds.*"). - Joins("JOIN repos ON builds.repo_id = repos.id and repos.org = ?", org). - Where(filters). - Order("created DESC"). - Order("id"). - Limit(perPage). - Offset(offset). - Scan(b).Error - - // iterate through all query results - for _, build := range *b { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := build - - // convert query result to library type - builds = append(builds, tmp.ToLibrary()) - } - - return builds, count, err -} - -// GetRepoBuildList gets a list of all builds by repo ID from the database. -// -// nolint: lll // ignore long line length due to variable names -func (c *client) GetRepoBuildList(r *library.Repo, filters map[string]interface{}, page, perPage int) ([]*library.Build, int64, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("listing builds for repo %s from the database", r.GetFullName()) - - // variable to store query results - b := new([]database.Build) - builds := []*library.Build{} - count := int64(0) - - // count the results - count, err := c.GetRepoBuildCount(r, filters) - if err != nil { - return builds, 0, err - } - - // short-circuit if there are no results - if count == 0 { - return builds, 0, nil - } - - // calculate offset for pagination through results - offset := perPage * (page - 1) - - err = c.Postgres. - Table(constants.TableBuild). - Where("repo_id = ?", r.GetID()). - Where(filters). - Order("number DESC"). - Limit(perPage). - Offset(offset). - Scan(b).Error - - // iterate through all query results - for _, build := range *b { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := build - - // convert query result to library type - builds = append(builds, tmp.ToLibrary()) - } - - return builds, count, err -} diff --git a/database/postgres/build_list_test.go b/database/postgres/build_list_test.go deleted file mode 100644 index 247e4c282..000000000 --- a/database/postgres/build_list_test.go +++ /dev/null @@ -1,446 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetBuildList(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListBuilds).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "number", "parent", "event", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}, - ).AddRow(1, 1, 1, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). - AddRow(2, 1, 2, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildOne, _buildTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetBuildList() - - if test.failure { - if err == nil { - t.Errorf("GetBuildList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetDeploymentBuildList(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - _buildOne.SetSource("https://github.com/github/octocat/deployments/1") - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - _buildTwo.SetSource("https://github.com/github/octocat/deployments/1") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "number", "parent", "event", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}, - ).AddRow(2, 1, 2, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "https://github.com/github/octocat/deployments/1", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). - AddRow(1, 1, 1, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "https://github.com/github/octocat/deployments/1", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT * FROM \"builds\" WHERE \"source\" = $1 ORDER BY number DESC LIMIT 3").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildTwo, _buildOne}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetDeploymentBuildList("https://github.com/github/octocat/deployments/1") - - if test.failure { - if err == nil { - t.Errorf("GetDeploymentBuildList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetDeploymentBuildList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetDeploymentBuildList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetOrgBuildList(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT count(*) FROM \"builds\" JOIN repos ON builds.repo_id = repos.id and repos.org = $1").WillReturnRows(_rows) - - // create expected return in mock - _rows = sqlmock.NewRows( - []string{"id", "repo_id", "number", "parent", "event", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}, - ).AddRow(1, 1, 1, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). - AddRow(2, 1, 2, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT builds.* FROM \"builds\" JOIN repos ON builds.repo_id = repos.id and repos.org = $1 ORDER BY created DESC,id LIMIT 10").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildOne, _buildTwo}, - }, - } - - filters := map[string]interface{}{} - - // run tests - for _, test := range tests { - got, _, err := _database.GetOrgBuildList("foo", filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetOrgBuildList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgBuildList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgBuildList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetOrgBuildList_NonAdmin(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT count(*) FROM \"builds\" JOIN repos ON builds.repo_id = repos.id and repos.org = $1 WHERE \"visibility\" = $2").WillReturnRows(_rows) - - // create expected return in mock - _rows = sqlmock.NewRows( - []string{"id", "repo_id", "number", "parent", "event", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}, - ).AddRow(1, 1, 1, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT builds.* FROM \"builds\" JOIN repos ON builds.repo_id = repos.id and repos.org = $1 WHERE \"visibility\" = $2 ORDER BY created DESC,id LIMIT 10").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildOne}, - }, - } - - filters := map[string]interface{}{ - "visibility": "public", - } - - // run tests - for _, test := range tests { - got, _, err := _database.GetOrgBuildList("foo", filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetOrgBuildList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgBuildList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgBuildList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetOrgBuildListByEvent(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT count(*) FROM \"builds\" JOIN repos ON builds.repo_id = repos.id and repos.org = $1 WHERE \"event\" = $2").WillReturnRows(_rows) - - // create expected return in mock - _rows = sqlmock.NewRows( - []string{"id", "repo_id", "number", "parent", "event", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}, - ).AddRow(1, 1, 1, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). - AddRow(2, 1, 2, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT builds.* FROM \"builds\" JOIN repos ON builds.repo_id = repos.id and repos.org = $1 WHERE \"event\" = $2 ORDER BY created DESC,id LIMIT 10").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildOne, _buildTwo}, - }, - } - - filters := map[string]interface{}{ - "event": "push", - } - - // run tests - for _, test := range tests { - got, _, err := _database.GetOrgBuildList("foo", filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetOrgBuildListByEvent should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgBuildListByEvent returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgBuildListByEvent is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetRepoBuildList(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(`SELECT count(*) FROM "builds" WHERE repo_id = $1`).WillReturnRows(_rows) - - // create expected return in mock - _rows = sqlmock.NewRows( - []string{"id", "repo_id", "number", "parent", "event", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}, - ).AddRow(1, 1, 1, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0). - AddRow(2, 1, 2, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) - - // ensure the mock expects the query - _mock.ExpectQuery(`SELECT * FROM "builds" WHERE repo_id = $1 ORDER BY number DESC LIMIT 10`).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildOne, _buildTwo}, - }, - } - - filters := map[string]interface{}{} - - // run tests - for _, test := range tests { - got, _, err := _database.GetRepoBuildList(_repo, filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetRepoBuildList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoBuildList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoBuildList is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/build_test.go b/database/postgres/build_test.go deleted file mode 100644 index ea15b41d8..000000000 --- a/database/postgres/build_test.go +++ /dev/null @@ -1,515 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetBuild(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - _build.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectRepoBuild, 1, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "number", "parent", "event", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}, - ).AddRow(1, 1, 1, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Build - }{ - { - failure: false, - want: _build, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetBuild(1, _repo) - - if test.failure { - if err == nil { - t.Errorf("GetBuild should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuild returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuild is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetLastBuild(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - _build.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectLastRepoBuild, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "number", "parent", "event", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}, - ).AddRow(1, 1, 1, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Build - }{ - { - failure: false, - want: _build, - }, - { - failure: false, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetLastBuild(_repo) - - if test.failure { - if err == nil { - t.Errorf("GetLastBuild should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetLastBuild returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetLastBuild is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetLastBuildByBranch(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - _build.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectLastRepoBuildByBranch, 1, "master").Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "number", "parent", "event", "status", "error", "enqueued", "created", "started", "finished", "deploy", "deploy_payload", "clone", "source", "title", "message", "commit", "sender", "author", "email", "link", "branch", "ref", "base_ref", "head_ref", "host", "runtime", "distribution", "timestamp"}, - ).AddRow(1, 1, 1, 0, "", "", "", 0, 0, 0, 0, "", nil, "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", 0) - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Build - }{ - { - failure: false, - want: _build, - }, - { - failure: false, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetLastBuildByBranch(_repo, "master") - - if test.failure { - if err == nil { - t.Errorf("GetLastBuildByBranch should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetLastBuildByBranch returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetLastBuildByBranch is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetPendingAndRunningBuilds(t *testing.T) { - // setup types - _buildOne := new(library.BuildQueue) - _buildOne.SetCreated(0) - _buildOne.SetFullName("") - _buildOne.SetNumber(1) - _buildOne.SetStatus("") - - _buildTwo := new(library.BuildQueue) - _buildTwo.SetCreated(0) - _buildTwo.SetFullName("") - _buildTwo.SetNumber(2) - _buildTwo.SetStatus("") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectPendingAndRunningBuilds, "").Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"created", "full_name", "number", "status"}). - AddRow(0, "", 1, "").AddRow(0, "", 2, "") - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want []*library.BuildQueue - }{ - { - failure: false, - want: []*library.BuildQueue{_buildOne, _buildTwo}, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetPendingAndRunningBuilds("") - - if test.failure { - if err == nil { - t.Errorf("GetPendingAndRunningBuilds should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetPendingAndRunningBuilds returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetPendingAndRunningBuilds is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_CreateBuild(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery(`INSERT INTO "builds" ("repo_id","number","parent","event","status","error","enqueued","created","started","finished","deploy","deploy_payload","clone","source","title","message","commit","sender","author","email","link","branch","ref","base_ref","head_ref","host","runtime","distribution","id") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29) RETURNING "id"`). - WithArgs(1, 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, AnyArgument{}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). - WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.CreateBuild(_build) - - if test.failure { - if err == nil { - t.Errorf("CreateBuild should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateBuild returned err: %v", err) - } - } -} - -func TestPostgres_Client_UpdateBuild(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the query - _mock.ExpectExec(`UPDATE "builds" SET "repo_id"=$1,"number"=$2,"parent"=$3,"event"=$4,"status"=$5,"error"=$6,"enqueued"=$7,"created"=$8,"started"=$9,"finished"=$10,"deploy"=$11,"deploy_payload"=$12,"clone"=$13,"source"=$14,"title"=$15,"message"=$16,"commit"=$17,"sender"=$18,"author"=$19,"email"=$20,"link"=$21,"branch"=$22,"ref"=$23,"base_ref"=$24,"head_ref"=$25,"host"=$26,"runtime"=$27,"distribution"=$28 WHERE "id" = $29`). - WithArgs(1, 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, AnyArgument{}, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.UpdateBuild(_build) - - if test.failure { - if err == nil { - t.Errorf("UpdateBuild should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateBuild returned err: %v", err) - } - } -} - -func TestPostgres_Client_DeleteBuild(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Exec(dml.DeleteBuild, 1).Statement - - // ensure the mock expects the query - _mock.ExpectExec(_query.SQL.String()).WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.DeleteBuild(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteBuild should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteBuild returned err: %v", err) - } - } -} - -// testBuild is a test helper function to create a -// library Build type with all fields set to their -// zero values. -func testBuild() *library.Build { - i64 := int64(0) - i := 0 - str := "" - - return &library.Build{ - ID: &i64, - RepoID: &i64, - Number: &i, - Parent: &i, - Event: &str, - Status: &str, - Error: &str, - Enqueued: &i64, - Created: &i64, - Started: &i64, - Finished: &i64, - Deploy: &str, - Clone: &str, - Source: &str, - Title: &str, - Message: &str, - Commit: &str, - Sender: &str, - Author: &str, - Email: &str, - Link: &str, - Branch: &str, - Ref: &str, - BaseRef: &str, - HeadRef: &str, - Host: &str, - Runtime: &str, - Distribution: &str, - } -} diff --git a/database/postgres/ddl/build.go b/database/postgres/ddl/build.go deleted file mode 100644 index da8f60295..000000000 --- a/database/postgres/ddl/build.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateBuildTable represents a query to - // create the builds table for Vela. - CreateBuildTable = ` -CREATE TABLE -IF NOT EXISTS -builds ( - id SERIAL PRIMARY KEY, - repo_id INTEGER, - number INTEGER, - parent INTEGER, - event VARCHAR(250), - status VARCHAR(250), - error VARCHAR(500), - enqueued INTEGER, - created INTEGER, - started INTEGER, - finished INTEGER, - deploy VARCHAR(500), - deploy_payload VARCHAR(2000), - clone VARCHAR(1000), - source VARCHAR(1000), - title VARCHAR(1000), - message VARCHAR(2000), - commit VARCHAR(500), - sender VARCHAR(250), - author VARCHAR(250), - email VARCHAR(500), - link VARCHAR(1000), - branch VARCHAR(500), - ref VARCHAR(500), - base_ref VARCHAR(500), - head_ref VARCHAR(500), - host VARCHAR(250), - runtime VARCHAR(250), - distribution VARCHAR(250), - timestamp INTEGER, - UNIQUE(repo_id, number) -); -` - - // CreateBuildRepoIDIndex represents a query to create an - // index on the builds table for the repo_id column. - CreateBuildRepoIDIndex = ` -CREATE INDEX -IF NOT EXISTS -builds_repo_id -ON builds (repo_id); -` - - // CreateBuildStatusIndex represents a query to create an - // index on the builds table for the status column. - CreateBuildStatusIndex = ` -CREATE INDEX -IF NOT EXISTS -builds_status -ON builds (status); -` - - // CreateBuildCreatedIndex represents a query to create an - // index on the builds table for the created column. - CreateBuildCreatedIndex = ` -CREATE INDEX CONCURRENTLY -IF NOT EXISTS -builds_created -ON builds (created); -` -) diff --git a/database/postgres/ddl/doc.go b/database/postgres/ddl/doc.go deleted file mode 100644 index 6a21d9d02..000000000 --- a/database/postgres/ddl/doc.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -// Package ddl provides the Postgres data definition language (DDL) for Vela. -// -// https://en.wikipedia.org/wiki/Data_definition_language -// -// Usage: -// -// import "github.com/go-vela/server/database/postgres/ddl" -package ddl diff --git a/database/postgres/ddl/hook.go b/database/postgres/ddl/hook.go deleted file mode 100644 index 98e93e1e1..000000000 --- a/database/postgres/ddl/hook.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateHookTable represents a query to - // create the hooks table for Vela. - CreateHookTable = ` -CREATE TABLE -IF NOT EXISTS -hooks ( - id SERIAL PRIMARY KEY, - repo_id INTEGER, - build_id INTEGER, - number INTEGER, - source_id VARCHAR(250), - created INTEGER, - host VARCHAR(250), - event VARCHAR(250), - branch VARCHAR(500), - error VARCHAR(500), - status VARCHAR(250), - link VARCHAR(1000), - UNIQUE(repo_id, number) -); -` - - // CreateHookRepoIDIndex represents a query to create an - // index on the hooks table for the repo_id column. - CreateHookRepoIDIndex = ` -CREATE INDEX -IF NOT EXISTS -hooks_repo_id -ON hooks (repo_id); -` -) diff --git a/database/postgres/ddl/log.go b/database/postgres/ddl/log.go deleted file mode 100644 index f9c9d7bd9..000000000 --- a/database/postgres/ddl/log.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateLogTable represents a query to - // create the logs table for Vela. - CreateLogTable = ` -CREATE TABLE -IF NOT EXISTS -logs ( - id SERIAL PRIMARY KEY, - build_id INTEGER, - repo_id INTEGER, - service_id INTEGER, - step_id INTEGER, - data BYTEA, - UNIQUE(step_id), - UNIQUE(service_id) -); -` - - // CreateLogBuildIDIndex represents a query to create an - // index on the logs table for the build_id column. - CreateLogBuildIDIndex = ` -CREATE INDEX -IF NOT EXISTS -logs_build_id -ON logs (build_id); -` -) diff --git a/database/postgres/ddl/repo.go b/database/postgres/ddl/repo.go deleted file mode 100644 index f9b01d274..000000000 --- a/database/postgres/ddl/repo.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateRepoTable represents a query to - // create the repos table for Vela. - CreateRepoTable = ` -CREATE TABLE -IF NOT EXISTS -repos ( - id SERIAL PRIMARY KEY, - user_id INTEGER, - hash VARCHAR(500), - org VARCHAR(250), - name VARCHAR(250), - full_name VARCHAR(500), - link VARCHAR(1000), - clone VARCHAR(1000), - branch VARCHAR(250), - build_limit INTEGER, - timeout INTEGER, - counter INTEGER, - visibility TEXT, - private BOOLEAN, - trusted BOOLEAN, - active BOOLEAN, - allow_pull BOOLEAN, - allow_push BOOLEAN, - allow_deploy BOOLEAN, - allow_tag BOOLEAN, - allow_comment BOOLEAN, - pipeline_type TEXT, - previous_name VARCHAR(100), - UNIQUE(full_name) -); -` - - // CreateRepoOrgNameIndex represents a query to create an - // index on the repos table for the org and name columns. - CreateRepoOrgNameIndex = ` -CREATE INDEX -IF NOT EXISTS -repos_org_name -ON repos (org, name); -` -) diff --git a/database/postgres/ddl/secret.go b/database/postgres/ddl/secret.go deleted file mode 100644 index 6139c783a..000000000 --- a/database/postgres/ddl/secret.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateSecretTable represents a query to - // create the secrets table for Vela. - CreateSecretTable = ` -CREATE TABLE -IF NOT EXISTS -secrets ( - id SERIAL PRIMARY KEY, - type VARCHAR(100), - org VARCHAR(250), - repo VARCHAR(250), - team VARCHAR(250), - name VARCHAR(250), - value BYTEA, - images VARCHAR(1000), - events VARCHAR(1000), - allow_command BOOLEAN, - created_at INTEGER, - created_by VARCHAR(250), - updated_at INTEGER, - updated_by VARCHAR(250), - UNIQUE(type, org, repo, name), - UNIQUE(type, org, team, name) -); -` - - // CreateSecretTypeOrgRepo represents a query to create an - // index on the secrets table for the type, org and repo columns. - // - // nolint: gosec // ignore false positive - CreateSecretTypeOrgRepo = ` -CREATE INDEX -IF NOT EXISTS -secrets_type_org_repo -ON secrets (type, org, repo); -` - - // CreateSecretTypeOrgTeam represents a query to create an - // index on the secrets table for the type, org and team columns. - // - // nolint: gosec // ignore false positive - CreateSecretTypeOrgTeam = ` -CREATE INDEX -IF NOT EXISTS -secrets_type_org_team -ON secrets (type, org, team); -` - - // CreateSecretTypeOrg represents a query to create an - // index on the secrets table for the type, and org columns. - // - // nolint: gosec // ignore false positive - CreateSecretTypeOrg = ` -CREATE INDEX -IF NOT EXISTS -secrets_type_org -ON secrets (type, org); -` -) diff --git a/database/postgres/ddl/service.go b/database/postgres/ddl/service.go deleted file mode 100644 index b714586b5..000000000 --- a/database/postgres/ddl/service.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateServiceTable represents a query to - // create the services table for Vela. - CreateServiceTable = ` -CREATE TABLE -IF NOT EXISTS -services ( - id SERIAL PRIMARY KEY, - repo_id INTEGER, - build_id INTEGER, - number INTEGER, - name VARCHAR(250), - image VARCHAR(500), - status VARCHAR(250), - error VARCHAR(500), - exit_code INTEGER, - created INTEGER, - started INTEGER, - finished INTEGER, - host VARCHAR(250), - runtime VARCHAR(250), - distribution VARCHAR(250), - UNIQUE(build_id, number) -); -` -) diff --git a/database/postgres/ddl/step.go b/database/postgres/ddl/step.go deleted file mode 100644 index 8e5540e95..000000000 --- a/database/postgres/ddl/step.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateStepTable represents a query to - // create the steps table for Vela. - CreateStepTable = ` -CREATE TABLE -IF NOT EXISTS -steps ( - id SERIAL PRIMARY KEY, - repo_id INTEGER, - build_id INTEGER, - number INTEGER, - name VARCHAR(250), - image VARCHAR(500), - stage VARCHAR(250), - status VARCHAR(250), - error VARCHAR(500), - exit_code INTEGER, - created INTEGER, - started INTEGER, - finished INTEGER, - host VARCHAR(250), - runtime VARCHAR(250), - distribution VARCHAR(250), - UNIQUE(build_id, number) -); -` -) diff --git a/database/postgres/ddl/user.go b/database/postgres/ddl/user.go deleted file mode 100644 index b6fec0794..000000000 --- a/database/postgres/ddl/user.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateUserTable represents a query to - // create the users table for Vela. - CreateUserTable = ` -CREATE TABLE -IF NOT EXISTS -users ( - id SERIAL PRIMARY KEY, - name VARCHAR(250), - refresh_token VARCHAR(500), - token VARCHAR(500), - hash VARCHAR(500), - favorites VARCHAR(5000), - active BOOLEAN, - admin BOOLEAN, - UNIQUE(name) -); -` - - // CreateUserRefreshIndex represents a query to create an - // index on the users table for the refresh_token column. - CreateUserRefreshIndex = ` -CREATE INDEX -IF NOT EXISTS -users_refresh -ON users (refresh_token); -` -) diff --git a/database/postgres/ddl/worker.go b/database/postgres/ddl/worker.go deleted file mode 100644 index dd8658e7c..000000000 --- a/database/postgres/ddl/worker.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateWorkerTable represents a query to - // create the workers table for Vela. - CreateWorkerTable = ` -CREATE TABLE -IF NOT EXISTS -workers ( - id SERIAL PRIMARY KEY, - hostname VARCHAR(250), - address VARCHAR(250), - routes VARCHAR(1000), - active BOOLEAN, - last_checked_in INTEGER, - build_limit INTEGER, - UNIQUE(hostname) -); -` - - // CreateWorkerHostnameAddressIndex represents a query to create an - // index on the workers table for the hostname and address columns. - CreateWorkerHostnameAddressIndex = ` -CREATE INDEX -IF NOT EXISTS -workers_hostname_address -ON workers (hostname, address); -` -) diff --git a/database/postgres/dml/build.go b/database/postgres/dml/build.go deleted file mode 100644 index 115e7f0a5..000000000 --- a/database/postgres/dml/build.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListBuilds represents a query to - // list all builds in the database. - ListBuilds = ` -SELECT * -FROM builds; -` - - // SelectRepoBuild represents a query to select - // a build for a repo_id in the database. - SelectRepoBuild = ` -SELECT * -FROM builds -WHERE repo_id = ? -AND number = ? -LIMIT 1; -` - - // SelectLastRepoBuild represents a query to select - // the last build for a repo_id in the database. - SelectLastRepoBuild = ` -SELECT * -FROM builds -WHERE repo_id = ? -ORDER BY number DESC -LIMIT 1; -` - - // SelectLastRepoBuildByBranch represents a query to - // select the last build for a repo_id and branch name - // in the database. - SelectLastRepoBuildByBranch = ` -SELECT * -FROM builds -WHERE repo_id = ? -AND branch = ? -ORDER BY number DESC -LIMIT 1; -` - - // SelectBuildsCount represents a query to select - // the count of builds in the database. - SelectBuildsCount = ` -SELECT count(*) as count -FROM builds; -` - - // SelectBuildsCountByStatus represents a query to select - // the count of builds for a status in the database. - SelectBuildsCountByStatus = ` -SELECT count(*) as count -FROM builds -WHERE status = ?; -` - - // DeleteBuild represents a query to - // remove a build from the database. - DeleteBuild = ` -DELETE -FROM builds -WHERE id = ?; -` - - // SelectPendingAndRunningBuilds represents a joined query - // between the builds & repos table to select - // the created builds that are in pending or running builds status - // since the specified timeframe. - SelectPendingAndRunningBuilds = ` -SELECT builds.created, builds.number, builds.status, repos.full_name -FROM builds INNER JOIN repos -ON builds.repo_id = repos.id -WHERE builds.created > ? -AND (builds.status = 'running' OR builds.status = 'pending'); -` -) diff --git a/database/postgres/dml/doc.go b/database/postgres/dml/doc.go deleted file mode 100644 index cdab7d21b..000000000 --- a/database/postgres/dml/doc.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -// Package dml provides the Postgres data manipulation language (DML) for Vela. -// -// https://en.wikipedia.org/wiki/Data_manipulation_language -// -// Usage: -// -// import "github.com/go-vela/server/database/postgres/dml" -package dml diff --git a/database/postgres/dml/hook.go b/database/postgres/dml/hook.go deleted file mode 100644 index b06d79ad5..000000000 --- a/database/postgres/dml/hook.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListHooks represents a query to - // list all webhooks in the database. - ListHooks = ` -SELECT * -FROM hooks; -` - - // ListRepoHooks represents a query to list - // all webhooks for a repo_id in the database. - ListRepoHooks = ` -SELECT * -FROM hooks -WHERE repo_id = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectRepoHookCount represents a query to select - // the count of webhooks for a repo_id in the database. - SelectRepoHookCount = ` -SELECT count(*) as count -FROM hooks -WHERE repo_id = ?; -` - - // SelectRepoHook represents a query to select - // a webhook for a repo_id in the database. - SelectRepoHook = ` -SELECT * -FROM hooks -WHERE repo_id = ? -AND number = ? -LIMIT 1; -` - - // SelectLastRepoHook represents a query to select - // the last hook for a repo_id in the database. - SelectLastRepoHook = ` -SELECT * -FROM hooks -WHERE repo_id = ? -ORDER BY number DESC -LIMIT 1; -` - - // DeleteHook represents a query to - // remove a webhook from the database. - DeleteHook = ` -DELETE -FROM hooks -WHERE id = ?; -` -) diff --git a/database/postgres/dml/log.go b/database/postgres/dml/log.go deleted file mode 100644 index e3e904cc4..000000000 --- a/database/postgres/dml/log.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListLogs represents a query to - // list all logs in the database. - ListLogs = ` -SELECT * -FROM logs; -` - - // ListBuildLogs represents a query to list - // all logs for a build_id in the database. - ListBuildLogs = ` -SELECT * -FROM logs -WHERE build_id = ? -ORDER BY step_id ASC; -` - - // SelectStepLog represents a query to select - // a log for a step_id in the database. - SelectStepLog = ` -SELECT * -FROM logs -WHERE step_id = ? -LIMIT 1; -` - - // SelectServiceLog represents a query to select - // a log for a service_id in the database. - SelectServiceLog = ` -SELECT * -FROM logs -WHERE service_id = ? -LIMIT 1; -` - - // DeleteLog represents a query to - // remove a log from the database. - DeleteLog = ` -DELETE -FROM logs -WHERE id = ?; -` -) diff --git a/database/postgres/dml/repo.go b/database/postgres/dml/repo.go deleted file mode 100644 index 3de708b3e..000000000 --- a/database/postgres/dml/repo.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListRepos represents a query to - // list all repos in the database. - ListRepos = ` -SELECT * -FROM repos; -` - - // ListUserRepos represents a query to list - // all repos for a user_id in the database. - ListUserRepos = ` -SELECT * -FROM repos -WHERE user_id = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectRepo represents a query to select a - // repo for an org and name in the database. - SelectRepo = ` -SELECT * -FROM repos -WHERE org = ? -AND name = ? -LIMIT 1; -` - - // SelectUserReposCount represents a query to select - // the count of repos for a user_id in the database. - SelectUserReposCount = ` -SELECT count(*) as count -FROM repos -WHERE user_id = ?; -` - - // SelectReposCount represents a query to select - // the count of repos in the database. - SelectReposCount = ` -SELECT count(*) as count -FROM repos; -` - - // DeleteRepo represents a query to - // remove a repo from the database. - DeleteRepo = ` -DELETE -FROM repos -WHERE id = ?; -` -) diff --git a/database/postgres/dml/secret.go b/database/postgres/dml/secret.go deleted file mode 100644 index 9fe094a51..000000000 --- a/database/postgres/dml/secret.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListSecrets represents a query to - // list all secrets in the database. - // - // nolint: gosec // ignore false positive - ListSecrets = ` -SELECT * -FROM secrets; -` - - // ListOrgSecrets represents a query to list all - // secrets for a type and org in the database. - // - // nolint: gosec // ignore false positive - ListOrgSecrets = ` -SELECT * -FROM secrets -WHERE type = 'org' -AND org = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // ListRepoSecrets represents a query to list all - // secrets for a type, org and repo in the database. - // - // nolint: gosec // ignore false positive - ListRepoSecrets = ` -SELECT * -FROM secrets -WHERE type = 'repo' -AND org = ? -AND repo = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // ListSharedSecrets represents a query to list all - // secrets for a type, org and team in the database. - // - // nolint: gosec // ignore false positive - ListSharedSecrets = ` -SELECT * -FROM secrets -WHERE type = 'shared' -AND org = ? -AND team = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectOrgSecretsCount represents a query to select the - // count of org secrets for an org in the database. - // - // nolint: gosec // ignore false positive - SelectOrgSecretsCount = ` -SELECT count(*) as count -FROM secrets -WHERE type = 'org' -AND org = ?; -` - - // SelectRepoSecretsCount represents a query to select the - // count of repo secrets for an org and repo in the database. - // - // nolint: gosec // ignore false positive - SelectRepoSecretsCount = ` -SELECT count(*) as count -FROM secrets -WHERE type = 'repo' -AND org = ? -AND repo = ?; -` - - // SelectSharedSecretsCount represents a query to select the - // count of shared secrets for an org and repo in the database. - // - // nolint: gosec // ignore false positive - SelectSharedSecretsCount = ` -SELECT count(*) as count -FROM secrets -WHERE type = 'shared' -AND org = ? -AND team = ?; -` - - // SelectOrgSecret represents a query to select a - // secret for an org and name in the database. - // - // nolint: gosec // ignore false positive - SelectOrgSecret = ` -SELECT * -FROM secrets -WHERE type = 'org' -AND org = ? -AND name = ? -LIMIT 1; -` - - // SelectRepoSecret represents a query to select a - // secret for an org, repo and name in the database. - // - // nolint: gosec // ignore false positive - SelectRepoSecret = ` -SELECT * -FROM secrets -WHERE type = 'repo' -AND org = ? -AND repo = ? -AND name = ? -LIMIT 1; -` - - // SelectSharedSecret represents a query to select a - // secret for an org, team and name in the database. - // - // nolint: gosec // ignore false positive - SelectSharedSecret = ` -SELECT * -FROM secrets -WHERE type = 'shared' -AND org = ? -AND team = ? -AND name = ? -LIMIT 1; -` - - // DeleteSecret represents a query to - // remove a secret from the database. - // - // nolint: gosec // ignore false positive - DeleteSecret = ` -DELETE -FROM secrets -WHERE id = ?; -` -) diff --git a/database/postgres/dml/service.go b/database/postgres/dml/service.go deleted file mode 100644 index 66e009406..000000000 --- a/database/postgres/dml/service.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListServices represents a query to - // list all services in the database. - ListServices = ` -SELECT * -FROM services; -` - - // ListBuildServices represents a query to list - // all services for a build_id in the database. - ListBuildServices = ` -SELECT * -FROM services -WHERE build_id = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectBuildServicesCount represents a query to select - // the count of services for a build_id in the database. - SelectBuildServicesCount = ` -SELECT count(*) as count -FROM services -WHERE build_id = ? -` - - // SelectServiceImagesCount represents a query to select - // the count of an images appearances in the database. - SelectServiceImagesCount = ` -SELECT image, count(image) as count -FROM services -GROUP BY image -` - - // SelectServiceStatusesCount represents a query to select - // the count of service status appearances in the database. - SelectServiceStatusesCount = ` -SELECT status, count(status) as count -FROM services -GROUP BY status; -` - - // SelectBuildService represents a query to select a - // service for a build_id and number in the database. - SelectBuildService = ` -SELECT * -FROM services -WHERE build_id = ? -AND number = ? -LIMIT 1; -` - - // DeleteService represents a query to - // remove a service from the database. - DeleteService = ` -DELETE -FROM services -WHERE id = ?; -` -) diff --git a/database/postgres/dml/step.go b/database/postgres/dml/step.go deleted file mode 100644 index 7aa82b29a..000000000 --- a/database/postgres/dml/step.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListSteps represents a query to - // list all steps in the database. - ListSteps = ` -SELECT * -FROM steps; -` - - // ListBuildSteps represents a query to list - // all steps for a build_id in the database. - ListBuildSteps = ` -SELECT * -FROM steps -WHERE build_id = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectBuildStepsCount represents a query to select - // the count of steps for a build_id in the database. - SelectBuildStepsCount = ` -SELECT count(*) as count -FROM steps -WHERE build_id = ? -` - - // SelectStepImagesCount represents a query to select - // the count of an images appearances in the database. - SelectStepImagesCount = ` -SELECT image, count(image) as count -FROM steps -GROUP BY image; -` - - // SelectStepStatusesCount represents a query to select - // the count of a statuses appearances in the database. - SelectStepStatusesCount = ` -SELECT status, count(status) as count -FROM steps -GROUP BY status; -` - - // SelectBuildStep represents a query to select a - // step for a build_id and number in the database. - SelectBuildStep = ` -SELECT * -FROM steps -WHERE build_id = ? -AND number = ? -LIMIT 1; -` - - // DeleteStep represents a query to - // remove a step from the database. - DeleteStep = ` -DELETE -FROM steps -WHERE id = ?; -` -) diff --git a/database/postgres/dml/user.go b/database/postgres/dml/user.go deleted file mode 100644 index 9febce658..000000000 --- a/database/postgres/dml/user.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListUsers represents a query to - // list all users in the database. - ListUsers = ` -SELECT * -FROM users; -` - - // ListLiteUsers represents a query to - // list all lite users in the database. - ListLiteUsers = ` -SELECT id, name -FROM users -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectUser represents a query to select - // a user for an id in the database. - SelectUser = ` -SELECT * -FROM users -WHERE id = ? -LIMIT 1; -` - - // SelectUserName represents a query to select - // a user for a name in the database. - SelectUserName = ` -SELECT * -FROM users -WHERE name = ? -LIMIT 1; -` - - // SelectUsersCount represents a query to select - // the count of users in the database. - SelectUsersCount = ` -SELECT count(*) as count -FROM users; -` - - // SelectRefreshToken represents a query to select - // a user for a refresh_token in the database. - // - // nolint: gosec // ignore false positive - SelectRefreshToken = ` -SELECT * -FROM users -WHERE refresh_token = ? -LIMIT 1; -` - - // DeleteUser represents a query to - // remove a user from the database. - DeleteUser = ` -DELETE -FROM users -WHERE id = ?; -` -) diff --git a/database/postgres/dml/worker.go b/database/postgres/dml/worker.go deleted file mode 100644 index 3ac74fd2e..000000000 --- a/database/postgres/dml/worker.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListWorkers represents a query to - // list all workers in the database. - ListWorkers = ` -SELECT * -FROM workers; -` - - // SelectWorkersCount represents a query to select the - // count of workers in the database. - SelectWorkersCount = ` -SELECT count(*) as count -FROM workers; -` - - // SelectWorker represents a query to select a - // worker by hostname in the database. - SelectWorker = ` -SELECT * -FROM workers -WHERE hostname = ? -LIMIT 1; -` - - // SelectWorkerByAddress represents a query to select a - // worker by address in the database. - SelectWorkerByAddress = ` -SELECT * -FROM workers -WHERE address = ? -LIMIT 1; -` - - // DeleteWorker represents a query to - // remove a worker from the database. - DeleteWorker = ` -DELETE -FROM workers -WHERE id = ?; -` -) diff --git a/database/postgres/doc.go b/database/postgres/doc.go deleted file mode 100644 index 13879fdbd..000000000 --- a/database/postgres/doc.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -// Package postgres provides the ability for Vela to -// integrate with Postgres as a SQL backend. -// -// Usage: -// -// import "github.com/go-vela/server/database/postgres" -package postgres diff --git a/database/postgres/driver.go b/database/postgres/driver.go deleted file mode 100644 index 2a61cae42..000000000 --- a/database/postgres/driver.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import "github.com/go-vela/types/constants" - -// Driver outputs the configured database driver. -func (c *client) Driver() string { - return constants.DriverPostgres -} diff --git a/database/postgres/driver_test.go b/database/postgres/driver_test.go deleted file mode 100644 index de49bc671..000000000 --- a/database/postgres/driver_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/constants" -) - -func TestPostgres_Client_Driver(t *testing.T) { - // setup types - want := constants.DriverPostgres - - // setup the test database client - _database, _, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // run test - got := _database.Driver() - - if !reflect.DeepEqual(got, want) { - t.Errorf("Driver is %v, want %v", got, want) - } -} diff --git a/database/postgres/hook.go b/database/postgres/hook.go deleted file mode 100644 index 9bd5f8a2d..000000000 --- a/database/postgres/hook.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "errors" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetHook gets a hook by number and repo ID from the database. -// -// nolint: dupl // ignore similar code with build -func (c *client) GetHook(number int, r *library.Repo) (*library.Hook, error) { - c.Logger.WithFields(logrus.Fields{ - "hook": number, - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("getting hook %s/%d from the database", r.GetFullName(), number) - - // variable to store query results - h := new(database.Hook) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableHook). - Raw(dml.SelectRepoHook, r.GetID(), number). - Scan(h) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return h.ToLibrary(), result.Error -} - -// GetLastHook gets the last hook by repo ID from the database. -func (c *client) GetLastHook(r *library.Repo) (*library.Hook, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("getting last hook for repo %s from the database", r.GetFullName()) - - // variable to store query results - h := new(database.Hook) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableHook). - Raw(dml.SelectLastRepoHook, r.GetID()). - Scan(h) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - // the record will not exist if it's a new repo - return nil, nil - } - - return h.ToLibrary(), result.Error -} - -// CreateHook creates a new hook in the database. -func (c *client) CreateHook(h *library.Hook) error { - c.Logger.WithFields(logrus.Fields{ - "hook": h.GetNumber(), - }).Tracef("creating hook %d in the database", h.GetNumber()) - - // cast to database type - hook := database.HookFromLibrary(h) - - // validate the necessary fields are populated - err := hook.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Postgres. - Table(constants.TableHook). - Create(hook).Error -} - -// UpdateHook updates a hook in the database. -func (c *client) UpdateHook(h *library.Hook) error { - c.Logger.WithFields(logrus.Fields{ - "hook": h.GetNumber(), - }).Tracef("updating hook %d in the database", h.GetNumber()) - - // cast to database type - hook := database.HookFromLibrary(h) - - // validate the necessary fields are populated - err := hook.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Postgres. - Table(constants.TableHook). - Save(hook).Error -} - -// DeleteHook deletes a hook by unique ID from the database. -func (c *client) DeleteHook(id int64) error { - c.Logger.Tracef("deleting hook %d in the database", id) - - // send query to the database - return c.Postgres. - Table(constants.TableHook). - Exec(dml.DeleteHook, id).Error -} diff --git a/database/postgres/hook_count.go b/database/postgres/hook_count.go deleted file mode 100644 index 7d65f873e..000000000 --- a/database/postgres/hook_count.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetRepoHookCount gets the count of webhooks by repo ID from the database. -func (c *client) GetRepoHookCount(r *library.Repo) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("getting count of hooks for repo %s from the database", r.GetFullName()) - - // variable to store query results - var h int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableHook). - Raw(dml.SelectRepoHookCount, r.GetID()). - Pluck("count", &h).Error - - return h, err -} diff --git a/database/postgres/hook_count_test.go b/database/postgres/hook_count_test.go deleted file mode 100644 index c86f29446..000000000 --- a/database/postgres/hook_count_test.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetRepoHookCount(t *testing.T) { - // setup types - _hookOne := testHook() - _hookOne.SetID(1) - _hookOne.SetRepoID(1) - _hookOne.SetBuildID(1) - _hookOne.SetNumber(1) - _hookOne.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - _hookTwo := testHook() - _hookTwo.SetID(2) - _hookTwo.SetRepoID(1) - _hookTwo.SetBuildID(2) - _hookTwo.SetNumber(2) - _hookTwo.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectRepoHookCount, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetRepoHookCount(_repo) - - if test.failure { - if err == nil { - t.Errorf("GetRepoHookCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoHookCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoHookCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/hook_list.go b/database/postgres/hook_list.go deleted file mode 100644 index c46867fec..000000000 --- a/database/postgres/hook_list.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetHookList gets a list of all hooks from the database. -func (c *client) GetHookList() ([]*library.Hook, error) { - c.Logger.Trace("listing hooks from the database") - - // variable to store query results - h := new([]database.Hook) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableHook). - Raw(dml.ListHooks). - Scan(h).Error - - // variable we want to return - hooks := []*library.Hook{} - // iterate through all query results - for _, hook := range *h { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := hook - - // convert query result to library type - hooks = append(hooks, tmp.ToLibrary()) - } - - return hooks, err -} - -// GetRepoHookList gets a list of hooks by repo ID from the database. -func (c *client) GetRepoHookList(r *library.Repo, page, perPage int) ([]*library.Hook, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("listing hooks for repo %s from the database", r.GetFullName()) - - // variable to store query results - h := new([]database.Hook) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableHook). - Raw(dml.ListRepoHooks, r.GetID(), perPage, offset). - Scan(h).Error - - // variable we want to return - hooks := []*library.Hook{} - // iterate through all query results - for _, hook := range *h { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := hook - - // convert query result to library type - hooks = append(hooks, tmp.ToLibrary()) - } - - return hooks, err -} diff --git a/database/postgres/hook_list_test.go b/database/postgres/hook_list_test.go deleted file mode 100644 index dbc1524fe..000000000 --- a/database/postgres/hook_list_test.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetHookList(t *testing.T) { - // setup types - _hookOne := testHook() - _hookOne.SetID(1) - _hookOne.SetRepoID(1) - _hookOne.SetBuildID(1) - _hookOne.SetNumber(1) - _hookOne.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - _hookTwo := testHook() - _hookTwo.SetID(2) - _hookTwo.SetRepoID(1) - _hookTwo.SetBuildID(2) - _hookTwo.SetNumber(2) - _hookTwo.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListHooks).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "build_id", "number", "source_id", "created", "host", "event", "branch", "error", "status", "link"}, - ).AddRow(1, 1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", ""). - AddRow(2, 1, 2, 2, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", "") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Hook - }{ - { - failure: false, - want: []*library.Hook{_hookOne, _hookTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetHookList() - - if test.failure { - if err == nil { - t.Errorf("GetHookList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetHookList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetHookList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetRepoHookList(t *testing.T) { - // setup types - _hookOne := testHook() - _hookOne.SetID(1) - _hookOne.SetRepoID(1) - _hookOne.SetBuildID(1) - _hookOne.SetNumber(1) - _hookOne.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - _hookTwo := testHook() - _hookTwo.SetID(2) - _hookTwo.SetRepoID(1) - _hookTwo.SetBuildID(2) - _hookTwo.SetNumber(2) - _hookTwo.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListRepoHooks, 1, 1, 10).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "build_id", "number", "source_id", "created", "host", "event", "branch", "error", "status", "link"}, - ).AddRow(1, 1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", ""). - AddRow(2, 1, 2, 2, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", "") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Hook - }{ - { - failure: false, - want: []*library.Hook{_hookOne, _hookTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetRepoHookList(_repo, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetRepoHookList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoHookList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoHookList is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/hook_test.go b/database/postgres/hook_test.go deleted file mode 100644 index 8d94bd0a2..000000000 --- a/database/postgres/hook_test.go +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetHook(t *testing.T) { - // setup types - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - - _hook := testHook() - _hook.SetID(1) - _hook.SetRepoID(1) - _hook.SetBuildID(1) - _hook.SetNumber(1) - _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectRepoHook, 1, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "build_id", "number", "source_id", "created", "host", "event", "branch", "error", "status", "link"}, - ).AddRow(1, 1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", "") - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Hook - }{ - { - failure: false, - want: _hook, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetHook(1, _repo) - - if test.failure { - if err == nil { - t.Errorf("GetHook should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetHook returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetHook is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetLastHook(t *testing.T) { - // setup types - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - - _hook := testHook() - _hook.SetID(1) - _hook.SetRepoID(1) - _hook.SetBuildID(1) - _hook.SetNumber(1) - _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectLastRepoHook, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "build_id", "number", "source_id", "created", "host", "event", "branch", "error", "status", "link"}, - ).AddRow(1, 1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", 0, "", "", "", "", "", "") - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Hook - }{ - { - failure: false, - want: _hook, - }, - { - failure: false, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetLastHook(_repo) - - if test.failure { - if err == nil { - t.Errorf("GetLastHook should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetLastHook returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetLastHook is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_CreateHook(t *testing.T) { - // setup types - _hook := testHook() - _hook.SetID(1) - _hook.SetRepoID(1) - _hook.SetBuildID(1) - _hook.SetNumber(1) - _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery(`INSERT INTO "hooks" ("repo_id","build_id","number","source_id","created","host","event","branch","error","status","link","id") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) RETURNING "id"`). - WithArgs(1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", nil, nil, nil, nil, nil, nil, nil, 1). - WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.CreateHook(_hook) - - if test.failure { - if err == nil { - t.Errorf("CreateHook should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateHook returned err: %v", err) - } - } -} - -func TestPostgres_Client_UpdateHook(t *testing.T) { - // setup types - _hook := testHook() - _hook.SetID(1) - _hook.SetRepoID(1) - _hook.SetBuildID(1) - _hook.SetNumber(1) - _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the query - _mock.ExpectExec(`UPDATE "hooks" SET "repo_id"=$1,"build_id"=$2,"number"=$3,"source_id"=$4,"created"=$5,"host"=$6,"event"=$7,"branch"=$8,"error"=$9,"status"=$10,"link"=$11 WHERE "id" = $12`). - WithArgs(1, 1, 1, "c8da1302-07d6-11ea-882f-4893bca275b8", nil, nil, nil, nil, nil, nil, nil, 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.UpdateHook(_hook) - - if test.failure { - if err == nil { - t.Errorf("UpdateHook should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateHook returned err: %v", err) - } - } -} - -func TestPostgres_Client_DeleteHook(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.DeleteHook, 1).Statement - - // ensure the mock expects the query - _mock.ExpectExec(_query.SQL.String()).WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.DeleteHook(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteHook should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteHook returned err: %v", err) - } - } -} - -// testHook is a test helper function to create a -// library Hook type with all fields set to their -// zero values. -func testHook() *library.Hook { - i := 0 - i64 := int64(0) - str := "" - - return &library.Hook{ - ID: &i64, - RepoID: &i64, - BuildID: &i64, - Number: &i, - SourceID: &str, - Created: &i64, - Host: &str, - Event: &str, - Branch: &str, - Error: &str, - Status: &str, - Link: &str, - } -} diff --git a/database/postgres/log.go b/database/postgres/log.go deleted file mode 100644 index 7ab708ced..000000000 --- a/database/postgres/log.go +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "errors" - "fmt" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetBuildLogs gets a collection of logs for a build by unique ID from the database. -func (c *client) GetBuildLogs(id int64) ([]*library.Log, error) { - c.Logger.Tracef("listing logs for build %d from the database", id) - - // variable to store query results - l := new([]database.Log) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableLog). - Raw(dml.ListBuildLogs, id). - Scan(l).Error - if err != nil { - return nil, err - } - - // variable we want to return - logs := []*library.Log{} - // iterate through all query results - for _, log := range *l { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := log - - // decompress log data for the step - // - // https://pkg.go.dev/github.com/go-vela/types/database#Log.Decompress - err = tmp.Decompress() - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch uncompressed logs - c.Logger.Errorf("unable to decompress logs for build %d: %v", id, err) - } - - // convert query result to library type - logs = append(logs, tmp.ToLibrary()) - } - - return logs, nil -} - -// GetStepLog gets a log by unique ID from the database. -// -// nolint: dupl // ignore similar code with service -func (c *client) GetStepLog(id int64) (*library.Log, error) { - c.Logger.Tracef("getting log for step %d from the database", id) - - // variable to store query results - l := new(database.Log) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableLog). - Raw(dml.SelectStepLog, id). - Scan(l) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decompress log data for the step - // - // https://pkg.go.dev/github.com/go-vela/types/database#Log.Decompress - err := l.Decompress() - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch uncompressed logs - c.Logger.Errorf("unable to decompress logs for step %d: %v", id, err) - - // return the uncompressed log - return l.ToLibrary(), result.Error - } - - // return the decompressed log - return l.ToLibrary(), result.Error -} - -// GetServiceLog gets a log by unique ID from the database. -// -// nolint: dupl // ignore similar code with step -func (c *client) GetServiceLog(id int64) (*library.Log, error) { - c.Logger.Tracef("getting log for service %d from the database", id) - - // variable to store query results - l := new(database.Log) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableLog). - Raw(dml.SelectServiceLog, id). - Scan(l) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decompress log data for the service - // - // https://pkg.go.dev/github.com/go-vela/types/database#Log.Decompress - err := l.Decompress() - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allowing us to fetch uncompressed logs - c.Logger.Errorf("unable to decompress logs for service %d: %v", id, err) - - // return the uncompressed log - return l.ToLibrary(), result.Error - } - - // return the decompressed log - return l.ToLibrary(), result.Error -} - -// CreateLog creates a new log in the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) CreateLog(l *library.Log) error { - // check if the log entry is for a step - if l.GetStepID() > 0 { - c.Logger.Tracef("creating log for step %d in the database", l.GetStepID()) - } else { - c.Logger.Tracef("creating log for service %d in the database", l.GetServiceID()) - } - - // cast to database type - log := database.LogFromLibrary(l) - - // validate the necessary fields are populated - err := log.Validate() - if err != nil { - return err - } - - // compress log data for the resource - // - // https://pkg.go.dev/github.com/go-vela/types/database#Log.Compress - err = log.Compress(c.config.CompressionLevel) - if err != nil { - return fmt.Errorf("unable to compress logs for step %d: %v", l.GetStepID(), err) - } - - // send query to the database - return c.Postgres. - Table(constants.TableLog). - Create(log).Error -} - -// UpdateLog updates a log in the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) UpdateLog(l *library.Log) error { - // check if the log entry is for a step - if l.GetStepID() > 0 { - c.Logger.Tracef("updating log for step %d in the database", l.GetStepID()) - } else { - c.Logger.Tracef("updating log for service %d in the database", l.GetServiceID()) - } - - // cast to database type - log := database.LogFromLibrary(l) - - // validate the necessary fields are populated - err := log.Validate() - if err != nil { - return err - } - - // compress log data for the resource - // - // https://pkg.go.dev/github.com/go-vela/types/database#Log.Compress - err = log.Compress(c.config.CompressionLevel) - if err != nil { - return fmt.Errorf("unable to compress logs for step %d: %v", l.GetStepID(), err) - } - - // send query to the database - return c.Postgres. - Table(constants.TableLog). - Save(log).Error -} - -// DeleteLog deletes a log by unique ID from the database. -func (c *client) DeleteLog(id int64) error { - c.Logger.Tracef("deleting log %d from the database", id) - - // send query to the database - return c.Postgres. - Table(constants.TableLog). - Exec(dml.DeleteLog, id).Error -} diff --git a/database/postgres/log_test.go b/database/postgres/log_test.go deleted file mode 100644 index 1f4551378..000000000 --- a/database/postgres/log_test.go +++ /dev/null @@ -1,383 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetBuildLogs(t *testing.T) { - // setup types - _logOne := testLog() - _logOne.SetID(1) - _logOne.SetStepID(1) - _logOne.SetBuildID(1) - _logOne.SetRepoID(1) - _logOne.SetData([]byte{}) - - _logTwo := testLog() - _logTwo.SetID(2) - _logTwo.SetServiceID(1) - _logTwo.SetBuildID(1) - _logTwo.SetRepoID(1) - _logTwo.SetData([]byte{}) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListBuildLogs, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "build_id", "repo_id", "service_id", "step_id", "data"}, - ).AddRow(1, 1, 1, 0, 1, []byte{}).AddRow(2, 1, 1, 1, 0, []byte{}) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Log - }{ - { - failure: false, - want: []*library.Log{_logOne, _logTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetBuildLogs(1) - - if test.failure { - if err == nil { - t.Errorf("GetBuildLogs should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildLogs returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildLogs is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetStepLog(t *testing.T) { - // setup types - _log := testLog() - _log.SetID(1) - _log.SetStepID(1) - _log.SetBuildID(1) - _log.SetRepoID(1) - _log.SetData([]byte{}) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectStepLog, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "build_id", "repo_id", "service_id", "step_id", "data"}, - ).AddRow(1, 1, 1, 0, 1, []byte{}) - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Log - }{ - { - failure: false, - want: _log, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetStepLog(1) - - if test.failure { - if err == nil { - t.Errorf("GetStepLog should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetStepLog returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetStepLog is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetServiceLog(t *testing.T) { - // setup types - _log := testLog() - _log.SetID(1) - _log.SetServiceID(1) - _log.SetBuildID(1) - _log.SetRepoID(1) - _log.SetData([]byte{}) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectServiceLog, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "build_id", "repo_id", "service_id", "step_id", "data"}, - ).AddRow(1, 1, 1, 1, 0, []byte{}) - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Log - }{ - { - failure: false, - want: _log, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetServiceLog(1) - - if test.failure { - if err == nil { - t.Errorf("GetServiceLog should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetServiceLog returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetServiceLog is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_CreateLog(t *testing.T) { - // setup types - _log := testLog() - _log.SetID(1) - _log.SetStepID(1) - _log.SetBuildID(1) - _log.SetRepoID(1) - _log.SetData([]byte{}) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery(`INSERT INTO "logs" ("build_id","repo_id","service_id","step_id","data","id") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id"`). - WithArgs(1, 1, nil, 1, AnyArgument{}, 1). - WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.CreateLog(_log) - - if test.failure { - if err == nil { - t.Errorf("CreateLog should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateLog returned err: %v", err) - } - } -} - -func TestPostgres_Client_UpdateLog(t *testing.T) { - // setup types - _log := testLog() - _log.SetID(1) - _log.SetStepID(1) - _log.SetBuildID(1) - _log.SetRepoID(1) - _log.SetData([]byte{}) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the query - _mock.ExpectExec(`UPDATE "logs" SET "build_id"=$1,"repo_id"=$2,"service_id"=$3,"step_id"=$4,"data"=$5 WHERE "id" = $6`). - WithArgs(1, 1, nil, 1, AnyArgument{}, 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.UpdateLog(_log) - - if test.failure { - if err == nil { - t.Errorf("UpdateLog should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateLog returned err: %v", err) - } - } -} - -func TestPostgres_Client_DeleteLog(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Exec(dml.DeleteLog, 1).Statement - - // ensure the mock expects the query - _mock.ExpectExec(_query.SQL.String()).WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.DeleteLog(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteLog should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteLog returned err: %v", err) - } - } -} - -// testLog is a test helper function to create a -// library Log type with all fields set to their -// zero values. -func testLog() *library.Log { - i64 := int64(0) - b := []byte{} - - return &library.Log{ - ID: &i64, - BuildID: &i64, - RepoID: &i64, - ServiceID: &i64, - StepID: &i64, - Data: &b, - } -} diff --git a/database/postgres/opts.go b/database/postgres/opts.go deleted file mode 100644 index 0564f4c0d..000000000 --- a/database/postgres/opts.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "fmt" - "time" -) - -// ClientOpt represents a configuration option to initialize the database client for Postgres. -type ClientOpt func(*client) error - -// WithAddress sets the address in the database client for Postgres. -func WithAddress(address string) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring address in postgres database client") - - // check if the Postgres address provided is empty - if len(address) == 0 { - return fmt.Errorf("no Postgres address provided") - } - - // set the address in the postgres client - c.config.Address = address - - return nil - } -} - -// WithCompressionLevel sets the compression level in the database client for Postgres. -func WithCompressionLevel(level int) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring compression level in postgres database client") - - // set the compression level in the postgres client - c.config.CompressionLevel = level - - return nil - } -} - -// WithConnectionLife sets the connection duration in the database client for Postgres. -func WithConnectionLife(duration time.Duration) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring connection duration in postgres database client") - - // set the connection duration in the postgres client - c.config.ConnectionLife = duration - - return nil - } -} - -// WithConnectionIdle sets the maximum idle connections in the database client for Postgres. -func WithConnectionIdle(idle int) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring maximum idle connections in postgres database client") - - // set the maximum idle connections in the postgres client - c.config.ConnectionIdle = idle - - return nil - } -} - -// WithConnectionOpen sets the maximum open connections in the database client for Postgres. -func WithConnectionOpen(open int) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring maximum open connections in postgres database client") - - // set the maximum open connections in the postgres client - c.config.ConnectionOpen = open - - return nil - } -} - -// WithEncryptionKey sets the encryption key in the database client for Postgres. -func WithEncryptionKey(key string) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring encryption key in postgres database client") - - // check if the Postgres encryption key provided is empty - if len(key) == 0 { - return fmt.Errorf("no Postgres encryption key provided") - } - - // set the encryption key in the postgres client - c.config.EncryptionKey = key - - return nil - } -} - -// WithSkipCreation sets the skip creation logic in the database client for Postgres. -func WithSkipCreation(skipCreation bool) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring skip creating objects in postgres database client") - - // set to skip creating tables and indexes in the postgres client - c.config.SkipCreation = skipCreation - - return nil - } -} diff --git a/database/postgres/opts_test.go b/database/postgres/opts_test.go deleted file mode 100644 index cb3e687da..000000000 --- a/database/postgres/opts_test.go +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - "time" - - "github.com/sirupsen/logrus" -) - -func TestPostgres_ClientOpt_WithAddress(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - failure bool - address string - want string - }{ - { - failure: false, - address: "postgres://foo:bar@localhost:5432/vela", - want: "postgres://foo:bar@localhost:5432/vela", - }, - { - failure: true, - address: "", - want: "", - }, - } - - // run tests - for _, test := range tests { - err := WithAddress(test.address)(c) - - if test.failure { - if err == nil { - t.Errorf("WithAddress should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("WithAddress returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.Address, test.want) { - t.Errorf("WithAddress is %v, want %v", c.config.Address, test.want) - } - } -} - -func TestPostgres_ClientOpt_WithCompressionLevel(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - level int - want int - }{ - { - level: 3, - want: 3, - }, - { - level: 0, - want: 0, - }, - } - - // run tests - for _, test := range tests { - err := WithCompressionLevel(test.level)(c) - - if err != nil { - t.Errorf("WithCompressionLevel returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.CompressionLevel, test.want) { - t.Errorf("WithCompressionLevel is %v, want %v", c.config.CompressionLevel, test.want) - } - } -} - -func TestPostgres_ClientOpt_WithConnectionLife(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - duration time.Duration - want time.Duration - }{ - { - duration: 10 * time.Second, - want: 10 * time.Second, - }, - { - duration: 0, - want: 0, - }, - } - - // run tests - for _, test := range tests { - err := WithConnectionLife(test.duration)(c) - - if err != nil { - t.Errorf("WithConnectionLife returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.ConnectionLife, test.want) { - t.Errorf("WithConnectionLife is %v, want %v", c.config.ConnectionLife, test.want) - } - } -} - -func TestPostgres_ClientOpt_WithConnectionIdle(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - idle int - want int - }{ - { - idle: 5, - want: 5, - }, - { - idle: 0, - want: 0, - }, - } - - // run tests - for _, test := range tests { - err := WithConnectionIdle(test.idle)(c) - - if err != nil { - t.Errorf("WithConnectionIdle returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.ConnectionIdle, test.want) { - t.Errorf("WithConnectionIdle is %v, want %v", c.config.ConnectionIdle, test.want) - } - } -} - -func TestPostgres_ClientOpt_WithConnectionOpen(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - open int - want int - }{ - { - open: 10, - want: 10, - }, - { - open: 0, - want: 0, - }, - } - - // run tests - for _, test := range tests { - err := WithConnectionOpen(test.open)(c) - - if err != nil { - t.Errorf("WithConnectionOpen returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.ConnectionOpen, test.want) { - t.Errorf("WithConnectionOpen is %v, want %v", c.config.ConnectionOpen, test.want) - } - } -} - -func TestPostgres_ClientOpt_WithEncryptionKey(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - failure bool - key string - want string - }{ - { - failure: false, - key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", - want: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", - }, - { - failure: true, - key: "", - want: "", - }, - } - - // run tests - for _, test := range tests { - err := WithEncryptionKey(test.key)(c) - - if test.failure { - if err == nil { - t.Errorf("WithEncryptionKey should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("WithEncryptionKey returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.EncryptionKey, test.want) { - t.Errorf("WithEncryptionKey is %v, want %v", c.config.EncryptionKey, test.want) - } - } -} - -func TestPostgres_ClientOpt_WithSkipCreation(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - skipCreation bool - want bool - }{ - { - skipCreation: true, - want: true, - }, - { - skipCreation: false, - want: false, - }, - } - - // run tests - for _, test := range tests { - err := WithSkipCreation(test.skipCreation)(c) - - if err != nil { - t.Errorf("WithSkipCreation returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.SkipCreation, test.want) { - t.Errorf("WithSkipCreation is %v, want %v", c.config.SkipCreation, test.want) - } - } -} diff --git a/database/postgres/ping.go b/database/postgres/ping.go deleted file mode 100644 index acdc29782..000000000 --- a/database/postgres/ping.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "fmt" - "time" -) - -// Ping sends a "ping" request with backoff to the database. -func (c *client) Ping() error { - c.Logger.Trace("sending ping requests to the database") - - // create a loop to attempt ping requests 5 times - for i := 0; i < 5; i++ { - // capture database/sql database from gorm database - // - // https://pkg.go.dev/gorm.io/gorm#DB.DB - _sql, err := c.Postgres.DB() - if err != nil { - return err - } - - // send ping request to database - // - // https://pkg.go.dev/database/sql#DB.Ping - err = _sql.Ping() - if err != nil { - c.Logger.Debugf("unable to ping database - retrying in %v", time.Duration(i)*time.Second) - - // sleep for loop iteration in seconds - time.Sleep(time.Duration(i) * time.Second) - - // continue to next iteration of the loop - continue - } - - // able to ping database so return with no error - return nil - } - - return fmt.Errorf("unable to successfully ping database") -} diff --git a/database/postgres/ping_test.go b/database/postgres/ping_test.go deleted file mode 100644 index 9853ecd90..000000000 --- a/database/postgres/ping_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "testing" -) - -func TestPostgres_Client_Ping(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the ping - _mock.ExpectPing() - - // setup the closed test database client - _closed, _, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - // capture the closed test sql database - _sql, _ := _closed.Postgres.DB() - // close the test sql database to simulate failures to ping - _sql.Close() - - // setup tests - tests := []struct { - failure bool - database *client - }{ - { - failure: false, - database: _database, - }, - { - failure: true, - database: _closed, - }, - } - - // run tests - for _, test := range tests { - err = test.database.Ping() - - if test.failure { - if err == nil { - t.Errorf("Ping should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("Ping returned err: %v", err) - } - } -} diff --git a/database/postgres/postgres.go b/database/postgres/postgres.go deleted file mode 100644 index c0efe59b4..000000000 --- a/database/postgres/postgres.go +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "fmt" - "time" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/go-vela/server/database/postgres/ddl" - "github.com/go-vela/types/constants" - "github.com/sirupsen/logrus" - - "gorm.io/driver/postgres" - "gorm.io/gorm" -) - -type ( - config struct { - // specifies the address to use for the Postgres client - Address string - // specifies the level of compression to use for the Postgres client - CompressionLevel int - // specifies the connection duration to use for the Postgres client - ConnectionLife time.Duration - // specifies the maximum idle connections for the Postgres client - ConnectionIdle int - // specifies the maximum open connections for the Postgres client - ConnectionOpen int - // specifies the encryption key to use for the Postgres client - EncryptionKey string - // specifies to skip creating tables and indexes for the Postgres client - SkipCreation bool - } - - client struct { - config *config - Postgres *gorm.DB - // https://pkg.go.dev/github.com/sirupsen/logrus#Entry - Logger *logrus.Entry - } -) - -// New returns a Database implementation that integrates with a Postgres instance. -// -// nolint: revive // ignore returning unexported client -func New(opts ...ClientOpt) (*client, error) { - // create new Postgres client - c := new(client) - - // create new fields - c.config = new(config) - c.Postgres = new(gorm.DB) - - // create new logger for the client - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#StandardLogger - logger := logrus.StandardLogger() - - // create new logger for the client - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#NewEntry - c.Logger = logrus.NewEntry(logger).WithField("database", c.Driver()) - - // apply all provided configuration options - for _, opt := range opts { - err := opt(c) - if err != nil { - return nil, err - } - } - - // create the new Postgres database client - // - // https://pkg.go.dev/gorm.io/gorm#Open - _postgres, err := gorm.Open(postgres.Open(c.config.Address), &gorm.Config{}) - if err != nil { - return nil, err - } - - // set the Postgres database client in the Postgres client - c.Postgres = _postgres - - // setup database with proper configuration - err = setupDatabase(c) - if err != nil { - return nil, err - } - - return c, nil -} - -// NewTest returns a Database implementation that integrates with a fake Postgres instance. -// -// This function is intended for running tests only. -// -// nolint: revive // ignore returning unexported client -func NewTest() (*client, sqlmock.Sqlmock, error) { - // create new Postgres client - c := new(client) - - // create new fields - c.config = &config{ - CompressionLevel: 3, - ConnectionLife: 30 * time.Minute, - ConnectionIdle: 2, - ConnectionOpen: 0, - EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", - SkipCreation: false, - } - c.Postgres = new(gorm.DB) - - // create new logger for the client - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#StandardLogger - logger := logrus.StandardLogger() - - // create new logger for the client - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#NewEntry - c.Logger = logrus.NewEntry(logger) - - // create the new mock sql database - // - // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New - _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) - if err != nil { - return nil, nil, err - } - - // create the new mock Postgres database client - // - // https://pkg.go.dev/gorm.io/gorm#Open - c.Postgres, err = gorm.Open( - postgres.New(postgres.Config{Conn: _sql}), - &gorm.Config{SkipDefaultTransaction: true}, - ) - if err != nil { - return nil, nil, err - } - - return c, _mock, nil -} - -// setupDatabase is a helper function to setup -// the database with the proper configuration. -func setupDatabase(c *client) error { - // capture database/sql database from gorm database - // - // https://pkg.go.dev/gorm.io/gorm#DB.DB - _sql, err := c.Postgres.DB() - if err != nil { - return err - } - - // set the maximum amount of time a connection may be reused - // - // https://golang.org/pkg/database/sql/#DB.SetConnMaxLifetime - _sql.SetConnMaxLifetime(c.config.ConnectionLife) - - // set the maximum number of connections in the idle connection pool - // - // https://golang.org/pkg/database/sql/#DB.SetMaxIdleConns - _sql.SetMaxIdleConns(c.config.ConnectionIdle) - - // set the maximum number of open connections to the database - // - // https://golang.org/pkg/database/sql/#DB.SetMaxOpenConns - _sql.SetMaxOpenConns(c.config.ConnectionOpen) - - // verify connection to the database - err = c.Ping() - if err != nil { - return err - } - - // check if we should skip creating database objects - if c.config.SkipCreation { - c.Logger.Warning("skipping creation of data tables and indexes in the postgres database") - - return nil - } - - // create the tables in the database - err = createTables(c) - if err != nil { - return err - } - - // create the indexes in the database - err = createIndexes(c) - if err != nil { - return err - } - - return nil -} - -// createTables is a helper function to setup -// the database with the necessary tables. -func createTables(c *client) error { - c.Logger.Trace("creating data tables in the postgres database") - - // create the builds table - err := c.Postgres.Exec(ddl.CreateBuildTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableBuild, err) - } - - // create the hooks table - err = c.Postgres.Exec(ddl.CreateHookTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableHook, err) - } - - // create the logs table - err = c.Postgres.Exec(ddl.CreateLogTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableLog, err) - } - - // create the repos table - err = c.Postgres.Exec(ddl.CreateRepoTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableRepo, err) - } - - // create the secrets table - err = c.Postgres.Exec(ddl.CreateSecretTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableSecret, err) - } - - // create the services table - err = c.Postgres.Exec(ddl.CreateServiceTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableService, err) - } - - // create the steps table - err = c.Postgres.Exec(ddl.CreateStepTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableStep, err) - } - - // create the users table - err = c.Postgres.Exec(ddl.CreateUserTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableUser, err) - } - - // create the workers table - err = c.Postgres.Exec(ddl.CreateWorkerTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableWorker, err) - } - - return nil -} - -// createIndexes is a helper function to setup -// the database with the necessary indexes. -// -// nolint: lll // ignore long line length due to error messages -func createIndexes(c *client) error { - c.Logger.Trace("creating data indexes in the postgres database") - - // create the builds_repo_id index for the builds table - err := c.Postgres.Exec(ddl.CreateBuildRepoIDIndex).Error - if err != nil { - return fmt.Errorf("unable to create builds_repo_id index for the %s table: %v", constants.TableBuild, err) - } - - // create the builds_status index for the builds table - err = c.Postgres.Exec(ddl.CreateBuildStatusIndex).Error - if err != nil { - return fmt.Errorf("unable to create builds_status index for the %s table: %v", constants.TableBuild, err) - } - - // create the builds_created index for the builds table - err = c.Postgres.Exec(ddl.CreateBuildCreatedIndex).Error - if err != nil { - return fmt.Errorf("unable to create builds_created index for the %s table: %v", constants.TableBuild, err) - } - - // create the hooks_repo_id index for the hooks table - err = c.Postgres.Exec(ddl.CreateHookRepoIDIndex).Error - if err != nil { - return fmt.Errorf("unable to create hooks_repo_id index for the %s table: %v", constants.TableHook, err) - } - - // create the logs_build_id index for the logs table - err = c.Postgres.Exec(ddl.CreateLogBuildIDIndex).Error - if err != nil { - return fmt.Errorf("unable to create logs_build_id index for the %s table: %v", constants.TableLog, err) - } - - // create the repos_org_name index for the repos table - err = c.Postgres.Exec(ddl.CreateRepoOrgNameIndex).Error - if err != nil { - return fmt.Errorf("unable to create repos_org_name index for the %s table: %v", constants.TableRepo, err) - } - - // create the secrets_type_org_repo index for the secrets table - err = c.Postgres.Exec(ddl.CreateSecretTypeOrgRepo).Error - if err != nil { - return fmt.Errorf("unable to create secrets_type_org_repo index for the %s table: %v", constants.TableSecret, err) - } - - // create the secrets_type_org_team index for the secrets table - err = c.Postgres.Exec(ddl.CreateSecretTypeOrgTeam).Error - if err != nil { - return fmt.Errorf("unable to create secrets_type_org_team index for the %s table: %v", constants.TableSecret, err) - } - - // create the secrets_type_org index for the secrets table - err = c.Postgres.Exec(ddl.CreateSecretTypeOrg).Error - if err != nil { - return fmt.Errorf("unable to create secrets_type_org index for the %s table: %v", constants.TableSecret, err) - } - - // create the users_refresh index for the users table - err = c.Postgres.Exec(ddl.CreateUserRefreshIndex).Error - if err != nil { - return fmt.Errorf("unable to create users_refresh index for the %s table: %v", constants.TableUser, err) - } - - // create the workers_hostname_address index for the workers table - err = c.Postgres.Exec(ddl.CreateWorkerHostnameAddressIndex).Error - if err != nil { - return fmt.Errorf("unable to create workers_hostname_address index for the %s table: %v", constants.TableWorker, err) - } - - return nil -} diff --git a/database/postgres/postgres_test.go b/database/postgres/postgres_test.go deleted file mode 100644 index 15859fd3a..000000000 --- a/database/postgres/postgres_test.go +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "database/sql/driver" - "testing" - "time" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - "github.com/go-vela/server/database/postgres/ddl" -) - -func TestPostgres_New(t *testing.T) { - // setup tests - tests := []struct { - failure bool - address string - want string - }{ - { - failure: true, - address: "postgres://foo:bar@localhost:5432/vela", - want: "postgres://foo:bar@localhost:5432/vela", - }, - { - failure: true, - address: "", - want: "", - }, - } - - // run tests - for _, test := range tests { - _, err := New( - WithAddress(test.address), - WithCompressionLevel(3), - WithConnectionLife(10*time.Second), - WithConnectionIdle(5), - WithConnectionOpen(20), - WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), - WithSkipCreation(false), - ) - - if test.failure { - if err == nil { - t.Errorf("New should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("New returned err: %v", err) - } - } -} - -func TestPostgres_setupDatabase(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the ping - _mock.ExpectPing() - - // ensure the mock expects the table queries - _mock.ExpectExec(ddl.CreateBuildTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateHookTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateLogTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateRepoTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateSecretTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateServiceTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateStepTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateUserTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateWorkerTable).WillReturnResult(sqlmock.NewResult(1, 1)) - - // ensure the mock expects the index queries - _mock.ExpectExec(ddl.CreateBuildRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateBuildStatusIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateBuildCreatedIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateHookRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateLogBuildIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateRepoOrgNameIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateSecretTypeOrgRepo).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateSecretTypeOrgTeam).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateSecretTypeOrg).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateWorkerHostnameAddressIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup the skip test database client - _skipDatabase, _skipMock, err := NewTest() - if err != nil { - t.Errorf("unable to create new skip postgres test database: %v", err) - } - defer func() { _sql, _ := _skipDatabase.Postgres.DB(); _sql.Close() }() - - err = WithSkipCreation(true)(_skipDatabase) - if err != nil { - t.Errorf("unable to set SkipCreation for postgres test database: %v", err) - } - - // ensure the mock expects the ping - _skipMock.ExpectPing() - - tests := []struct { - failure bool - database *client - }{ - { - failure: false, - database: _database, - }, - { - failure: false, - database: _skipDatabase, - }, - } - - // run tests - for _, test := range tests { - err := setupDatabase(test.database) - - if test.failure { - if err == nil { - t.Errorf("setupDatabase should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("setupDatabase returned err: %v", err) - } - } -} - -func TestPostgres_createTables(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the table queries - _mock.ExpectExec(ddl.CreateBuildTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateHookTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateLogTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateRepoTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateSecretTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateServiceTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateStepTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateUserTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateWorkerTable).WillReturnResult(sqlmock.NewResult(1, 1)) - - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := createTables(_database) - - if test.failure { - if err == nil { - t.Errorf("createTables should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("createTables returned err: %v", err) - } - } -} - -func TestPostgres_createIndexes(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the index queries - _mock.ExpectExec(ddl.CreateBuildRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateBuildStatusIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateBuildCreatedIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateHookRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateLogBuildIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateRepoOrgNameIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateSecretTypeOrgRepo).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateSecretTypeOrgTeam).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateSecretTypeOrg).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateWorkerHostnameAddressIndex).WillReturnResult(sqlmock.NewResult(1, 1)) - - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := createIndexes(_database) - - if test.failure { - if err == nil { - t.Errorf("createIndexes should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("createIndexes returned err: %v", err) - } - } -} - -// This will be used with the github.com/DATA-DOG/go-sqlmock -// library to compare values that are otherwise not easily -// compared. These typically would be values generated before -// adding or updating them in the database. -// -// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime -type AnyArgument struct{} - -// Match satisfies sqlmock.Argument interface. -func (a AnyArgument) Match(v driver.Value) bool { - return true -} diff --git a/database/postgres/repo.go b/database/postgres/repo.go deleted file mode 100644 index 8085c5a45..000000000 --- a/database/postgres/repo.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "errors" - "fmt" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetRepo gets a repo by org and name from the database. -func (c *client) GetRepo(org, name string) (*library.Repo, error) { - c.Logger.WithFields(logrus.Fields{ - "org": org, - "repo": name, - }).Tracef("getting repo %s/%s from the database", org, name) - - // variable to store query results - r := new(database.Repo) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableRepo). - Raw(dml.SelectRepo, org, name). - Scan(r) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt - err := r.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted repos - c.Logger.Errorf("unable to decrypt repo %s/%s: %v", org, name, err) - - // return the unencrypted repo - return r.ToLibrary(), result.Error - } - - // return the decrypted repo - return r.ToLibrary(), result.Error -} - -// CreateRepo creates a new repo in the database. -// -// nolint: dupl // ignore similar code with update -func (c *client) CreateRepo(r *library.Repo) error { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("creating repo %s in the database", r.GetFullName()) - - // cast to database type - repo := database.RepoFromLibrary(r) - - // validate the necessary fields are populated - err := repo.Validate() - if err != nil { - return err - } - - // encrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Encrypt - err = repo.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt repo %s: %v", r.GetFullName(), err) - } - - // send query to the database - return c.Postgres. - Table(constants.TableRepo). - Create(repo).Error -} - -// UpdateRepo updates a repo in the database. -// -// nolint: dupl // ignore similar code with create -func (c *client) UpdateRepo(r *library.Repo) error { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("updating repo %s in the database", r.GetFullName()) - - // cast to database type - repo := database.RepoFromLibrary(r) - - // validate the necessary fields are populated - err := repo.Validate() - if err != nil { - return err - } - - // encrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Encrypt - err = repo.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt repo %s: %v", r.GetFullName(), err) - } - - // send query to the database - return c.Postgres. - Table(constants.TableRepo). - Save(repo).Error -} - -// DeleteRepo deletes a repo by unique ID from the database. -func (c *client) DeleteRepo(id int64) error { - c.Logger.Tracef("deleting repo %d in the database", id) - - // send query to the database - return c.Postgres. - Table(constants.TableRepo). - Exec(dml.DeleteRepo, id).Error -} diff --git a/database/postgres/repo_count.go b/database/postgres/repo_count.go deleted file mode 100644 index aa3408c1a..000000000 --- a/database/postgres/repo_count.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetRepoCount gets a count of all repos from the database. -func (c *client) GetRepoCount() (int64, error) { - c.Logger.Trace("getting count of repos from the database") - - // variable to store query results - var r int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableRepo). - Raw(dml.SelectReposCount). - Pluck("count", &r).Error - - return r, err -} - -// GetOrgRepoCount gets a count of all repos for a specific org from the database. -func (c *client) GetOrgRepoCount(org string, filters map[string]string) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "org": org, - }).Tracef("getting count of repos for org %s from the database", org) - - // variable to store query results - var r int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableRepo). - Select("count(*)"). - Where("org = ?", org). - Where(filters). - Pluck("count", &r).Error - - return r, err -} - -// GetUserRepoCount gets a count of all repos for a specific user from the database. -func (c *client) GetUserRepoCount(u *library.User) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("getting count of repos for user %s in the database", u.GetName()) - - // variable to store query results - var r int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableRepo). - Raw(dml.SelectUserReposCount, u.GetID()). - Pluck("count", &r).Error - - return r, err -} diff --git a/database/postgres/repo_count_test.go b/database/postgres/repo_count_test.go deleted file mode 100644 index f5a03f7f1..000000000 --- a/database/postgres/repo_count_test.go +++ /dev/null @@ -1,297 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetRepoCount(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - - _repoTwo := testRepo() - _repoTwo.SetID(1) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("public") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectReposCount).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetRepoCount() - - if test.failure { - if err == nil { - t.Errorf("GetRepoCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetUserRepoCount(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - - _repoTwo := testRepo() - _repoTwo.SetID(1) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("public") - - _user := new(library.User) - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectUserReposCount, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetUserRepoCount(_user) - - if test.failure { - if err == nil { - t.Errorf("GetUserRepoCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserRepoCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserRepoCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetOrgRepoCount(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - - _repoTwo := testRepo() - _repoTwo.SetID(1) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("public") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT count(*) FROM \"repos\" WHERE org = $1").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 1, - }, - } - filters := map[string]string{} - // run tests - for _, test := range tests { - got, err := _database.GetOrgRepoCount("foo", filters) - - if test.failure { - if err == nil { - t.Errorf("GetRepoCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetOrgRepoCount_NonAdmin(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - - _repoTwo := testRepo() - _repoTwo.SetID(1) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("private") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT count(*) FROM \"repos\" WHERE (org = $1) AND \"visibility\" = $2").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 1, - }, - } - filters := map[string]string{} - filters["visibility"] = "private" - // run tests - for _, test := range tests { - got, err := _database.GetOrgRepoCount("foo", filters) - - if test.failure { - if err == nil { - t.Errorf("GetRepoCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/repo_list.go b/database/postgres/repo_list.go deleted file mode 100644 index 773d62666..000000000 --- a/database/postgres/repo_list.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetRepoList gets a list of all repos from the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) GetRepoList() ([]*library.Repo, error) { - c.Logger.Trace("listing repos from the database") - - // variable to store query results - r := new([]database.Repo) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableRepo). - Raw(dml.ListRepos). - Scan(r).Error - if err != nil { - return nil, err - } - - // variable we want to return - repos := []*library.Repo{} - // iterate through all query results - for _, repo := range *r { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := repo - - // decrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted repos - c.Logger.Errorf("unable to decrypt repo %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - repos = append(repos, tmp.ToLibrary()) - } - - return repos, nil -} - -// GetOrgRepoList gets a list of all repos by org from the database. -// -// nolint: lll // ignore long line length due to variable names -func (c *client) GetOrgRepoList(org string, filters map[string]string, page, perPage int) ([]*library.Repo, error) { - c.Logger.WithFields(logrus.Fields{ - "org": org, - }).Tracef("listing repos for org %s from the database", org) - - // variable to store query results - r := new([]database.Repo) - - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableRepo). - Where("org = ?", org). - Where(filters). - Order("name"). - Limit(perPage). - Offset(offset). - Scan(r).Error - if err != nil { - return nil, err - } - - // variable we want to return - repos := []*library.Repo{} - // iterate through all query results - for _, repo := range *r { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := repo - - // decrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted repos - c.Logger.Errorf("unable to decrypt repo %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - repos = append(repos, tmp.ToLibrary()) - } - - return repos, nil -} - -// GetUserRepoList gets a list of all repos by user ID from the database. -func (c *client) GetUserRepoList(u *library.User, page, perPage int) ([]*library.Repo, error) { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("listing repos for user %s from the database", u.GetName()) - - // variable to store query results - r := new([]database.Repo) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableRepo). - Raw(dml.ListUserRepos, u.GetID(), perPage, offset). - Scan(r).Error - if err != nil { - return nil, err - } - - // variable we want to return - repos := []*library.Repo{} - // iterate through all query results - for _, repo := range *r { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := repo - - // decrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted repos - c.Logger.Errorf("unable to decrypt repo %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - repos = append(repos, tmp.ToLibrary()) - } - - return repos, nil -} diff --git a/database/postgres/repo_list_test.go b/database/postgres/repo_list_test.go deleted file mode 100644 index 78caad661..000000000 --- a/database/postgres/repo_list_test.go +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetRepoList(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - _repoOne.SetPipelineType("yaml") - _repoOne.SetPreviousName("") - - _repoTwo := testRepo() - _repoTwo.SetID(1) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("public") - _repoTwo.SetPipelineType("yaml") - _repoTwo.SetPreviousName("oldName") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListRepos).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}, - ).AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", ""). - AddRow(1, 1, "baz", "bar", "foo", "bar/foo", "", "", "", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", "oldName") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Repo - }{ - { - failure: false, - want: []*library.Repo{_repoOne, _repoTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetRepoList() - - if test.failure { - if err == nil { - t.Errorf("GetRepoList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetOrgRepoList(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - _repoOne.SetPipelineType("yaml") - _repoOne.SetPreviousName("") - - _repoTwo := testRepo() - _repoTwo.SetID(1) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("foo") - _repoTwo.SetName("baz") - _repoTwo.SetFullName("foo/baz") - _repoTwo.SetVisibility("public") - _repoTwo.SetPipelineType("yaml") - _repoTwo.SetPreviousName("oldName") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}, - ).AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", ""). - AddRow(1, 1, "baz", "foo", "baz", "foo/baz", "", "", "", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", "oldName") - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT * FROM \"repos\" WHERE org = $1 ORDER BY name LIMIT 10").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Repo - }{ - { - failure: false, - want: []*library.Repo{_repoOne, _repoTwo}, - }, - } - filters := map[string]string{} - // run tests - for _, test := range tests { - got, err := _database.GetOrgRepoList("foo", filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetOrgRepoList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgRepoList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgRepoList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetOrgRepoList_NonAdmin(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - _repoOne.SetPipelineType("yaml") - _repoOne.SetPreviousName("") - - _repoTwo := testRepo() - _repoTwo.SetID(1) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("foo") - _repoTwo.SetName("baz") - _repoTwo.SetFullName("foo/baz") - _repoTwo.SetVisibility("private") - _repoTwo.SetPipelineType("yaml") - _repoTwo.SetPreviousName("oldName") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}, - ).AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", "") - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT * FROM \"repos\" WHERE (org = $1) AND \"visibility\" = $2 ORDER BY name LIMIT 10").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Repo - }{ - { - failure: false, - want: []*library.Repo{_repoOne}, - }, - } - filters := map[string]string{} - filters["visibility"] = "public" - // run tests - for _, test := range tests { - got, err := _database.GetOrgRepoList("foo", filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetOrgRepoList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgRepoList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgRepoList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetUserRepoList(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - _repoOne.SetPipelineType("yaml") - _repoOne.SetPreviousName("") - - _repoTwo := testRepo() - _repoTwo.SetID(1) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("public") - _repoTwo.SetPipelineType("yaml") - _repoTwo.SetPreviousName("") - - _user := new(library.User) - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListUserRepos, 1, 1, 10).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}, - ).AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", ""). - AddRow(1, 1, "baz", "bar", "foo", "bar/foo", "", "", "", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", "") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Repo - }{ - { - failure: false, - want: []*library.Repo{_repoOne, _repoTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetUserRepoList(_user, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetUserRepoList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserRepoList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserRepoList is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/repo_test.go b/database/postgres/repo_test.go deleted file mode 100644 index 17f4cd63e..000000000 --- a/database/postgres/repo_test.go +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetRepo(t *testing.T) { - // setup types - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - _repo.SetPipelineType("yaml") - _repo.SetPreviousName("") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectRepo, "foo", "bar").Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "build_limit", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}, - ).AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", 0, 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", "") - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Repo - }{ - { - failure: false, - want: _repo, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetRepo("foo", "bar") - - if test.failure { - if err == nil { - t.Errorf("GetRepo should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepo returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepo is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_CreateRepo(t *testing.T) { - // setup types - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - _repo.SetPipelineType("yaml") - _repo.SetPreviousName("oldName") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery(`INSERT INTO "repos" ("user_id","hash","org","name","full_name","link","clone","branch","build_limit","timeout","counter","visibility","private","trusted","active","allow_pull","allow_push","allow_deploy","allow_tag","allow_comment","pipeline_type","previous_name","id") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23) RETURNING "id"`). - WithArgs(1, AnyArgument{}, "foo", "bar", "foo/bar", nil, nil, nil, AnyArgument{}, AnyArgument{}, AnyArgument{}, "public", false, false, false, false, false, false, false, false, "yaml", "oldName", 1). - WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.CreateRepo(_repo) - - if test.failure { - if err == nil { - t.Errorf("CreateRepo should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateRepo returned err: %v", err) - } - } -} - -func TestPostgres_Client_UpdateRepo(t *testing.T) { - // setup types - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - _repo.SetPipelineType("yaml") - _repo.SetPreviousName("oldName") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the query - _mock.ExpectExec(`UPDATE "repos" SET "user_id"=$1,"hash"=$2,"org"=$3,"name"=$4,"full_name"=$5,"link"=$6,"clone"=$7,"branch"=$8,"build_limit"=$9,"timeout"=$10,"counter"=$11,"visibility"=$12,"private"=$13,"trusted"=$14,"active"=$15,"allow_pull"=$16,"allow_push"=$17,"allow_deploy"=$18,"allow_tag"=$19,"allow_comment"=$20,"pipeline_type"=$21,"previous_name"=$22 WHERE "id" = $23`). - WithArgs(1, AnyArgument{}, "foo", "bar", "foo/bar", nil, nil, nil, AnyArgument{}, AnyArgument{}, AnyArgument{}, "public", false, false, false, false, false, false, false, false, "yaml", "oldName", 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.UpdateRepo(_repo) - - if test.failure { - if err == nil { - t.Errorf("UpdateRepo should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateRepo returned err: %v", err) - } - } -} - -func TestPostgres_Client_DeleteRepo(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Exec(dml.DeleteRepo, 1).Statement - - // ensure the mock expects the query - _mock.ExpectExec(_query.SQL.String()).WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.DeleteRepo(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteRepo should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteRepo returned err: %v", err) - } - } -} - -// testRepo is a test helper function to create a -// library Repo type with all fields set to their -// zero values. -func testRepo() *library.Repo { - i64 := int64(0) - i := 0 - str := "" - b := false - - return &library.Repo{ - ID: &i64, - PipelineType: &str, - UserID: &i64, - Hash: &str, - Org: &str, - Name: &str, - FullName: &str, - Link: &str, - Clone: &str, - Branch: &str, - BuildLimit: &i64, - Timeout: &i64, - Counter: &i, - Visibility: &str, - Private: &b, - Trusted: &b, - Active: &b, - AllowPull: &b, - AllowPush: &b, - AllowDeploy: &b, - AllowTag: &b, - AllowComment: &b, - PreviousName: &str, - } -} diff --git a/database/postgres/secret.go b/database/postgres/secret.go deleted file mode 100644 index 9c5b2a577..000000000 --- a/database/postgres/secret.go +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "errors" - "fmt" - "strings" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetSecret gets a secret by type, org, name (repo or team) and secret name from the database. -func (c *client) GetSecret(t, o, n, secretName string) (*library.Secret, error) { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": o, - "repo": n, - "secret": secretName, - "type": t, - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ - "org": o, - "team": n, - "secret": secretName, - "type": t, - } - } - - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("getting %s secret %s for %s/%s from the database", t, secretName, o, n) - - var err error - - // variable to store query results - s := new(database.Secret) - - // send query to the database and store result in variable - switch t { - case constants.SecretOrg: - result := c.Postgres. - Table(constants.TableSecret). - Raw(dml.SelectOrgSecret, o, secretName). - Scan(s) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - err = result.Error - case constants.SecretRepo: - result := c.Postgres. - Table(constants.TableSecret). - Raw(dml.SelectRepoSecret, o, n, secretName). - Scan(s) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - err = result.Error - case constants.SecretShared: - result := c.Postgres. - Table(constants.TableSecret). - Raw(dml.SelectSharedSecret, o, n, secretName). - Scan(s) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - err = result.Error - } - if err != nil { - return nil, err - } - - // decrypt the value for the secret - // - // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt - err = s.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted secrets - c.Logger.Errorf("unable to decrypt %s secret %s for %s/%s: %v", t, secretName, o, n, err) - - // return the unencrypted secret - return s.ToLibrary(), nil - } - - // return the decrypted secret - return s.ToLibrary(), nil -} - -// CreateSecret creates a new secret in the database. -// -// nolint: dupl // ignore similar code with update -func (c *client) CreateSecret(s *library.Secret) error { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": s.GetOrg(), - "repo": s.GetRepo(), - "secret": s.GetName(), - "type": s.GetType(), - } - - // check if secret is a shared secret - if strings.EqualFold(s.GetType(), constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ - "org": s.GetOrg(), - "team": s.GetTeam(), - "secret": s.GetName(), - "type": s.GetType(), - } - } - - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("creating %s secret %s in the database", s.GetType(), s.GetName()) - - // cast to database type - secret := database.SecretFromLibrary(s) - - // validate the necessary fields are populated - err := secret.Validate() - if err != nil { - return err - } - - // encrypt the value for the secret - // - // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Encrypt - err = secret.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt secret %s: %v", s.GetName(), err) - } - - // send query to the database - return c.Postgres. - Table(constants.TableSecret). - Create(secret.Nullify()).Error -} - -// UpdateSecret updates a secret in the database. -// -// nolint: dupl // ignore similar code with create -func (c *client) UpdateSecret(s *library.Secret) error { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": s.GetOrg(), - "repo": s.GetRepo(), - "secret": s.GetName(), - "type": s.GetType(), - } - - // check if secret is a shared secret - if strings.EqualFold(s.GetType(), constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ - "org": s.GetOrg(), - "team": s.GetTeam(), - "secret": s.GetName(), - "type": s.GetType(), - } - } - - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("updating %s secret %s in the database", s.GetType(), s.GetName()) - - // cast to database type - secret := database.SecretFromLibrary(s) - - // validate the necessary fields are populated - err := secret.Validate() - if err != nil { - return err - } - - // encrypt the value for the secret - // - // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Encrypt - err = secret.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt secret %s: %v", s.GetName(), err) - } - - // send query to the database - return c.Postgres. - Table(constants.TableSecret). - Save(secret.Nullify()).Error -} - -// DeleteSecret deletes a secret by unique ID from the database. -func (c *client) DeleteSecret(id int64) error { - c.Logger.Tracef("deleting secret %d from the database", id) - - // send query to the database - return c.Postgres. - Table(constants.TableSecret). - Exec(dml.DeleteSecret, id).Error -} diff --git a/database/postgres/secret_count.go b/database/postgres/secret_count.go deleted file mode 100644 index 162ee17a5..000000000 --- a/database/postgres/secret_count.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "strings" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" -) - -// GetTypeSecretCount gets a count of secrets by type, -// owner, and name (repo or team) from the database. -func (c *client) GetTypeSecretCount(t, o, n string, teams []string) (int64, error) { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": o, - "repo": n, - "type": t, - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ - "org": o, - "team": n, - "type": t, - } - } - - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("getting count of %s secrets for %s/%s from the database", t, o, n) - - var err error - - // variable to store query results - var s int64 - - // send query to the database and store result in variable - switch t { - case constants.SecretOrg: - err = c.Postgres. - Table(constants.TableSecret). - Raw(dml.SelectOrgSecretsCount, o). - Pluck("count", &s).Error - case constants.SecretRepo: - err = c.Postgres. - Table(constants.TableSecret). - Raw(dml.SelectRepoSecretsCount, o, n). - Pluck("count", &s).Error - case constants.SecretShared: - if n == "*" { - // GitHub teams are not case-sensitive, the DB is lowercase everything for matching - var lowerTeams []string - for _, t := range teams { - lowerTeams = append(lowerTeams, strings.ToLower(t)) - } - err = c.Postgres. - Table(constants.TableSecret). - Select("count(*)"). - Where("type = 'shared' AND org = ?", o). - Where("LOWER(team) IN (?)", lowerTeams). - Pluck("count", &s).Error - } else { - err = c.Postgres. - Table(constants.TableSecret). - Raw(dml.SelectSharedSecretsCount, o, n). - Pluck("count", &s).Error - } - } - - return s, err -} diff --git a/database/postgres/secret_count_test.go b/database/postgres/secret_count_test.go deleted file mode 100644 index f5f465d5a..000000000 --- a/database/postgres/secret_count_test.go +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetTypeSecretCount_Org(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetRepo("*") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("org") - - _secretTwo := testSecret() - _secretTwo.SetID(1) - _secretTwo.SetOrg("foo") - _secretTwo.SetRepo("*") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("org") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectOrgSecretsCount, "foo").Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetTypeSecretCount("org", "foo", "*", []string{}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetTypeSecretCount_Repo(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetRepo("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("repo") - - _secretTwo := testSecret() - _secretTwo.SetID(1) - _secretTwo.SetOrg("foo") - _secretTwo.SetRepo("bar") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("repo") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectRepoSecretsCount, "foo", "bar").Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetTypeSecretCount("repo", "foo", "bar", []string{}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetTypeSecretCount_Shared(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetTeam("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("shared") - - _secretTwo := testSecret() - _secretTwo.SetID(1) - _secretTwo.SetOrg("foo") - _secretTwo.SetTeam("bar") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("shared") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectSharedSecretsCount, "foo", "bar").Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetTypeSecretCount("shared", "foo", "bar", []string{}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetTypeSecretCount_Shared_Wildcard(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetTeam("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("shared") - - _secretTwo := testSecret() - _secretTwo.SetID(1) - _secretTwo.SetOrg("foo") - _secretTwo.SetTeam("bared") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("shared") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT count(*) FROM \"secrets\" WHERE (type = 'shared' AND org = $1) AND LOWER(team) IN ($2,$3)").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetTypeSecretCount("shared", "foo", "*", []string{"bar", "bared"}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/secret_list.go b/database/postgres/secret_list.go deleted file mode 100644 index bdc26c041..000000000 --- a/database/postgres/secret_list.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "strings" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" -) - -// GetSecretList gets a list of all secrets from the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) GetSecretList() ([]*library.Secret, error) { - c.Logger.Tracef("listing secrets from the database") - - // variable to store query results - s := new([]database.Secret) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableSecret). - Raw(dml.ListSecrets). - Scan(s).Error - if err != nil { - return nil, err - } - - // variable we want to return - secrets := []*library.Secret{} - // iterate through all query results - for _, secret := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := secret - - // decrypt the value for the secret - // - // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted secrets - c.Logger.Errorf("unable to decrypt secret %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - secrets = append(secrets, tmp.ToLibrary()) - } - - return secrets, nil -} - -// GetTypeSecretList gets a list of secrets by type, -// owner, and name (repo or team) from the database. -// -// nolint: lll // ignore long line length -func (c *client) GetTypeSecretList(t, o, n string, page, perPage int, teams []string) ([]*library.Secret, error) { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": o, - "repo": n, - "type": t, - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ - "org": o, - "team": n, - "type": t, - } - } - - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("listing %s secrets for %s/%s from the database", t, o, n) - - var err error - // variable to store query results - s := new([]database.Secret) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - switch t { - case constants.SecretOrg: - err = c.Postgres. - Table(constants.TableSecret). - Raw(dml.ListOrgSecrets, o, perPage, offset). - Scan(s).Error - case constants.SecretRepo: - err = c.Postgres. - Table(constants.TableSecret). - Raw(dml.ListRepoSecrets, o, n, perPage, offset). - Scan(s).Error - case constants.SecretShared: - if n == "*" { - // GitHub teams are not case-sensitive, the DB is lowercase everything for matching - var lowerTeams []string - for _, t := range teams { - lowerTeams = append(lowerTeams, strings.ToLower(t)) - } - err = c.Postgres. - Table(constants.TableSecret). - Where("type = 'shared' AND org = ?", o). - Where("LOWER(team) IN (?)", lowerTeams). - Order("id DESC"). - Limit(perPage). - Offset(offset). - Scan(s).Error - } else { - err = c.Postgres. - Table(constants.TableSecret). - Raw(dml.ListSharedSecrets, o, n, perPage, offset). - Scan(s).Error - } - } - if err != nil { - return nil, err - } - - // variable we want to return - secrets := []*library.Secret{} - // iterate through all query results - for _, secret := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := secret - - // decrypt the value for the secret - // - // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted secrets - c.Logger.Errorf("unable to decrypt secret %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - secrets = append(secrets, tmp.ToLibrary()) - } - - return secrets, nil -} diff --git a/database/postgres/secret_list_test.go b/database/postgres/secret_list_test.go deleted file mode 100644 index 12b4b4c34..000000000 --- a/database/postgres/secret_list_test.go +++ /dev/null @@ -1,412 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetSecretList(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetRepo("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("repo") - _secretOne.SetCreatedAt(1) - _secretOne.SetCreatedBy("user") - _secretOne.SetUpdatedAt(1) - _secretOne.SetUpdatedBy("user2") - - _secretTwo := testSecret() - _secretTwo.SetID(1) - _secretTwo.SetOrg("foo") - _secretTwo.SetRepo("bar") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("repo") - _secretTwo.SetCreatedAt(1) - _secretTwo.SetCreatedBy("user") - _secretTwo.SetUpdatedAt(1) - _secretTwo.SetUpdatedBy("user2") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListSecrets).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}, - ).AddRow(1, "repo", "foo", "bar", "", "baz", "foob", "{}", "{}", false, 1, "user", 1, "user2"). - AddRow(1, "repo", "foo", "bar", "", "foob", "baz", "{}", "{}", false, 1, "user", 1, "user2") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Secret - }{ - { - failure: false, - want: []*library.Secret{_secretOne, _secretTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetSecretList() - - if test.failure { - if err == nil { - t.Errorf("GetSecretList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetSecretList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetSecretList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetTypeSecretList_Org(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetRepo("*") - _secretOne.SetName("baz") - _secretOne.SetValue("bar") - _secretOne.SetType("org") - _secretOne.SetCreatedAt(1) - _secretOne.SetCreatedBy("user") - _secretOne.SetUpdatedAt(1) - _secretOne.SetUpdatedBy("user2") - - _secretTwo := testSecret() - _secretTwo.SetID(1) - _secretTwo.SetOrg("foo") - _secretTwo.SetRepo("*") - _secretTwo.SetName("bar") - _secretTwo.SetValue("baz") - _secretTwo.SetType("org") - _secretTwo.SetCreatedAt(1) - _secretTwo.SetCreatedBy("user") - _secretTwo.SetUpdatedAt(1) - _secretTwo.SetUpdatedBy("user2") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListOrgSecrets, "foo", 1, 10).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}, - ).AddRow(1, "org", "foo", "*", "", "baz", "bar", "{}", "{}", false, 1, "user", 1, "user2"). - AddRow(1, "org", "foo", "*", "", "bar", "baz", "{}", "{}", false, 1, "user", 1, "user2") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Secret - }{ - { - failure: false, - want: []*library.Secret{_secretOne, _secretTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetTypeSecretList("org", "foo", "*", 1, 10, []string{}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetTypeSecretList_Repo(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetRepo("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("repo") - _secretOne.SetCreatedAt(1) - _secretOne.SetCreatedBy("user") - _secretOne.SetUpdatedAt(1) - _secretOne.SetUpdatedBy("user2") - - _secretTwo := testSecret() - _secretTwo.SetID(1) - _secretTwo.SetOrg("foo") - _secretTwo.SetRepo("bar") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("repo") - _secretTwo.SetCreatedAt(1) - _secretTwo.SetCreatedBy("user") - _secretTwo.SetUpdatedAt(1) - _secretTwo.SetUpdatedBy("user2") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListRepoSecrets, "foo", "bar", 1, 10).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}, - ).AddRow(1, "repo", "foo", "bar", "", "baz", "foob", "{}", "{}", false, 1, "user", 1, "user2"). - AddRow(1, "repo", "foo", "bar", "", "foob", "baz", "{}", "{}", false, 1, "user", 1, "user2") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Secret - }{ - { - failure: false, - want: []*library.Secret{_secretOne, _secretTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetTypeSecretList("repo", "foo", "bar", 1, 10, []string{}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetTypeSecretList_Shared(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetTeam("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("shared") - _secretOne.SetCreatedAt(1) - _secretOne.SetCreatedBy("user") - _secretOne.SetUpdatedAt(1) - _secretOne.SetUpdatedBy("user2") - - _secretTwo := testSecret() - _secretTwo.SetID(1) - _secretTwo.SetOrg("foo") - _secretTwo.SetTeam("bar") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("shared") - _secretTwo.SetCreatedAt(1) - _secretTwo.SetCreatedBy("user") - _secretTwo.SetUpdatedAt(1) - _secretTwo.SetUpdatedBy("user2") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListSharedSecrets, "foo", "bar", 1, 10).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}, - ).AddRow(1, "shared", "foo", "", "bar", "baz", "foob", "{}", "{}", false, 1, "user", 1, "user2"). - AddRow(1, "shared", "foo", "", "bar", "foob", "baz", "{}", "{}", false, 1, "user", 1, "user2") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Secret - }{ - { - failure: false, - want: []*library.Secret{_secretOne, _secretTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetTypeSecretList("shared", "foo", "bar", 1, 10, []string{"bar"}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetTypeSecretList_Shared_Wildcard(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetTeam("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("shared") - _secretOne.SetCreatedAt(1) - _secretOne.SetCreatedBy("user") - _secretOne.SetUpdatedAt(1) - _secretOne.SetUpdatedBy("user2") - - _secretTwo := testSecret() - _secretTwo.SetID(1) - _secretTwo.SetOrg("foo") - _secretTwo.SetTeam("bared") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("shared") - _secretTwo.SetCreatedAt(1) - _secretTwo.SetCreatedBy("user") - _secretTwo.SetUpdatedAt(1) - _secretTwo.SetUpdatedBy("user2") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}, - ).AddRow(1, "shared", "foo", "", "bar", "baz", "foob", "{}", "{}", false, 1, "user", 1, "user2"). - AddRow(1, "shared", "foo", "", "bared", "foob", "baz", "{}", "{}", false, 1, "user", 1, "user2") - - // ensure the mock expects the query - _mock.ExpectQuery("SELECT * FROM \"secrets\" WHERE (type = 'shared' AND org = $1) AND LOWER(team) IN ($2,$3) ORDER BY id DESC LIMIT 10").WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Secret - }{ - { - failure: false, - want: []*library.Secret{_secretOne, _secretTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetTypeSecretList("shared", "foo", "*", 1, 10, []string{"bar", "bared"}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretList is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/secret_test.go b/database/postgres/secret_test.go deleted file mode 100644 index 9ea9c7c19..000000000 --- a/database/postgres/secret_test.go +++ /dev/null @@ -1,418 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - "time" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetSecret_Org(t *testing.T) { - // setup types - _secret := testSecret() - _secret.SetID(1) - _secret.SetOrg("foo") - _secret.SetRepo("*") - _secret.SetName("bar") - _secret.SetValue("baz") - _secret.SetType("org") - _secret.SetCreatedAt(1) - _secret.SetCreatedBy("user") - _secret.SetUpdatedAt(1) - _secret.SetUpdatedBy("user2") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectOrgSecret, "foo", "bar").Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}, - ).AddRow(1, "org", "foo", "*", "", "bar", "baz", "{}", "{}", false, 1, "user", 1, "user2") - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Secret - }{ - { - failure: false, - want: _secret, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetSecret("org", "foo", "*", "bar") - - if test.failure { - if err == nil { - t.Errorf("GetSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetSecret returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetSecret is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetSecret_Repo(t *testing.T) { - // setup types - _secret := testSecret() - _secret.SetID(1) - _secret.SetOrg("foo") - _secret.SetRepo("bar") - _secret.SetName("baz") - _secret.SetValue("foob") - _secret.SetType("repo") - _secret.SetCreatedAt(1) - _secret.SetCreatedBy("user") - _secret.SetUpdatedAt(1) - _secret.SetUpdatedBy("user2") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectRepoSecret, "foo", "bar", "baz").Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}, - ).AddRow(1, "repo", "foo", "bar", "", "baz", "foob", "{}", "{}", false, 1, "user", 1, "user2") - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Secret - }{ - { - failure: false, - want: _secret, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetSecret("repo", "foo", "bar", "baz") - - if test.failure { - if err == nil { - t.Errorf("GetSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetSecret returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetSecret is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetSecret_Shared(t *testing.T) { - // setup types - _secret := testSecret() - _secret.SetID(1) - _secret.SetOrg("foo") - _secret.SetTeam("bar") - _secret.SetName("baz") - _secret.SetValue("foob") - _secret.SetType("shared") - _secret.SetCreatedAt(1) - _secret.SetCreatedBy("user") - _secret.SetUpdatedAt(1) - _secret.SetUpdatedBy("user2") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectSharedSecret, "foo", "bar", "baz").Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}, - ).AddRow(1, "shared", "foo", "", "bar", "baz", "foob", "{}", "{}", false, 1, "user", 1, "user2") - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Secret - }{ - { - failure: false, - want: _secret, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetSecret("shared", "foo", "bar", "baz") - - if test.failure { - if err == nil { - t.Errorf("GetSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetSecret returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetSecret is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_CreateSecret(t *testing.T) { - // setup types - _secret := testSecret() - _secret.SetID(1) - _secret.SetOrg("foo") - _secret.SetRepo("bar") - _secret.SetName("baz") - _secret.SetValue("foob") - _secret.SetType("repo") - _secret.SetCreatedAt(1) - _secret.SetCreatedBy("user") - _secret.SetUpdatedAt(1) - _secret.SetUpdatedBy("user2") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery(`INSERT INTO "secrets" ("org","repo","team","name","value","type","images","events","allow_command","created_at","created_by","updated_at","updated_by","id") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING "id"`). - WithArgs("foo", "bar", nil, "baz", AnyArgument{}, "repo", "{}", "{}", false, 1, "user", 1, "user2", 1). - WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.CreateSecret(_secret) - - if test.failure { - if err == nil { - t.Errorf("CreateSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateSecret returned err: %v", err) - } - } -} - -func TestPostgres_Client_UpdateSecret(t *testing.T) { - // setup types - _secret := testSecret() - _secret.SetID(1) - _secret.SetOrg("foo") - _secret.SetRepo("bar") - _secret.SetName("baz") - _secret.SetValue("foob") - _secret.SetType("repo") - _secret.SetCreatedAt(1) - _secret.SetCreatedBy("user") - _secret.SetUpdatedAt(1) - _secret.SetUpdatedBy("user2") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the query - _mock.ExpectExec(`UPDATE "secrets" SET "org"=$1,"repo"=$2,"team"=$3,"name"=$4,"value"=$5,"type"=$6,"images"=$7,"events"=$8,"allow_command"=$9,"created_at"=$10,"created_by"=$11,"updated_at"=$12,"updated_by"=$13 WHERE "id" = $14`). - WithArgs("foo", "bar", nil, "baz", AnyArgument{}, "repo", "{}", "{}", false, 1, "user", time.Now().UTC().Unix(), "user2", 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.UpdateSecret(_secret) - - if test.failure { - if err == nil { - t.Errorf("UpdateSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateSecret returned err: %v", err) - } - } -} - -func TestPostgres_Client_DeleteSecret(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Exec(dml.DeleteSecret, 1).Statement - - // ensure the mock expects the query - _mock.ExpectExec(_query.SQL.String()).WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.DeleteSecret(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteSecret returned err: %v", err) - } - } -} - -// testSecret is a test helper function to create a -// library Secret type with all fields set to their -// zero values. -func testSecret() *library.Secret { - i64 := int64(0) - str := "" - arr := []string{} - booL := false - - return &library.Secret{ - ID: &i64, - Org: &str, - Repo: &str, - Team: &str, - Name: &str, - Value: &str, - Type: &str, - Images: &arr, - Events: &arr, - AllowCommand: &booL, - CreatedAt: &i64, - CreatedBy: &str, - UpdatedAt: &i64, - UpdatedBy: &str, - } -} diff --git a/database/postgres/service.go b/database/postgres/service.go deleted file mode 100644 index 011a09770..000000000 --- a/database/postgres/service.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "errors" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetService gets a service by number and build ID from the database. -func (c *client) GetService(number int, b *library.Build) (*library.Service, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "service": number, - }).Tracef("getting service %d for build %d from the database", number, b.GetNumber()) - - // variable to store query results - s := new(database.Service) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableService). - Raw(dml.SelectBuildService, b.GetID(), number). - Scan(s) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return s.ToLibrary(), result.Error -} - -// CreateService creates a new service in the database. -func (c *client) CreateService(s *library.Service) error { - c.Logger.WithFields(logrus.Fields{ - "service": s.GetNumber(), - }).Tracef("creating service %s in the database", s.GetName()) - - // cast to database type - service := database.ServiceFromLibrary(s) - - // validate the necessary fields are populated - err := service.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Postgres. - Table(constants.TableService). - Create(service).Error -} - -// UpdateService updates a service in the database. -func (c *client) UpdateService(s *library.Service) error { - c.Logger.WithFields(logrus.Fields{ - "service": s.GetNumber(), - }).Tracef("updating service %s in the database", s.GetName()) - - // cast to database type - service := database.ServiceFromLibrary(s) - - // validate the necessary fields are populated - err := service.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Postgres. - Table(constants.TableService). - Save(service).Error -} - -// DeleteService deletes a service by unique ID from the database. -func (c *client) DeleteService(id int64) error { - c.Logger.Tracef("deleting service %d from the database", id) - - // send query to the database - return c.Postgres. - Table(constants.TableService). - Exec(dml.DeleteService, id).Error -} diff --git a/database/postgres/service_count.go b/database/postgres/service_count.go deleted file mode 100644 index 426239bc2..000000000 --- a/database/postgres/service_count.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetBuildServiceCount gets a count of all services by build ID from the database. -func (c *client) GetBuildServiceCount(b *library.Build) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("getting count of services for build %d from the database", b.GetNumber()) - - // variable to store query results - var s int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableService). - Raw(dml.SelectBuildServicesCount, b.GetID()). - Pluck("count", &s).Error - - return s, err -} - -// GetServiceImageCount gets a count of all service images -// and the count of their occurrence in the database. -func (c *client) GetServiceImageCount() (map[string]float64, error) { - c.Logger.Tracef("getting count of all images for services from the database") - - type imageCount struct { - Image string - Count int - } - - // variable to store query results - images := new([]imageCount) - counts := make(map[string]float64) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableService). - Raw(dml.SelectServiceImagesCount). - Scan(images).Error - - for _, image := range *images { - counts[image.Image] = float64(image.Count) - } - - return counts, err -} - -// GetServiceStatusCount gets a list of all service statuses -// and the count of their occurrence in the database. -func (c *client) GetServiceStatusCount() (map[string]float64, error) { - c.Logger.Trace("getting count of all statuses for services from the database") - - type statusCount struct { - Status string - Count int - } - - // variable to store query results - s := new([]statusCount) - counts := map[string]float64{ - "pending": 0, - "failure": 0, - "killed": 0, - "running": 0, - "success": 0, - } - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableService). - Raw(dml.SelectServiceStatusesCount). - Scan(s).Error - - for _, status := range *s { - counts[status.Status] = float64(status.Count) - } - - return counts, err -} diff --git a/database/postgres/service_count_test.go b/database/postgres/service_count_test.go deleted file mode 100644 index 513b1ac8d..000000000 --- a/database/postgres/service_count_test.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetBuildServiceCount(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectBuildServicesCount, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetBuildServiceCount(_build) - - if test.failure { - if err == nil { - t.Errorf("GetBuildServiceCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildServiceCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildServiceCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetServiceImageCount(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectServiceImagesCount).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"image", "count"}).AddRow("foo", 0) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want map[string]float64 - }{ - { - failure: false, - want: map[string]float64{"foo": 0}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetServiceImageCount() - - if test.failure { - if err == nil { - t.Errorf("GetServiceImageCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetServiceImageCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetServiceImageCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetServiceStatusCount(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectServiceStatusesCount).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"status", "count"}). - AddRow("failure", 0). - AddRow("killed", 0). - AddRow("pending", 0). - AddRow("running", 0). - AddRow("success", 0) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want map[string]float64 - }{ - { - failure: false, - want: map[string]float64{ - "pending": 0, - "failure": 0, - "killed": 0, - "running": 0, - "success": 0, - }, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetServiceStatusCount() - - if test.failure { - if err == nil { - t.Errorf("GetServiceStatusCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetServiceStatusCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetServiceStatusCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/service_list.go b/database/postgres/service_list.go deleted file mode 100644 index 04b3fc949..000000000 --- a/database/postgres/service_list.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetServiceList gets a list of all services from the database. -func (c *client) GetServiceList() ([]*library.Service, error) { - c.Logger.Trace("listing services from the database") - - // variable to store query results - s := new([]database.Service) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableService). - Raw(dml.ListServices). - Scan(s).Error - - // variable we want to return - services := []*library.Service{} - // iterate through all query results - for _, service := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := service - - // convert query result to library type - services = append(services, tmp.ToLibrary()) - } - - return services, err -} - -// GetBuildServiceList gets a list of services by build ID from the database. -// -// nolint: lll // ignore long line length due to parameters -func (c *client) GetBuildServiceList(b *library.Build, page, perPage int) ([]*library.Service, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("listing services for build %d from the database", b.GetNumber()) - - // variable to store query results - s := new([]database.Service) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableService). - Raw(dml.ListBuildServices, b.GetID(), perPage, offset). - Scan(s).Error - - // variable we want to return - services := []*library.Service{} - // iterate through all query results - for _, service := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := service - - // convert query result to library type - services = append(services, tmp.ToLibrary()) - } - - return services, err -} diff --git a/database/postgres/service_list_test.go b/database/postgres/service_list_test.go deleted file mode 100644 index 8787831f9..000000000 --- a/database/postgres/service_list_test.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetServiceList(t *testing.T) { - // setup types - _serviceOne := testService() - _serviceOne.SetID(1) - _serviceOne.SetRepoID(1) - _serviceOne.SetBuildID(1) - _serviceOne.SetNumber(1) - _serviceOne.SetName("foo") - _serviceOne.SetImage("bar") - - _serviceTwo := testService() - _serviceTwo.SetID(2) - _serviceTwo.SetRepoID(1) - _serviceTwo.SetBuildID(1) - _serviceTwo.SetNumber(1) - _serviceTwo.SetName("bar") - _serviceTwo.SetImage("foo") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListServices).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "build_id", "number", "name", "image", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}, - ).AddRow(1, 1, 1, 1, "foo", "bar", "", "", 0, 0, 0, 0, "", "", ""). - AddRow(2, 1, 1, 1, "bar", "foo", "", "", 0, 0, 0, 0, "", "", "") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Service - }{ - { - failure: false, - want: []*library.Service{_serviceOne, _serviceTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetServiceList() - - if test.failure { - if err == nil { - t.Errorf("GetServiceList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetServiceList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetServiceList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetBuildServiceList(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _serviceOne := testService() - _serviceOne.SetID(1) - _serviceOne.SetRepoID(1) - _serviceOne.SetBuildID(1) - _serviceOne.SetNumber(1) - _serviceOne.SetName("foo") - _serviceOne.SetImage("bar") - - _serviceTwo := testService() - _serviceTwo.SetID(2) - _serviceTwo.SetRepoID(1) - _serviceTwo.SetBuildID(1) - _serviceTwo.SetNumber(1) - _serviceTwo.SetName("bar") - _serviceTwo.SetImage("foo") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListBuildServices, 1, 1, 10).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "build_id", "number", "name", "image", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}, - ).AddRow(1, 1, 1, 1, "foo", "bar", "", "", 0, 0, 0, 0, "", "", ""). - AddRow(2, 1, 1, 1, "bar", "foo", "", "", 0, 0, 0, 0, "", "", "") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Service - }{ - { - failure: false, - want: []*library.Service{_serviceOne, _serviceTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetBuildServiceList(_build, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetBuildServiceList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildServiceList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildServiceList is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/service_test.go b/database/postgres/service_test.go deleted file mode 100644 index 7689f7ead..000000000 --- a/database/postgres/service_test.go +++ /dev/null @@ -1,264 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetService(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _service := testService() - _service.SetID(1) - _service.SetRepoID(1) - _service.SetBuildID(1) - _service.SetNumber(1) - _service.SetName("foo") - _service.SetImage("bar") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectBuildService, 1, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "build_id", "number", "name", "image", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}, - ).AddRow(1, 1, 1, 1, "foo", "bar", "", "", 0, 0, 0, 0, "", "", "") - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Service - }{ - { - failure: false, - want: _service, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetService(1, _build) - - if test.failure { - if err == nil { - t.Errorf("GetService should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetService returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetService is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_CreateService(t *testing.T) { - // setup types - _service := testService() - _service.SetID(1) - _service.SetRepoID(1) - _service.SetBuildID(1) - _service.SetNumber(1) - _service.SetName("foo") - _service.SetImage("bar") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery(`INSERT INTO "services" ("build_id","repo_id","number","name","image","status","error","exit_code","created","started","finished","host","runtime","distribution","id") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING "id"`). - WithArgs(1, 1, 1, "foo", "bar", nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). - WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.CreateService(_service) - - if test.failure { - if err == nil { - t.Errorf("CreateService should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateService returned err: %v", err) - } - } -} - -func TestPostgres_Client_UpdateService(t *testing.T) { - // setup types - _service := testService() - _service.SetID(1) - _service.SetRepoID(1) - _service.SetBuildID(1) - _service.SetNumber(1) - _service.SetName("foo") - _service.SetImage("bar") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the query - _mock.ExpectExec(`UPDATE "services" SET "build_id"=$1,"repo_id"=$2,"number"=$3,"name"=$4,"image"=$5,"status"=$6,"error"=$7,"exit_code"=$8,"created"=$9,"started"=$10,"finished"=$11,"host"=$12,"runtime"=$13,"distribution"=$14 WHERE "id" = $15`). - WithArgs(1, 1, 1, "foo", "bar", nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.UpdateService(_service) - - if test.failure { - if err == nil { - t.Errorf("UpdateService should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateService returned err: %v", err) - } - } -} - -func TestPostgres_Client_DeleteService(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Exec(dml.DeleteService, 1).Statement - - // ensure the mock expects the query - _mock.ExpectExec(_query.SQL.String()).WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.DeleteService(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteService should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteService returned err: %v", err) - } - } -} - -// testService is a test helper function to create a -// library Service type with all fields set to their -// zero values. -func testService() *library.Service { - i64 := int64(0) - i := 0 - str := "" - - return &library.Service{ - ID: &i64, - BuildID: &i64, - RepoID: &i64, - Number: &i, - Name: &str, - Image: &str, - Status: &str, - Error: &str, - ExitCode: &i, - Created: &i64, - Started: &i64, - Finished: &i64, - Host: &str, - Runtime: &str, - Distribution: &str, - } -} diff --git a/database/postgres/step.go b/database/postgres/step.go deleted file mode 100644 index 5f7ecd50e..000000000 --- a/database/postgres/step.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "errors" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetStep gets a step by number and build ID from the database. -func (c *client) GetStep(number int, b *library.Build) (*library.Step, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "step": number, - }).Tracef("getting step %d for build %d from the database", number, b.GetNumber()) - - // variable to store query results - s := new(database.Step) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableStep). - Raw(dml.SelectBuildStep, b.GetID(), number). - Scan(s) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return s.ToLibrary(), result.Error -} - -// CreateStep creates a new step in the database. -func (c *client) CreateStep(s *library.Step) error { - c.Logger.WithFields(logrus.Fields{ - "step": s.GetNumber(), - }).Tracef("creating step %s in the database", s.GetName()) - - // cast to database type - step := database.StepFromLibrary(s) - - // validate the necessary fields are populated - err := step.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Postgres. - Table(constants.TableStep). - Create(step).Error -} - -// UpdateStep updates a step in the database. -func (c *client) UpdateStep(s *library.Step) error { - c.Logger.WithFields(logrus.Fields{ - "step": s.GetNumber(), - }).Tracef("updating step %s in the database", s.GetName()) - - // cast to database type - step := database.StepFromLibrary(s) - - // validate the necessary fields are populated - err := step.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Postgres. - Table(constants.TableStep). - Save(step).Error -} - -// DeleteStep deletes a step by unique ID from the database. -func (c *client) DeleteStep(id int64) error { - c.Logger.Tracef("deleting step %d from the database", id) - - // send query to the database - return c.Postgres. - Table(constants.TableStep). - Exec(dml.DeleteStep, id).Error -} diff --git a/database/postgres/step_count.go b/database/postgres/step_count.go deleted file mode 100644 index 64fe6952b..000000000 --- a/database/postgres/step_count.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetBuildStepCount gets a count of all steps by build ID from the database. -func (c *client) GetBuildStepCount(b *library.Build) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("getting count of steps for build %d from the database", b.GetNumber()) - - // variable to store query results - var s int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableStep). - Raw(dml.SelectBuildStepsCount, b.GetID()). - Pluck("count", &s).Error - - return s, err -} - -// GetStepImageCount gets a count of all step images -// and the count of their occurrence in the database. -func (c *client) GetStepImageCount() (map[string]float64, error) { - c.Logger.Tracef("getting count of all images for steps from the database") - - type imageCount struct { - Image string - Count int - } - - // variable to store query results - images := new([]imageCount) - counts := make(map[string]float64) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableStep). - Raw(dml.SelectStepImagesCount). - Scan(images).Error - - for _, image := range *images { - counts[image.Image] = float64(image.Count) - } - - return counts, err -} - -// GetStepStatusCount gets a list of all step statuses -// and the count of their occurrence in the database. -func (c *client) GetStepStatusCount() (map[string]float64, error) { - c.Logger.Trace("getting count of all statuses for steps from the database") - - type statusCount struct { - Status string - Count int - } - - // variable to store query results - s := new([]statusCount) - counts := map[string]float64{ - "pending": 0, - "failure": 0, - "killed": 0, - "running": 0, - "success": 0, - } - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableStep). - Raw(dml.SelectStepStatusesCount). - Scan(s).Error - - for _, status := range *s { - counts[status.Status] = float64(status.Count) - } - - return counts, err -} diff --git a/database/postgres/step_count_test.go b/database/postgres/step_count_test.go deleted file mode 100644 index 227ba5021..000000000 --- a/database/postgres/step_count_test.go +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetBuildStepCount(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectBuildStepsCount, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetBuildStepCount(_build) - - if test.failure { - if err == nil { - t.Errorf("GetBuildStepCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildStepCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildStepCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetStepImageCount(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectStepImagesCount).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"image", "count"}).AddRow("foo", 0) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want map[string]float64 - }{ - { - failure: false, - want: map[string]float64{"foo": 0}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetStepImageCount() - - if test.failure { - if err == nil { - t.Errorf("GetStepImageCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetStepImageCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetStepImageCount is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetStepStatusCount(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectStepStatusesCount).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"status", "count"}). - AddRow("failure", 0). - AddRow("killed", 0). - AddRow("pending", 0). - AddRow("running", 0). - AddRow("success", 0) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want map[string]float64 - }{ - { - failure: false, - want: map[string]float64{ - "pending": 0, - "failure": 0, - "killed": 0, - "running": 0, - "success": 0, - }, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetStepStatusCount() - - if test.failure { - if err == nil { - t.Errorf("GetStepStatusCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetStepStatusCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetStepStatusCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/step_list.go b/database/postgres/step_list.go deleted file mode 100644 index 8e678f382..000000000 --- a/database/postgres/step_list.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetStepList gets a list of all steps from the database. -func (c *client) GetStepList() ([]*library.Step, error) { - c.Logger.Trace("listing steps from the database") - - // variable to store query results - s := new([]database.Step) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableStep). - Raw(dml.ListSteps). - Scan(s).Error - - // variable we want to return - steps := []*library.Step{} - // iterate through all query results - for _, step := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := step - - // convert query result to library type - steps = append(steps, tmp.ToLibrary()) - } - - return steps, err -} - -// GetBuildStepList gets a list of steps by build ID from the database. -func (c *client) GetBuildStepList(b *library.Build, page, perPage int) ([]*library.Step, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("listing steps for build %d from the database", b.GetNumber()) - - // variable to store query results - s := new([]database.Step) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableStep). - Raw(dml.ListBuildSteps, b.GetID(), perPage, offset). - Scan(s).Error - - // variable we want to return - steps := []*library.Step{} - // iterate through all query results - for _, step := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := step - - // convert query result to library type - steps = append(steps, tmp.ToLibrary()) - } - - return steps, err -} diff --git a/database/postgres/step_list_test.go b/database/postgres/step_list_test.go deleted file mode 100644 index 2c8ce586f..000000000 --- a/database/postgres/step_list_test.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetStepList(t *testing.T) { - // setup types - _stepOne := testStep() - _stepOne.SetID(1) - _stepOne.SetRepoID(1) - _stepOne.SetBuildID(1) - _stepOne.SetNumber(1) - _stepOne.SetName("foo") - _stepOne.SetImage("bar") - - _stepTwo := testStep() - _stepTwo.SetID(2) - _stepTwo.SetRepoID(1) - _stepTwo.SetBuildID(1) - _stepTwo.SetNumber(1) - _stepTwo.SetName("bar") - _stepTwo.SetImage("foo") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListSteps).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "build_id", "number", "name", "image", "stage", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}, - ).AddRow(1, 1, 1, 1, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", ""). - AddRow(2, 1, 1, 1, "bar", "foo", "", "", "", 0, 0, 0, 0, "", "", "") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Step - }{ - { - failure: false, - want: []*library.Step{_stepOne, _stepTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetStepList() - - if test.failure { - if err == nil { - t.Errorf("GetStepList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetStepList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetStepList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetBuildStepList(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _stepOne := testStep() - _stepOne.SetID(1) - _stepOne.SetRepoID(1) - _stepOne.SetBuildID(1) - _stepOne.SetNumber(1) - _stepOne.SetName("foo") - _stepOne.SetImage("bar") - - _stepTwo := testStep() - _stepTwo.SetID(2) - _stepTwo.SetRepoID(1) - _stepTwo.SetBuildID(1) - _stepTwo.SetNumber(1) - _stepTwo.SetName("bar") - _stepTwo.SetImage("foo") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListBuildSteps, 1, 1, 10).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "build_id", "number", "name", "image", "stage", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}, - ).AddRow(1, 1, 1, 1, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", ""). - AddRow(2, 1, 1, 1, "bar", "foo", "", "", "", 0, 0, 0, 0, "", "", "") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Step - }{ - { - failure: false, - want: []*library.Step{_stepOne, _stepTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetBuildStepList(_build, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetBuildStepList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildStepList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildStepList is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/step_test.go b/database/postgres/step_test.go deleted file mode 100644 index 31f8dc26a..000000000 --- a/database/postgres/step_test.go +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetStep(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _step := testStep() - _step.SetID(1) - _step.SetRepoID(1) - _step.SetBuildID(1) - _step.SetNumber(1) - _step.SetName("foo") - _step.SetImage("bar") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectBuildStep, 1, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "repo_id", "build_id", "number", "name", "image", "stage", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}, - ).AddRow(1, 1, 1, 1, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", "") - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Step - }{ - { - failure: false, - want: _step, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetStep(1, _build) - - if test.failure { - if err == nil { - t.Errorf("GetStep should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetStep returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetStep is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_CreateStep(t *testing.T) { - // setup types - _step := testStep() - _step.SetID(1) - _step.SetRepoID(1) - _step.SetBuildID(1) - _step.SetNumber(1) - _step.SetName("foo") - _step.SetImage("bar") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery(`INSERT INTO "steps" ("build_id","repo_id","number","name","image","stage","status","error","exit_code","created","started","finished","host","runtime","distribution","id") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) RETURNING "id"`). - WithArgs(1, 1, 1, "foo", "bar", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). - WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.CreateStep(_step) - - if test.failure { - if err == nil { - t.Errorf("CreateStep should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateStep returned err: %v", err) - } - } -} - -func TestPostgres_Client_UpdateStep(t *testing.T) { - // setup types - _step := testStep() - _step.SetID(1) - _step.SetRepoID(1) - _step.SetBuildID(1) - _step.SetNumber(1) - _step.SetName("foo") - _step.SetImage("bar") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the query - _mock.ExpectExec(`UPDATE "steps" SET "build_id"=$1,"repo_id"=$2,"number"=$3,"name"=$4,"image"=$5,"stage"=$6,"status"=$7,"error"=$8,"exit_code"=$9,"created"=$10,"started"=$11,"finished"=$12,"host"=$13,"runtime"=$14,"distribution"=$15 WHERE "id" = $16`). - WithArgs(1, 1, 1, "foo", "bar", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.UpdateStep(_step) - - if test.failure { - if err == nil { - t.Errorf("UpdateStep should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateStep returned err: %v", err) - } - } -} - -func TestPostgres_Client_DeleteStep(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Exec(dml.DeleteStep, 1).Statement - - // ensure the mock expects the query - _mock.ExpectExec(_query.SQL.String()).WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.DeleteStep(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteStep should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteStep returned err: %v", err) - } - } -} - -// testStep is a test helper function to create a -// library Step type with all fields set to their -// zero values. -func testStep() *library.Step { - i64 := int64(0) - i := 0 - str := "" - - return &library.Step{ - ID: &i64, - BuildID: &i64, - RepoID: &i64, - Number: &i, - Name: &str, - Image: &str, - Stage: &str, - Status: &str, - Error: &str, - ExitCode: &i, - Created: &i64, - Started: &i64, - Finished: &i64, - Host: &str, - Runtime: &str, - Distribution: &str, - } -} diff --git a/database/postgres/user.go b/database/postgres/user.go deleted file mode 100644 index d7b8c5f87..000000000 --- a/database/postgres/user.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "errors" - "fmt" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetUser gets a user by unique ID from the database. -func (c *client) GetUser(id int64) (*library.User, error) { - c.Logger.Tracef("getting user %d from the database", id) - - // variable to store query results - u := new(database.User) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableUser). - Raw(dml.SelectUser, id). - Scan(u) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err := u.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %d: %v", id, err) - - // return the unencrypted user - return u.ToLibrary(), result.Error - } - - // return the decrypted user - return u.ToLibrary(), result.Error -} - -// GetUserName gets a user by name from the database. -func (c *client) GetUserName(name string) (*library.User, error) { - c.Logger.WithFields(logrus.Fields{ - "user": name, - }).Tracef("getting user %s from the database", name) - - // variable to store query results - u := new(database.User) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableUser). - Raw(dml.SelectUserName, name). - Scan(u) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err := u.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %s: %v", name, err) - - // return the unencrypted user - return u.ToLibrary(), result.Error - } - - // return the decrypted user - return u.ToLibrary(), result.Error -} - -// CreateUser creates a new user in the database. -// -// nolint: dupl // ignore similar code with update -func (c *client) CreateUser(u *library.User) error { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("creating user %s in the database", u.GetName()) - - // cast to database type - // - // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary - user := database.UserFromLibrary(u) - - // validate the necessary fields are populated - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate - err := user.Validate() - if err != nil { - return err - } - - // encrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt - err = user.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt user %s: %v", u.GetName(), err) - } - - // send query to the database - return c.Postgres. - Table(constants.TableUser). - Create(user).Error -} - -// UpdateUser updates a user in the database. -// -// nolint: dupl // ignore similar code with create -func (c *client) UpdateUser(u *library.User) error { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("updating user %s in the database", u.GetName()) - - // cast to database type - // - // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary - user := database.UserFromLibrary(u) - - // validate the necessary fields are populated - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate - err := user.Validate() - if err != nil { - return err - } - - // encrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt - err = user.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt user %s: %v", u.GetName(), err) - } - - // send query to the database - return c.Postgres. - Table(constants.TableUser). - Save(user).Error -} - -// DeleteUser deletes a user by unique ID from the database. -func (c *client) DeleteUser(id int64) error { - c.Logger.Tracef("deleting user %d from the database", id) - - // send query to the database - return c.Postgres. - Table(constants.TableUser). - Exec(dml.DeleteUser, id).Error -} diff --git a/database/postgres/user_count.go b/database/postgres/user_count.go deleted file mode 100644 index 9b41092ab..000000000 --- a/database/postgres/user_count.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" -) - -// GetUserCount gets a count of all users from the database. -func (c *client) GetUserCount() (int64, error) { - c.Logger.Trace("getting count of users from the database") - - // variable to store query results - var u int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableUser). - Raw(dml.SelectUsersCount). - Pluck("count", &u).Error - - return u, err -} diff --git a/database/postgres/user_count_test.go b/database/postgres/user_count_test.go deleted file mode 100644 index ccc22c873..000000000 --- a/database/postgres/user_count_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetUserCount(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - _userOne.SetToken("bar") - _userOne.SetHash("baz") - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - _userTwo.SetToken("foo") - _userTwo.SetHash("baz") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectUsersCount).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetUserCount() - - if test.failure { - if err == nil { - t.Errorf("GetUserCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/user_list.go b/database/postgres/user_list.go deleted file mode 100644 index 0dfb227f4..000000000 --- a/database/postgres/user_list.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" -) - -// GetUserList gets a list of all users from the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) GetUserList() ([]*library.User, error) { - c.Logger.Trace("listing users from the database") - - // variable to store query results - u := new([]database.User) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableUser). - Raw(dml.ListUsers). - Scan(u).Error - if err != nil { - return nil, err - } - - // variable we want to return - users := []*library.User{} - // iterate through all query results - for _, user := range *u { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := user - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary - users = append(users, tmp.ToLibrary()) - } - - return users, nil -} - -// GetUserLiteList gets a lite list of all users from the database. -func (c *client) GetUserLiteList(page, perPage int) ([]*library.User, error) { - c.Logger.Trace("listing lite users from the database") - - // variable to store query results - u := new([]database.User) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableUser). - Raw(dml.ListLiteUsers, perPage, offset). - Scan(u).Error - - // variable we want to return - users := []*library.User{} - // iterate through all query results - for _, user := range *u { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := user - - // convert query result to library type - users = append(users, tmp.ToLibrary()) - } - - return users, err -} diff --git a/database/postgres/user_list_test.go b/database/postgres/user_list_test.go deleted file mode 100644 index eb69b6c90..000000000 --- a/database/postgres/user_list_test.go +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetUserList(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - _userOne.SetToken("bar") - _userOne.SetHash("baz") - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - _userTwo.SetToken("foo") - _userTwo.SetHash("baz") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListUsers).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "name", "refresh_token", "token", "hash", "favorites", "active", "admin"}, - ).AddRow(1, "foo", "", "bar", "baz", "{}", false, false). - AddRow(2, "bar", "", "foo", "baz", "{}", false, false) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.User - }{ - { - failure: false, - want: []*library.User{_userOne, _userTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetUserList() - - if test.failure { - if err == nil { - t.Errorf("GetUserList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetUserLiteList(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - _userOne.SetFavorites(nil) - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - _userTwo.SetFavorites(nil) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListLiteUsers, 1, 10).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "foo").AddRow(2, "bar") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.User - }{ - { - failure: false, - want: []*library.User{_userOne, _userTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetUserLiteList(1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetUserLiteList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserLiteList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserLiteList is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/user_test.go b/database/postgres/user_test.go deleted file mode 100644 index 3dd2f3ebf..000000000 --- a/database/postgres/user_test.go +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectUser, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "name", "refresh_token", "token", "hash", "favorites", "active", "admin"}, - ).AddRow(1, "foo", "", "bar", "baz", "{}", false, false) - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.User - }{ - { - failure: false, - want: _user, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetUser(1) - - if test.failure { - if err == nil { - t.Errorf("GetUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUser returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUser is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_CreateUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery(`INSERT INTO "users" ("name","refresh_token","token","hash","favorites","active","admin","id") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id"`). - WithArgs("foo", AnyArgument{}, AnyArgument{}, AnyArgument{}, "{}", false, false, 1). - WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.CreateUser(_user) - - if test.failure { - if err == nil { - t.Errorf("CreateUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateUser returned err: %v", err) - } - } -} - -func TestPostgres_Client_UpdateUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the query - _mock.ExpectExec(`UPDATE "users" SET "name"=$1,"refresh_token"=$2,"token"=$3,"hash"=$4,"favorites"=$5,"active"=$6,"admin"=$7 WHERE "id" = $8`). - WithArgs("foo", AnyArgument{}, AnyArgument{}, AnyArgument{}, "{}", false, false, 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.UpdateUser(_user) - - if test.failure { - if err == nil { - t.Errorf("UpdateUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateUser returned err: %v", err) - } - } -} - -func TestPostgres_Client_DeleteUser(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Exec(dml.DeleteUser, 1).Statement - - // ensure the mock expects the query - _mock.ExpectExec(_query.SQL.String()).WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.DeleteUser(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteUser returned err: %v", err) - } - } -} - -// testUser is a test helper function to create a -// library User type with all fields set to their -// zero values. -func testUser() *library.User { - i64 := int64(0) - str := "" - arr := []string{} - b := false - - return &library.User{ - ID: &i64, - Name: &str, - RefreshToken: &str, - Token: &str, - Hash: &str, - Favorites: &arr, - Active: &b, - Admin: &b, - } -} diff --git a/database/postgres/worker.go b/database/postgres/worker.go deleted file mode 100644 index 1d871f596..000000000 --- a/database/postgres/worker.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "errors" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetWorker gets a worker by hostname from the database. -func (c *client) GetWorker(hostname string) (*library.Worker, error) { - c.Logger.WithFields(logrus.Fields{ - "worker": hostname, - }).Tracef("getting worker %s from the database", hostname) - - // variable to store query results - w := new(database.Worker) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableWorker). - Raw(dml.SelectWorker, hostname). - Scan(w) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return w.ToLibrary(), result.Error -} - -// GetWorker gets a worker by address from the database. -func (c *client) GetWorkerByAddress(address string) (*library.Worker, error) { - c.Logger.Tracef("getting worker by address %s from the database", address) - - // variable to store query results - w := new(database.Worker) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableWorker). - Raw(dml.SelectWorkerByAddress, address). - Scan(w) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return w.ToLibrary(), result.Error -} - -// CreateWorker creates a new worker in the database. -func (c *client) CreateWorker(w *library.Worker) error { - c.Logger.WithFields(logrus.Fields{ - "worker": w.GetHostname(), - }).Tracef("creating worker %s in the database", w.GetHostname()) - - // cast to database type - worker := database.WorkerFromLibrary(w) - - // validate the necessary fields are populated - err := worker.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Postgres. - Table(constants.TableWorker). - Create(worker).Error -} - -// UpdateWorker updates a worker in the database. -func (c *client) UpdateWorker(w *library.Worker) error { - c.Logger.WithFields(logrus.Fields{ - "worker": w.GetHostname(), - }).Tracef("updating worker %s in the database", w.GetHostname()) - - // cast to database type - worker := database.WorkerFromLibrary(w) - - // validate the necessary fields are populated - err := worker.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Postgres. - Table(constants.TableWorker). - Save(worker).Error -} - -// DeleteWorker deletes a worker by unique ID from the database. -func (c *client) DeleteWorker(id int64) error { - c.Logger.Tracef("deleting worker %d in the database", id) - - // send query to the database - return c.Postgres. - Table(constants.TableWorker). - Exec(dml.DeleteWorker, id).Error -} diff --git a/database/postgres/worker_count.go b/database/postgres/worker_count.go deleted file mode 100644 index d5f867988..000000000 --- a/database/postgres/worker_count.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" -) - -// GetWorkerCount gets a count of all workers from the database. -func (c *client) GetWorkerCount() (int64, error) { - c.Logger.Trace("getting count of workers from the database") - - // variable to store query results - var w int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableWorker). - Raw(dml.SelectWorkersCount). - Pluck("count", &w).Error - - return w, err -} diff --git a/database/postgres/worker_count_test.go b/database/postgres/worker_count_test.go deleted file mode 100644 index 0c099c2e2..000000000 --- a/database/postgres/worker_count_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetWorkerCount(t *testing.T) { - // setup types - _workerOne := testWorker() - _workerOne.SetID(1) - _workerOne.SetHostname("worker_0") - _workerOne.SetAddress("localhost") - _workerOne.SetActive(true) - - _workerTwo := testWorker() - _workerTwo.SetID(2) - _workerTwo.SetHostname("worker_1") - _workerTwo.SetAddress("localhost") - _workerTwo.SetActive(true) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectWorkersCount).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetWorkerCount() - - if test.failure { - if err == nil { - t.Errorf("GetWorkerCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetWorkerCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetWorkerCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/worker_list_test.go b/database/postgres/worker_list_test.go deleted file mode 100644 index c6250062a..000000000 --- a/database/postgres/worker_list_test.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetWorkerList(t *testing.T) { - // setup types - _workerOne := testWorker() - _workerOne.SetID(1) - _workerOne.SetHostname("worker_0") - _workerOne.SetAddress("localhost") - _workerOne.SetActive(true) - - _workerTwo := testWorker() - _workerTwo.SetID(2) - _workerTwo.SetHostname("worker_1") - _workerTwo.SetAddress("localhost") - _workerTwo.SetActive(true) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListWorkers).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "hostname", "address", "routes", "active", "last_checked_in", "build_limit"}, - ).AddRow(1, "worker_0", "localhost", "{}", true, 0, 0). - AddRow(2, "worker_1", "localhost", "{}", true, 0, 0) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.Worker - }{ - { - failure: false, - want: []*library.Worker{_workerOne, _workerTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetWorkerList() - - if test.failure { - if err == nil { - t.Errorf("GetWorkerList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetWorkerList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetWorkerList is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/worker_test.go b/database/postgres/worker_test.go deleted file mode 100644 index 5167c5cc3..000000000 --- a/database/postgres/worker_test.go +++ /dev/null @@ -1,312 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - "gorm.io/gorm" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" -) - -func TestPostgres_Client_GetWorker(t *testing.T) { - // setup types - _worker := testWorker() - _worker.SetID(1) - _worker.SetHostname("worker_0") - _worker.SetAddress("localhost") - _worker.SetActive(true) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectWorker, "worker_0").Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "hostname", "address", "routes", "active", "last_checked_in", "build_limit"}, - ).AddRow(1, "worker_0", "localhost", "{}", true, 0, 0) - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Worker - }{ - { - failure: false, - want: _worker, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetWorker("worker_0") - - if test.failure { - if err == nil { - t.Errorf("GetWorker should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetWorker returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetWorker is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetWorkerByAddress(t *testing.T) { - // setup types - _worker := testWorker() - _worker.SetID(1) - _worker.SetHostname("worker_0") - _worker.SetAddress("localhost") - _worker.SetActive(true) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectWorkerByAddress, "localhost").Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "hostname", "address", "routes", "active", "last_checked_in", "build_limit"}, - ).AddRow(1, "worker_0", "localhost", "{}", true, 0, 0) - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.Worker - }{ - { - failure: false, - want: _worker, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetWorkerByAddress("localhost") - - if test.failure { - if err == nil { - t.Errorf("GetWorkerByAddress should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetWorkerByAddress returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetWorkerByAddress is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_CreateWorker(t *testing.T) { - // setup types - _worker := testWorker() - _worker.SetID(1) - _worker.SetHostname("worker_0") - _worker.SetAddress("localhost") - _worker.SetActive(true) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery(`INSERT INTO "workers" ("hostname","address","routes","active","last_checked_in","build_limit","id") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "id"`). - WithArgs("worker_0", "localhost", "{}", true, nil, nil, 1). - WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.CreateWorker(_worker) - - if test.failure { - if err == nil { - t.Errorf("CreateWorker should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateWorker returned err: %v", err) - } - } -} - -func TestPostgres_Client_UpdateWorker(t *testing.T) { - // setup types - _worker := testWorker() - _worker.SetID(1) - _worker.SetHostname("worker_0") - _worker.SetAddress("localhost") - _worker.SetActive(true) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the query - _mock.ExpectExec(`UPDATE "workers" SET "hostname"=$1,"address"=$2,"routes"=$3,"active"=$4,"last_checked_in"=$5,"build_limit"=$6 WHERE "id" = $7`). - WithArgs("worker_0", "localhost", "{}", true, nil, nil, 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.UpdateWorker(_worker) - - if test.failure { - if err == nil { - t.Errorf("UpdateWorker should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateWorker returned err: %v", err) - } - } -} - -func TestPostgres_Client_DeleteWorker(t *testing.T) { - // setup types - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Exec(dml.DeleteWorker, 1).Statement - - // ensure the mock expects the query - _mock.ExpectExec(_query.SQL.String()).WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.DeleteWorker(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteWorker should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteWorker returned err: %v", err) - } - } -} - -// testWorker is a test helper function to create a -// library Worker type with all fields set to their -// zero values. -func testWorker() *library.Worker { - i64 := int64(0) - str := "" - arr := []string{} - b := false - - return &library.Worker{ - ID: &i64, - Hostname: &str, - Address: &str, - Routes: &arr, - Active: &b, - LastCheckedIn: &i64, - BuildLimit: &i64, - } -} diff --git a/database/repo/count.go b/database/repo/count.go new file mode 100644 index 000000000..3ef5297d1 --- /dev/null +++ b/database/repo/count.go @@ -0,0 +1,27 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/go-vela/types/constants" +) + +// CountRepos gets the count of all repos from the database. +func (e *engine) CountRepos(ctx context.Context) (int64, error) { + e.logger.Tracef("getting count of all repos from the database") + + // variable to store query results + var r int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableRepo). + Count(&r). + Error + + return r, err +} diff --git a/database/repo/count_org.go b/database/repo/count_org.go new file mode 100644 index 000000000..0e44516e9 --- /dev/null +++ b/database/repo/count_org.go @@ -0,0 +1,32 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" +) + +// CountReposForOrg gets the count of repos by org name from the database. +func (e *engine) CountReposForOrg(ctx context.Context, org string, filters map[string]interface{}) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": org, + }).Tracef("getting count of repos for org %s from the database", org) + + // variable to store query results + var r int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableRepo). + Where("org = ?", org). + Where(filters). + Count(&r). + Error + + return r, err +} diff --git a/database/repo/count_org_test.go b/database/repo/count_org_test.go new file mode 100644 index 000000000..f3657f3eb --- /dev/null +++ b/database/repo/count_org_test.go @@ -0,0 +1,102 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestRepo_Engine_CountReposForOrg(t *testing.T) { + // setup types + _repoOne := testRepo() + _repoOne.SetID(1) + _repoOne.SetUserID(1) + _repoOne.SetHash("baz") + _repoOne.SetOrg("foo") + _repoOne.SetName("bar") + _repoOne.SetFullName("foo/bar") + _repoOne.SetVisibility("public") + + _repoTwo := testRepo() + _repoTwo.SetID(2) + _repoTwo.SetUserID(1) + _repoTwo.SetHash("baz") + _repoTwo.SetOrg("bar") + _repoTwo.SetName("foo") + _repoTwo.SetFullName("bar/foo") + _repoTwo.SetVisibility("public") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "repos" WHERE org = $1`).WithArgs("foo").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateRepo(context.TODO(), _repoOne) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + _, err = _sqlite.CreateRepo(context.TODO(), _repoTwo) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 1, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 1, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountReposForOrg(context.TODO(), "foo", filters) + + if test.failure { + if err == nil { + t.Errorf("CountReposForOrg for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountReposForOrg for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountReposForOrg for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/repo/count_test.go b/database/repo/count_test.go new file mode 100644 index 000000000..668018315 --- /dev/null +++ b/database/repo/count_test.go @@ -0,0 +1,100 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestRepo_Engine_CountRepos(t *testing.T) { + // setup types + _repoOne := testRepo() + _repoOne.SetID(1) + _repoOne.SetUserID(1) + _repoOne.SetHash("baz") + _repoOne.SetOrg("foo") + _repoOne.SetName("bar") + _repoOne.SetFullName("foo/bar") + _repoOne.SetVisibility("public") + + _repoTwo := testRepo() + _repoTwo.SetID(2) + _repoTwo.SetUserID(1) + _repoTwo.SetHash("baz") + _repoTwo.SetOrg("bar") + _repoTwo.SetName("foo") + _repoTwo.SetFullName("bar/foo") + _repoTwo.SetVisibility("public") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "repos"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateRepo(context.TODO(), _repoOne) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + _, err = _sqlite.CreateRepo(context.TODO(), _repoTwo) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountRepos(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("CountRepos for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountRepos for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountRepos for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/repo/count_user.go b/database/repo/count_user.go new file mode 100644 index 000000000..bfa7f56ee --- /dev/null +++ b/database/repo/count_user.go @@ -0,0 +1,33 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CountReposForUser gets the count of repos by user ID from the database. +func (e *engine) CountReposForUser(ctx context.Context, u *library.User, filters map[string]interface{}) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Tracef("getting count of repos for user %s from the database", u.GetName()) + + // variable to store query results + var r int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableRepo). + Where("user_id = ?", u.GetID()). + Where(filters). + Count(&r). + Error + + return r, err +} diff --git a/database/repo/count_user_test.go b/database/repo/count_user_test.go new file mode 100644 index 000000000..58342226e --- /dev/null +++ b/database/repo/count_user_test.go @@ -0,0 +1,108 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestRepo_Engine_CountReposForUser(t *testing.T) { + // setup types + _repoOne := testRepo() + _repoOne.SetID(1) + _repoOne.SetUserID(1) + _repoOne.SetHash("baz") + _repoOne.SetOrg("foo") + _repoOne.SetName("bar") + _repoOne.SetFullName("foo/bar") + _repoOne.SetVisibility("public") + + _repoTwo := testRepo() + _repoTwo.SetID(2) + _repoTwo.SetUserID(1) + _repoTwo.SetHash("baz") + _repoTwo.SetOrg("bar") + _repoTwo.SetName("foo") + _repoTwo.SetFullName("bar/foo") + _repoTwo.SetVisibility("public") + + _user := new(library.User) + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "repos" WHERE user_id = $1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateRepo(context.TODO(), _repoOne) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + _, err = _sqlite.CreateRepo(context.TODO(), _repoTwo) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountReposForUser(context.TODO(), _user, filters) + + if test.failure { + if err == nil { + t.Errorf("CountReposForUser for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountReposForUser for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountReposForUser for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/repo/create.go b/database/repo/create.go new file mode 100644 index 000000000..800365e55 --- /dev/null +++ b/database/repo/create.go @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with update.go +package repo + +import ( + "context" + "fmt" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreateRepo creates a new repo in the database. +func (e *engine) CreateRepo(ctx context.Context, r *library.Repo) (*library.Repo, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("creating repo %s in the database", r.GetFullName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#RepoFromLibrary + repo := database.RepoFromLibrary(r) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Validate + err := repo.Validate() + if err != nil { + return nil, err + } + + // encrypt the fields for the repo + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Encrypt + err = repo.Encrypt(e.config.EncryptionKey) + if err != nil { + return nil, fmt.Errorf("unable to encrypt repo %s: %w", r.GetFullName(), err) + } + + // send query to the database + err = e.client.Table(constants.TableRepo).Create(repo).Error + if err != nil { + return nil, err + } + + // decrypt the fields for the repo + err = repo.Decrypt(e.config.EncryptionKey) + if err != nil { + // only log to preserve backwards compatibility + e.logger.Errorf("unable to decrypt repo %d: %v", r.GetID(), err) + + return repo.ToLibrary(), nil + } + + return repo.ToLibrary(), nil +} diff --git a/database/repo/create_test.go b/database/repo/create_test.go new file mode 100644 index 000000000..4e59b2d99 --- /dev/null +++ b/database/repo/create_test.go @@ -0,0 +1,85 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestRepo_Engine_CreateRepo(t *testing.T) { + // setup types + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + _repo.SetPipelineType("yaml") + _repo.SetPreviousName("oldName") + _repo.SetTopics([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`INSERT INTO "repos" +("user_id","hash","org","name","full_name","link","clone","branch","topics","build_limit","timeout","counter","visibility","private","trusted","active","allow_pull","allow_push","allow_deploy","allow_tag","allow_comment","pipeline_type","previous_name","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24) RETURNING "id"`). + WithArgs(1, AnyArgument{}, "foo", "bar", "foo/bar", nil, nil, nil, AnyArgument{}, AnyArgument{}, AnyArgument{}, AnyArgument{}, "public", false, false, false, false, false, false, false, false, "yaml", "oldName", 1). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CreateRepo(context.TODO(), _repo) + + if test.failure { + if err == nil { + t.Errorf("CreateRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _repo) { + t.Errorf("CreateRepo for %s returned %s, want %s", test.name, got, _repo) + } + }) + } +} diff --git a/database/repo/delete.go b/database/repo/delete.go new file mode 100644 index 000000000..4e0df1918 --- /dev/null +++ b/database/repo/delete.go @@ -0,0 +1,33 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// DeleteRepo deletes an existing repo from the database. +func (e *engine) DeleteRepo(ctx context.Context, r *library.Repo) error { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("deleting repo %s from the database", r.GetFullName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#RepoFromLibrary + repo := database.RepoFromLibrary(r) + + // send query to the database + return e.client. + Table(constants.TableRepo). + Delete(repo). + Error +} diff --git a/database/repo/delete_test.go b/database/repo/delete_test.go new file mode 100644 index 000000000..f7f64e1aa --- /dev/null +++ b/database/repo/delete_test.go @@ -0,0 +1,77 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestRepo_Engine_DeleteRepo(t *testing.T) { + // setup types + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`DELETE FROM "repos" WHERE "repos"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateRepo(context.TODO(), _repo) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeleteRepo(context.TODO(), _repo) + + if test.failure { + if err == nil { + t.Errorf("DeleteRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeleteRepo for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/repo/get.go b/database/repo/get.go new file mode 100644 index 000000000..3adbaaffd --- /dev/null +++ b/database/repo/get.go @@ -0,0 +1,54 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetRepo gets a repo by ID from the database. +func (e *engine) GetRepo(ctx context.Context, id int64) (*library.Repo, error) { + e.logger.Tracef("getting repo %d from the database", id) + + // variable to store query results + r := new(database.Repo) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableRepo). + Where("id = ?", id). + Take(r). + Error + if err != nil { + return nil, err + } + + // decrypt the fields for the repo + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt + err = r.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted repos + e.logger.Errorf("unable to decrypt repo %d: %v", id, err) + + // return the unencrypted repo + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.ToLibrary + return r.ToLibrary(), nil + } + + // return the decrypted repo + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.ToLibrary + return r.ToLibrary(), nil +} diff --git a/database/repo/get_org.go b/database/repo/get_org.go new file mode 100644 index 000000000..2188e0a5e --- /dev/null +++ b/database/repo/get_org.go @@ -0,0 +1,59 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetRepoForOrg gets a repo by org and repo name from the database. +func (e *engine) GetRepoForOrg(ctx context.Context, org, name string) (*library.Repo, error) { + e.logger.WithFields(logrus.Fields{ + "org": org, + "repo": name, + }).Tracef("getting repo %s/%s from the database", org, name) + + // variable to store query results + r := new(database.Repo) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableRepo). + Where("org = ?", org). + Where("name = ?", name). + Take(r). + Error + if err != nil { + return nil, err + } + + // decrypt the fields for the repo + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt + err = r.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted repos + e.logger.Errorf("unable to decrypt repo %s/%s: %v", org, name, err) + + // return the unencrypted repo + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.ToLibrary + return r.ToLibrary(), nil + } + + // return the decrypted repo + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.ToLibrary + return r.ToLibrary(), nil +} diff --git a/database/repo/get_org_test.go b/database/repo/get_org_test.go new file mode 100644 index 000000000..1eb1b5940 --- /dev/null +++ b/database/repo/get_org_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestRepo_Engine_GetRepoForOrg(t *testing.T) { + // setup types + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + _repo.SetPipelineType("yaml") + _repo.SetTopics([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "topics", "build_limit", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}). + AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", "{}", 0, 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", "") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "repos" WHERE org = $1 AND name = $2 LIMIT 1`).WithArgs("foo", "bar").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateRepo(context.TODO(), _repo) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Repo + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _repo, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _repo, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetRepoForOrg(context.TODO(), "foo", "bar") + + if test.failure { + if err == nil { + t.Errorf("GetRepoForOrg for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetRepoForOrg for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetRepoForOrg for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/repo/get_test.go b/database/repo/get_test.go new file mode 100644 index 000000000..02ea9dd98 --- /dev/null +++ b/database/repo/get_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestRepo_Engine_GetRepo(t *testing.T) { + // setup types + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + _repo.SetPipelineType("yaml") + _repo.SetTopics([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "topics", "build_limit", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}). + AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", "{}", 0, 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", "") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "repos" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateRepo(context.TODO(), _repo) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Repo + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _repo, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _repo, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetRepo(context.TODO(), 1) + + if test.failure { + if err == nil { + t.Errorf("GetRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/repo/index.go b/database/repo/index.go new file mode 100644 index 000000000..8c90262a7 --- /dev/null +++ b/database/repo/index.go @@ -0,0 +1,26 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import "context" + +const ( + // CreateOrgNameIndex represents a query to create an + // index on the repos table for the org and name columns. + CreateOrgNameIndex = ` +CREATE INDEX +IF NOT EXISTS +repos_org_name +ON repos (org, name); +` +) + +// CreateRepoIndexes creates the indexes for the repos table in the database. +func (e *engine) CreateRepoIndexes(ctx context.Context) error { + e.logger.Tracef("creating indexes for repos table in the database") + + // create the org and name columns index for the repos table + return e.client.Exec(CreateOrgNameIndex).Error +} diff --git a/database/repo/index_test.go b/database/repo/index_test.go new file mode 100644 index 000000000..d9a527b61 --- /dev/null +++ b/database/repo/index_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestRepo_Engine_CreateRepoIndexes(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreateOrgNameIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateRepoIndexes(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("CreateRepoIndexes for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateRepoIndexes for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/repo/interface.go b/database/repo/interface.go new file mode 100644 index 000000000..6dad94d20 --- /dev/null +++ b/database/repo/interface.go @@ -0,0 +1,53 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/go-vela/types/library" +) + +// RepoInterface represents the Vela interface for repo +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type RepoInterface interface { + // Repo Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreateRepoIndexes defines a function that creates the indexes for the repos table. + CreateRepoIndexes(context.Context) error + // CreateRepoTable defines a function that creates the repos table. + CreateRepoTable(context.Context, string) error + + // Repo Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CountRepos defines a function that gets the count of all repos. + CountRepos(context.Context) (int64, error) + // CountReposForOrg defines a function that gets the count of repos by org name. + CountReposForOrg(context.Context, string, map[string]interface{}) (int64, error) + // CountReposForUser defines a function that gets the count of repos by user ID. + CountReposForUser(context.Context, *library.User, map[string]interface{}) (int64, error) + // CreateRepo defines a function that creates a new repo. + CreateRepo(context.Context, *library.Repo) (*library.Repo, error) + // DeleteRepo defines a function that deletes an existing repo. + DeleteRepo(context.Context, *library.Repo) error + // GetRepo defines a function that gets a repo by ID. + GetRepo(context.Context, int64) (*library.Repo, error) + // GetRepoForOrg defines a function that gets a repo by org and repo name. + GetRepoForOrg(context.Context, string, string) (*library.Repo, error) + // ListRepos defines a function that gets a list of all repos. + ListRepos(context.Context) ([]*library.Repo, error) + // ListReposForOrg defines a function that gets a list of repos by org name. + ListReposForOrg(context.Context, string, string, map[string]interface{}, int, int) ([]*library.Repo, int64, error) + // ListReposForUser defines a function that gets a list of repos by user ID. + ListReposForUser(context.Context, *library.User, string, map[string]interface{}, int, int) ([]*library.Repo, int64, error) + // UpdateRepo defines a function that updates an existing repo. + UpdateRepo(context.Context, *library.Repo) (*library.Repo, error) +} diff --git a/database/repo/list.go b/database/repo/list.go new file mode 100644 index 000000000..c65754c16 --- /dev/null +++ b/database/repo/list.go @@ -0,0 +1,69 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListRepos gets a list of all repos from the database. +func (e *engine) ListRepos(ctx context.Context) ([]*library.Repo, error) { + e.logger.Trace("listing all repos from the database") + + // variables to store query results and return value + count := int64(0) + r := new([]database.Repo) + repos := []*library.Repo{} + + // count the results + count, err := e.CountRepos(ctx) + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return repos, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableRepo). + Find(&r). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, repo := range *r { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := repo + + // decrypt the fields for the repo + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt + err = tmp.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted repos + e.logger.Errorf("unable to decrypt repo %d: %v", tmp.ID.Int64, err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.ToLibrary + repos = append(repos, tmp.ToLibrary()) + } + + return repos, nil +} diff --git a/database/repo/list_org.go b/database/repo/list_org.go new file mode 100644 index 000000000..9809716bf --- /dev/null +++ b/database/repo/list_org.go @@ -0,0 +1,106 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListReposForOrg gets a list of repos by org name from the database. +// +//nolint:lll // ignore long line length due to variable names +func (e *engine) ListReposForOrg(ctx context.Context, org, sortBy string, filters map[string]interface{}, page, perPage int) ([]*library.Repo, int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": org, + }).Tracef("listing repos for org %s from the database", org) + + // variables to store query results and return values + count := int64(0) + r := new([]database.Repo) + repos := []*library.Repo{} + + // count the results + count, err := e.CountReposForOrg(ctx, org, filters) + if err != nil { + return repos, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return repos, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + switch sortBy { + case "latest": + query := e.client. + Table(constants.TableBuild). + Select("repos.id, MAX(builds.created) AS latest_build"). + Joins("INNER JOIN repos repos ON builds.repo_id = repos.id"). + Where("repos.org = ?", org). + Group("repos.id") + + err = e.client. + Table(constants.TableRepo). + Select("repos.*"). + Joins("LEFT JOIN (?) t on repos.id = t.id", query). + Order("latest_build DESC NULLS LAST"). + Limit(perPage). + Offset(offset). + Find(&r). + Error + if err != nil { + return nil, count, err + } + case "name": + fallthrough + default: + err = e.client. + Table(constants.TableRepo). + Where("org = ?", org). + Where(filters). + Order("name"). + Limit(perPage). + Offset(offset). + Find(&r). + Error + if err != nil { + return nil, count, err + } + } + + // iterate through all query results + for _, repo := range *r { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := repo + + // decrypt the fields for the repo + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt + err = tmp.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted repos + e.logger.Errorf("unable to decrypt repo %d: %v", tmp.ID.Int64, err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.ToLibrary + repos = append(repos, tmp.ToLibrary()) + } + + return repos, count, nil +} diff --git a/database/repo/list_org_test.go b/database/repo/list_org_test.go new file mode 100644 index 000000000..c87707385 --- /dev/null +++ b/database/repo/list_org_test.go @@ -0,0 +1,178 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +func TestRepo_Engine_ListReposForOrg(t *testing.T) { + // setup types + _buildOne := new(library.Build) + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetCreated(time.Now().UTC().Unix()) + + _buildTwo := new(library.Build) + _buildTwo.SetID(2) + _buildTwo.SetRepoID(2) + _buildTwo.SetNumber(1) + _buildTwo.SetCreated(time.Now().UTC().Unix()) + + _repoOne := testRepo() + _repoOne.SetID(1) + _repoOne.SetUserID(1) + _repoOne.SetHash("baz") + _repoOne.SetOrg("foo") + _repoOne.SetName("bar") + _repoOne.SetFullName("foo/bar") + _repoOne.SetVisibility("public") + _repoOne.SetPipelineType("yaml") + _repoOne.SetTopics([]string{}) + + _repoTwo := testRepo() + _repoTwo.SetID(2) + _repoTwo.SetUserID(1) + _repoTwo.SetHash("bar") + _repoTwo.SetOrg("foo") + _repoTwo.SetName("baz") + _repoTwo.SetFullName("foo/baz") + _repoTwo.SetVisibility("public") + _repoTwo.SetPipelineType("yaml") + _repoTwo.SetTopics([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected name count query result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the name count query + _mock.ExpectQuery(`SELECT count(*) FROM "repos" WHERE org = $1`).WithArgs("foo").WillReturnRows(_rows) + + // create expected name query result in mock + _rows = sqlmock.NewRows( + []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "topics", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}). + AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", "{}", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", nil). + AddRow(2, 1, "bar", "foo", "baz", "foo/baz", "", "", "", "{}", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", nil) + + // ensure the mock expects the name query + _mock.ExpectQuery(`SELECT * FROM "repos" WHERE org = $1 ORDER BY name LIMIT 10`).WithArgs("foo").WillReturnRows(_rows) + + // create expected latest count query result in mock + _rows = sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the latest count query + _mock.ExpectQuery(`SELECT count(*) FROM "repos" WHERE org = $1`).WithArgs("foo").WillReturnRows(_rows) + + // create expected latest query result in mock + _rows = sqlmock.NewRows( + []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "topics", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}). + AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", "{}", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", nil). + AddRow(2, 1, "bar", "foo", "baz", "foo/baz", "", "", "", "{}", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", nil) + + // ensure the mock expects the latest query + _mock.ExpectQuery(`SELECT repos.* FROM "repos" LEFT JOIN (SELECT repos.id, MAX(builds.created) AS latest_build FROM "builds" INNER JOIN repos repos ON builds.repo_id = repos.id WHERE repos.org = $1 GROUP BY "repos"."id") t on repos.id = t.id ORDER BY latest_build DESC NULLS LAST LIMIT 10`).WithArgs("foo").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateRepo(context.TODO(), _repoOne) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + _, err = _sqlite.CreateRepo(context.TODO(), _repoTwo) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + err = _sqlite.client.AutoMigrate(&database.Build{}) + if err != nil { + t.Errorf("unable to create build table for sqlite: %v", err) + } + + err = _sqlite.client.Table(constants.TableBuild).Create(database.BuildFromLibrary(_buildOne).Crop()).Error + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + err = _sqlite.client.Table(constants.TableBuild).Create(database.BuildFromLibrary(_buildTwo).Crop()).Error + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + sort string + database *engine + want []*library.Repo + }{ + { + failure: false, + name: "postgres with name", + database: _postgres, + sort: "name", + want: []*library.Repo{_repoOne, _repoTwo}, + }, + { + failure: false, + name: "postgres with latest", + database: _postgres, + sort: "latest", + want: []*library.Repo{_repoOne, _repoTwo}, + }, + { + failure: false, + name: "sqlite with name", + database: _sqlite, + sort: "name", + want: []*library.Repo{_repoOne, _repoTwo}, + }, + { + failure: false, + name: "sqlite with latest", + database: _sqlite, + sort: "latest", + want: []*library.Repo{_repoOne, _repoTwo}, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListReposForOrg(context.TODO(), "foo", test.sort, filters, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListReposForOrg for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListReposForOrg for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListReposForOrg for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/repo/list_test.go b/database/repo/list_test.go new file mode 100644 index 000000000..01b6a257f --- /dev/null +++ b/database/repo/list_test.go @@ -0,0 +1,114 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestRepo_Engine_ListRepos(t *testing.T) { + // setup types + _repoOne := testRepo() + _repoOne.SetID(1) + _repoOne.SetUserID(1) + _repoOne.SetHash("baz") + _repoOne.SetOrg("foo") + _repoOne.SetName("bar") + _repoOne.SetFullName("foo/bar") + _repoOne.SetVisibility("public") + _repoOne.SetPipelineType("yaml") + _repoOne.SetTopics([]string{}) + + _repoTwo := testRepo() + _repoTwo.SetID(2) + _repoTwo.SetUserID(1) + _repoTwo.SetHash("baz") + _repoTwo.SetOrg("bar") + _repoTwo.SetName("foo") + _repoTwo.SetFullName("bar/foo") + _repoTwo.SetVisibility("public") + _repoTwo.SetPipelineType("yaml") + _repoTwo.SetTopics([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "repos"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "topics", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}). + AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", "{}", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", nil). + AddRow(2, 1, "baz", "bar", "foo", "bar/foo", "", "", "", "{}", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", nil) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "repos"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateRepo(context.TODO(), _repoOne) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + _, err = _sqlite.CreateRepo(context.TODO(), _repoTwo) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Repo + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Repo{_repoOne, _repoTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Repo{_repoOne, _repoTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListRepos(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("ListRepos for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListRepos for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListRepos for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/repo/list_user.go b/database/repo/list_user.go new file mode 100644 index 000000000..bf9b6ea8b --- /dev/null +++ b/database/repo/list_user.go @@ -0,0 +1,106 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListReposForUser gets a list of repos by user ID from the database. +// +//nolint:lll // ignore long line length due to variable names +func (e *engine) ListReposForUser(ctx context.Context, u *library.User, sortBy string, filters map[string]interface{}, page, perPage int) ([]*library.Repo, int64, error) { + e.logger.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Tracef("listing repos for user %s from the database", u.GetName()) + + // variables to store query results and return values + count := int64(0) + r := new([]database.Repo) + repos := []*library.Repo{} + + // count the results + count, err := e.CountReposForUser(ctx, u, filters) + if err != nil { + return repos, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return repos, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + switch sortBy { + case "latest": + query := e.client. + Table(constants.TableBuild). + Select("repos.id, MAX(builds.created) AS latest_build"). + Joins("INNER JOIN repos repos ON builds.repo_id = repos.id"). + Where("repos.user_id = ?", u.GetID()). + Group("repos.id") + + err = e.client. + Table(constants.TableRepo). + Select("repos.*"). + Joins("LEFT JOIN (?) t on repos.id = t.id", query). + Order("latest_build DESC NULLS LAST"). + Limit(perPage). + Offset(offset). + Find(&r). + Error + if err != nil { + return nil, count, err + } + case "name": + fallthrough + default: + err = e.client. + Table(constants.TableRepo). + Where("user_id = ?", u.GetID()). + Where(filters). + Order("name"). + Limit(perPage). + Offset(offset). + Find(&r). + Error + if err != nil { + return nil, count, err + } + } + + // iterate through all query results + for _, repo := range *r { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := repo + + // decrypt the fields for the repo + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt + err = tmp.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted repos + e.logger.Errorf("unable to decrypt repo %d: %v", tmp.ID.Int64, err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.ToLibrary + repos = append(repos, tmp.ToLibrary()) + } + + return repos, count, nil +} diff --git a/database/repo/list_user_test.go b/database/repo/list_user_test.go new file mode 100644 index 000000000..7f4fe0629 --- /dev/null +++ b/database/repo/list_user_test.go @@ -0,0 +1,183 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "reflect" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +func TestRepo_Engine_ListReposForUser(t *testing.T) { + // setup types + _buildOne := new(library.Build) + _buildOne.SetID(1) + _buildOne.SetRepoID(1) + _buildOne.SetNumber(1) + _buildOne.SetCreated(time.Now().UTC().Unix()) + + _buildTwo := new(library.Build) + _buildTwo.SetID(2) + _buildTwo.SetRepoID(2) + _buildTwo.SetNumber(1) + _buildTwo.SetCreated(time.Now().UTC().Unix()) + + _repoOne := testRepo() + _repoOne.SetID(1) + _repoOne.SetUserID(1) + _repoOne.SetHash("baz") + _repoOne.SetOrg("foo") + _repoOne.SetName("bar") + _repoOne.SetFullName("foo/bar") + _repoOne.SetVisibility("public") + _repoOne.SetPipelineType("yaml") + _repoOne.SetTopics([]string{}) + + _repoTwo := testRepo() + _repoTwo.SetID(2) + _repoTwo.SetUserID(1) + _repoTwo.SetHash("baz") + _repoTwo.SetOrg("bar") + _repoTwo.SetName("foo") + _repoTwo.SetFullName("bar/foo") + _repoTwo.SetVisibility("public") + _repoTwo.SetPipelineType("yaml") + _repoTwo.SetTopics([]string{}) + + _user := new(library.User) + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected name count query result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the name count query + _mock.ExpectQuery(`SELECT count(*) FROM "repos" WHERE user_id = $1`).WithArgs(1).WillReturnRows(_rows) + + // create expected name query result in mock + _rows = sqlmock.NewRows( + []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "topics", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}). + AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", "{}", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", nil). + AddRow(2, 1, "baz", "bar", "foo", "bar/foo", "", "", "", "{}", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", nil) + + // ensure the mock expects the name query + _mock.ExpectQuery(`SELECT * FROM "repos" WHERE user_id = $1 ORDER BY name LIMIT 10`).WithArgs(1).WillReturnRows(_rows) + + // create expected latest count query result in mock + _rows = sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the latest count query + _mock.ExpectQuery(`SELECT count(*) FROM "repos" WHERE user_id = $1`).WithArgs(1).WillReturnRows(_rows) + + // create expected latest query result in mock + _rows = sqlmock.NewRows( + []string{"id", "user_id", "hash", "org", "name", "full_name", "link", "clone", "branch", "topics", "timeout", "counter", "visibility", "private", "trusted", "active", "allow_pull", "allow_push", "allow_deploy", "allow_tag", "allow_comment", "pipeline_type", "previous_name"}). + AddRow(1, 1, "baz", "foo", "bar", "foo/bar", "", "", "", "{}", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", nil). + AddRow(2, 1, "baz", "bar", "foo", "bar/foo", "", "", "", "{}", 0, 0, "public", false, false, false, false, false, false, false, false, "yaml", nil) + + // ensure the mock expects the latest query + _mock.ExpectQuery(`SELECT repos.* FROM "repos" LEFT JOIN (SELECT repos.id, MAX(builds.created) AS latest_build FROM "builds" INNER JOIN repos repos ON builds.repo_id = repos.id WHERE repos.user_id = $1 GROUP BY "repos"."id") t on repos.id = t.id ORDER BY latest_build DESC NULLS LAST LIMIT 10`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateRepo(context.TODO(), _repoOne) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + _, err = _sqlite.CreateRepo(context.TODO(), _repoTwo) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + err = _sqlite.client.AutoMigrate(&database.Build{}) + if err != nil { + t.Errorf("unable to create build table for sqlite: %v", err) + } + + err = _sqlite.client.Table(constants.TableBuild).Create(database.BuildFromLibrary(_buildOne).Crop()).Error + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + err = _sqlite.client.Table(constants.TableBuild).Create(database.BuildFromLibrary(_buildTwo).Crop()).Error + if err != nil { + t.Errorf("unable to create test build for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + sort string + database *engine + want []*library.Repo + }{ + { + failure: false, + name: "postgres with name", + database: _postgres, + sort: "name", + want: []*library.Repo{_repoOne, _repoTwo}, + }, + { + failure: false, + name: "postgres with latest", + database: _postgres, + sort: "latest", + want: []*library.Repo{_repoOne, _repoTwo}, + }, + { + failure: false, + name: "sqlite with name", + database: _sqlite, + sort: "name", + want: []*library.Repo{_repoOne, _repoTwo}, + }, + { + failure: false, + name: "sqlite with latest", + database: _sqlite, + sort: "latest", + want: []*library.Repo{_repoOne, _repoTwo}, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListReposForUser(context.TODO(), _user, test.sort, filters, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListReposForUser for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListReposForUser for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListReposForUser for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/repo/opts.go b/database/repo/opts.go new file mode 100644 index 000000000..41a2b9492 --- /dev/null +++ b/database/repo/opts.go @@ -0,0 +1,65 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Repos. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Repos. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the repo engine + e.client = client + + return nil + } +} + +// WithEncryptionKey sets the encryption key in the database engine for Repos. +func WithEncryptionKey(key string) EngineOpt { + return func(e *engine) error { + // set the encryption key in the repo engine + e.config.EncryptionKey = key + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Repos. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the repo engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Repos. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the repo engine + e.config.SkipCreation = skipCreation + + return nil + } +} + +// WithContext sets the context in the database engine for Repos. +func WithContext(ctx context.Context) EngineOpt { + return func(e *engine) error { + e.ctx = ctx + + return nil + } +} diff --git a/database/repo/opts_test.go b/database/repo/opts_test.go new file mode 100644 index 000000000..4f9e981ae --- /dev/null +++ b/database/repo/opts_test.go @@ -0,0 +1,260 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestRepo_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestRepo_EngineOpt_WithEncryptionKey(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + key string + want string + }{ + { + failure: false, + name: "encryption key set", + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + want: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + }, + { + failure: false, + name: "encryption key not set", + key: "", + want: "", + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithEncryptionKey(test.key)(e) + + if test.failure { + if err == nil { + t.Errorf("WithEncryptionKey for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithEncryptionKey returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.EncryptionKey, test.want) { + t.Errorf("WithEncryptionKey is %v, want %v", e.config.EncryptionKey, test.want) + } + }) + } +} + +func TestRepo_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestRepo_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} + +func TestRepo_EngineOpt_WithContext(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + ctx context.Context + want context.Context + }{ + { + failure: false, + name: "context set to TODO", + ctx: context.TODO(), + want: context.TODO(), + }, + { + failure: false, + name: "context set to nil", + ctx: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithContext(test.ctx)(e) + + if test.failure { + if err == nil { + t.Errorf("WithContext for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithContext returned err: %v", err) + } + + if !reflect.DeepEqual(e.ctx, test.want) { + t.Errorf("WithContext is %v, want %v", e.ctx, test.want) + } + }) + } +} diff --git a/database/repo/repo.go b/database/repo/repo.go new file mode 100644 index 000000000..a74c34956 --- /dev/null +++ b/database/repo/repo.go @@ -0,0 +1,85 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the RepoInterface interface. + config struct { + // specifies the encryption key to use for the Repo engine + EncryptionKey string + // specifies to skip creating tables and indexes for the Repo engine + SkipCreation bool + } + + // engine represents the repo functionality that implements the RepoInterface interface. + engine struct { + // engine configuration settings used in repo functions + config *config + + ctx context.Context + + // gorm.io/gorm database client used in repo functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in repo functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with repos in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new Repo engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating repo database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of repos table and indexes in the database") + + return e, nil + } + + // create the repos table + err := e.CreateRepoTable(e.ctx, e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableRepo, err) + } + + // create the indexes for the repos table + err = e.CreateRepoIndexes(e.ctx) + if err != nil { + return nil, fmt.Errorf("unable to create indexes for %s table: %w", constants.TableRepo, err) + } + + return e, nil +} diff --git a/database/repo/repo_test.go b/database/repo/repo_test.go new file mode 100644 index 000000000..8729a56f7 --- /dev/null +++ b/database/repo/repo_test.go @@ -0,0 +1,215 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "database/sql/driver" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestRepo_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateOrgNameIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", SkipCreation: false}, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", SkipCreation: false}, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithClient(test.client), + WithEncryptionKey(test.key), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateOrgNameIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres repo engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite repo engine: %v", err) + } + + return _engine +} + +// testRepo is a test helper function to create a library +// Repo type with all fields set to their zero values. +func testRepo() *library.Repo { + return &library.Repo{ + ID: new(int64), + UserID: new(int64), + BuildLimit: new(int64), + Timeout: new(int64), + Counter: new(int), + PipelineType: new(string), + Hash: new(string), + Org: new(string), + Name: new(string), + FullName: new(string), + Link: new(string), + Clone: new(string), + Branch: new(string), + Visibility: new(string), + PreviousName: new(string), + Private: new(bool), + Trusted: new(bool), + Active: new(bool), + AllowPull: new(bool), + AllowPush: new(bool), + AllowDeploy: new(bool), + AllowTag: new(bool), + AllowComment: new(bool), + } +} + +// This will be used with the github.com/DATA-DOG/go-sqlmock library to compare values +// that are otherwise not easily compared. These typically would be values generated +// before adding or updating them in the database. +// +// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime +type AnyArgument struct{} + +// Match satisfies sqlmock.Argument interface. +func (a AnyArgument) Match(v driver.Value) bool { + return true +} diff --git a/database/repo/table.go b/database/repo/table.go new file mode 100644 index 000000000..aa570dd43 --- /dev/null +++ b/database/repo/table.go @@ -0,0 +1,96 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres repos table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +repos ( + id SERIAL PRIMARY KEY, + user_id INTEGER, + hash VARCHAR(500), + org VARCHAR(250), + name VARCHAR(250), + full_name VARCHAR(500), + link VARCHAR(1000), + clone VARCHAR(1000), + branch VARCHAR(250), + topics VARCHAR(1020), + build_limit INTEGER, + timeout INTEGER, + counter INTEGER, + visibility TEXT, + private BOOLEAN, + trusted BOOLEAN, + active BOOLEAN, + allow_pull BOOLEAN, + allow_push BOOLEAN, + allow_deploy BOOLEAN, + allow_tag BOOLEAN, + allow_comment BOOLEAN, + pipeline_type TEXT, + previous_name VARCHAR(100), + UNIQUE(full_name) +); +` + + // CreateSqliteTable represents a query to create the Sqlite repos table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +repos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + hash TEXT, + org TEXT, + name TEXT, + full_name TEXT, + link TEXT, + clone TEXT, + branch TEXT, + topics TEXT, + build_limit INTEGER, + timeout INTEGER, + counter INTEGER, + visibility TEXT, + private BOOLEAN, + trusted BOOLEAN, + active BOOLEAN, + allow_pull BOOLEAN, + allow_push BOOLEAN, + allow_deploy BOOLEAN, + allow_tag BOOLEAN, + allow_comment BOOLEAN, + pipeline_type TEXT, + previous_name TEXT, + UNIQUE(full_name) +); +` +) + +// CreateRepoTable creates the repos table in the database. +func (e *engine) CreateRepoTable(ctx context.Context, driver string) error { + e.logger.Tracef("creating repos table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the repos table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the repos table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/repo/table_test.go b/database/repo/table_test.go new file mode 100644 index 000000000..021f76673 --- /dev/null +++ b/database/repo/table_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestRepo_Engine_CreateRepoTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateRepoTable(context.TODO(), test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateRepoTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateRepoTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/repo/update.go b/database/repo/update.go new file mode 100644 index 000000000..fbbce6b64 --- /dev/null +++ b/database/repo/update.go @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with create.go +package repo + +import ( + "context" + "fmt" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// UpdateRepo updates an existing repo in the database. +func (e *engine) UpdateRepo(ctx context.Context, r *library.Repo) (*library.Repo, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("creating repo %s in the database", r.GetFullName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#RepoFromLibrary + repo := database.RepoFromLibrary(r) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Validate + err := repo.Validate() + if err != nil { + return nil, err + } + + // encrypt the fields for the repo + // + // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Encrypt + err = repo.Encrypt(e.config.EncryptionKey) + if err != nil { + return nil, fmt.Errorf("unable to encrypt repo %s: %w", r.GetFullName(), err) + } + + // send query to the database + err = e.client.Table(constants.TableRepo).Save(repo).Error + if err != nil { + return nil, err + } + + // decrypt the fields for the repo + err = repo.Decrypt(e.config.EncryptionKey) + if err != nil { + // only log to preserve backwards compatibility + e.logger.Errorf("unable to decrypt repo %d: %v", r.GetID(), err) + + return repo.ToLibrary(), nil + } + + return repo.ToLibrary(), nil +} diff --git a/database/repo/update_test.go b/database/repo/update_test.go new file mode 100644 index 000000000..459a4325a --- /dev/null +++ b/database/repo/update_test.go @@ -0,0 +1,87 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package repo + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestRepo_Engine_UpdateRepo(t *testing.T) { + // setup types + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + _repo.SetPipelineType("yaml") + _repo.SetPreviousName("oldName") + _repo.SetTopics([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`UPDATE "repos" +SET "user_id"=$1,"hash"=$2,"org"=$3,"name"=$4,"full_name"=$5,"link"=$6,"clone"=$7,"branch"=$8,"topics"=$9,"build_limit"=$10,"timeout"=$11,"counter"=$12,"visibility"=$13,"private"=$14,"trusted"=$15,"active"=$16,"allow_pull"=$17,"allow_push"=$18,"allow_deploy"=$19,"allow_tag"=$20,"allow_comment"=$21,"pipeline_type"=$22,"previous_name"=$23 +WHERE "id" = $24`). + WithArgs(1, AnyArgument{}, "foo", "bar", "foo/bar", nil, nil, nil, AnyArgument{}, AnyArgument{}, AnyArgument{}, AnyArgument{}, "public", false, false, false, false, false, false, false, false, "yaml", "oldName", 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateRepo(context.TODO(), _repo) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.UpdateRepo(context.TODO(), _repo) + + if test.failure { + if err == nil { + t.Errorf("UpdateRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _repo) { + t.Errorf("UpdateRepo for %s returned %s, want %s", test.name, got, _repo) + } + }) + } +} diff --git a/database/resource.go b/database/resource.go new file mode 100644 index 000000000..74a4b8fe5 --- /dev/null +++ b/database/resource.go @@ -0,0 +1,163 @@ +// Copyright (c) 2023 Target Brands, Ine. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +import ( + "context" + + "github.com/go-vela/server/database/build" + "github.com/go-vela/server/database/executable" + "github.com/go-vela/server/database/hook" + "github.com/go-vela/server/database/log" + "github.com/go-vela/server/database/pipeline" + "github.com/go-vela/server/database/repo" + "github.com/go-vela/server/database/schedule" + "github.com/go-vela/server/database/secret" + "github.com/go-vela/server/database/service" + "github.com/go-vela/server/database/step" + "github.com/go-vela/server/database/user" + "github.com/go-vela/server/database/worker" +) + +// NewResources creates and returns the database agnostic engines for resources. +func (e *engine) NewResources(ctx context.Context) error { + var err error + + // create the database agnostic engine for builds + e.BuildInterface, err = build.New( + build.WithContext(e.ctx), + build.WithClient(e.client), + build.WithLogger(e.logger), + build.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + + // create the database agnostic engine for build_executables + e.BuildExecutableInterface, err = executable.New( + executable.WithContext(e.ctx), + executable.WithClient(e.client), + executable.WithLogger(e.logger), + executable.WithSkipCreation(e.config.SkipCreation), + executable.WithEncryptionKey(e.config.EncryptionKey), + executable.WithDriver(e.config.Driver), + ) + if err != nil { + return err + } + + // create the database agnostic engine for hooks + e.HookInterface, err = hook.New( + hook.WithClient(e.client), + hook.WithLogger(e.logger), + hook.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + + // create the database agnostic engine for logs + e.LogInterface, err = log.New( + log.WithClient(e.client), + log.WithCompressionLevel(e.config.CompressionLevel), + log.WithLogger(e.logger), + log.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + + // create the database agnostic engine for pipelines + e.PipelineInterface, err = pipeline.New( + pipeline.WithContext(e.ctx), + pipeline.WithClient(e.client), + pipeline.WithCompressionLevel(e.config.CompressionLevel), + pipeline.WithLogger(e.logger), + pipeline.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + + // create the database agnostic engine for repos + e.RepoInterface, err = repo.New( + repo.WithContext(e.ctx), + repo.WithClient(e.client), + repo.WithEncryptionKey(e.config.EncryptionKey), + repo.WithLogger(e.logger), + repo.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + + // create the database agnostic engine for schedules + e.ScheduleInterface, err = schedule.New( + schedule.WithContext(e.ctx), + schedule.WithClient(e.client), + schedule.WithLogger(e.logger), + schedule.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + + // create the database agnostic engine for secrets + // + // https://pkg.go.dev/github.com/go-vela/server/database/secret#New + e.SecretInterface, err = secret.New( + secret.WithClient(e.client), + secret.WithEncryptionKey(e.config.EncryptionKey), + secret.WithLogger(e.logger), + secret.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + + // create the database agnostic engine for services + e.ServiceInterface, err = service.New( + service.WithClient(e.client), + service.WithLogger(e.logger), + service.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + + // create the database agnostic engine for steps + e.StepInterface, err = step.New( + step.WithClient(e.client), + step.WithLogger(e.logger), + step.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + + // create the database agnostic engine for users + e.UserInterface, err = user.New( + user.WithClient(e.client), + user.WithEncryptionKey(e.config.EncryptionKey), + user.WithLogger(e.logger), + user.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + + // create the database agnostic engine for workers + e.WorkerInterface, err = worker.New( + worker.WithClient(e.client), + worker.WithLogger(e.logger), + worker.WithSkipCreation(e.config.SkipCreation), + ) + if err != nil { + return err + } + + return nil +} diff --git a/database/resource_test.go b/database/resource_test.go new file mode 100644 index 000000000..328f04f58 --- /dev/null +++ b/database/resource_test.go @@ -0,0 +1,116 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/server/database/build" + "github.com/go-vela/server/database/executable" + "github.com/go-vela/server/database/hook" + "github.com/go-vela/server/database/log" + "github.com/go-vela/server/database/pipeline" + "github.com/go-vela/server/database/repo" + "github.com/go-vela/server/database/schedule" + "github.com/go-vela/server/database/secret" + "github.com/go-vela/server/database/service" + "github.com/go-vela/server/database/step" + "github.com/go-vela/server/database/user" + "github.com/go-vela/server/database/worker" +) + +func TestDatabase_Engine_NewResources(t *testing.T) { + _postgres, _mock := testPostgres(t) + defer _postgres.Close() + + // ensure the mock expects the build queries + _mock.ExpectExec(build.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(build.CreateCreatedIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(build.CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(build.CreateSourceIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(build.CreateStatusIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the build executable queries + _mock.ExpectExec(executable.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the hook queries + _mock.ExpectExec(hook.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(hook.CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the log queries + _mock.ExpectExec(log.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(log.CreateBuildIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the pipeline queries + _mock.ExpectExec(pipeline.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(pipeline.CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the repo queries + _mock.ExpectExec(repo.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(repo.CreateOrgNameIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the schedule queries + _mock.ExpectExec(schedule.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(schedule.CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the secret queries + _mock.ExpectExec(secret.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(secret.CreateTypeOrgRepo).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(secret.CreateTypeOrgTeam).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(secret.CreateTypeOrg).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the service queries + _mock.ExpectExec(service.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the step queries + _mock.ExpectExec(step.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the user queries + _mock.ExpectExec(user.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(user.CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + // ensure the mock expects the worker queries + _mock.ExpectExec(worker.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(worker.CreateHostnameAddressIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create a test database without mocking the call + _unmocked, _ := testPostgres(t) + + _sqlite := testSqlite(t) + defer _sqlite.Close() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + name: "success with postgres", + failure: false, + database: _postgres, + }, + { + name: "success with sqlite3", + failure: false, + database: _sqlite, + }, + { + name: "failure without mocked call", + failure: true, + database: _unmocked, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.NewResources(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("NewResources for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("NewResources for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/schedule/count.go b/database/schedule/count.go new file mode 100644 index 000000000..7a0d8c1ee --- /dev/null +++ b/database/schedule/count.go @@ -0,0 +1,26 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "github.com/go-vela/types/constants" +) + +// CountSchedules gets the count of all schedules from the database. +func (e *engine) CountSchedules(ctx context.Context) (int64, error) { + e.logger.Tracef("getting count of all schedules from the database") + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSchedule). + Count(&s). + Error + + return s, err +} diff --git a/database/schedule/count_active.go b/database/schedule/count_active.go new file mode 100644 index 000000000..d65315186 --- /dev/null +++ b/database/schedule/count_active.go @@ -0,0 +1,27 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "github.com/go-vela/types/constants" +) + +// CountActiveSchedules gets the count of all active schedules from the database. +func (e *engine) CountActiveSchedules(ctx context.Context) (int64, error) { + e.logger.Tracef("getting count of all active schedules from the database") + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSchedule). + Where("active = ?", true). + Count(&s). + Error + + return s, err +} diff --git a/database/schedule/count_active_test.go b/database/schedule/count_active_test.go new file mode 100644 index 000000000..dcf9d2cb0 --- /dev/null +++ b/database/schedule/count_active_test.go @@ -0,0 +1,105 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSchedule_Engine_CountActiveSchedules(t *testing.T) { + _scheduleOne := testSchedule() + _scheduleOne.SetID(1) + _scheduleOne.SetRepoID(1) + _scheduleOne.SetActive(true) + _scheduleOne.SetName("nightly") + _scheduleOne.SetEntry("0 0 * * *") + _scheduleOne.SetCreatedAt(1) + _scheduleOne.SetCreatedBy("user1") + _scheduleOne.SetUpdatedAt(1) + _scheduleOne.SetUpdatedBy("user2") + _scheduleOne.SetBranch("main") + + _scheduleTwo := testSchedule() + _scheduleTwo.SetID(2) + _scheduleTwo.SetRepoID(2) + _scheduleTwo.SetActive(false) + _scheduleTwo.SetName("hourly") + _scheduleTwo.SetEntry("0 * * * *") + _scheduleTwo.SetCreatedAt(1) + _scheduleTwo.SetCreatedBy("user1") + _scheduleTwo.SetUpdatedAt(1) + _scheduleTwo.SetUpdatedBy("user2") + _scheduleTwo.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "schedules" WHERE active = $1`).WithArgs(true).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSchedule(context.TODO(), _scheduleOne) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + _, err = _sqlite.CreateSchedule(context.TODO(), _scheduleTwo) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 1, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 1, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountActiveSchedules(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("CountActiveSchedules for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountActiveSchedules for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountActiveSchedules for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/schedule/count_repo.go b/database/schedule/count_repo.go new file mode 100644 index 000000000..da806151c --- /dev/null +++ b/database/schedule/count_repo.go @@ -0,0 +1,32 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CountSchedulesForRepo gets the count of schedules by repo ID from the database. +func (e *engine) CountSchedulesForRepo(ctx context.Context, r *library.Repo) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("getting count of schedules for repo %s from the database", r.GetFullName()) + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSchedule). + Where("repo_id = ?", r.GetID()). + Count(&s). + Error + + return s, err +} diff --git a/database/schedule/count_repo_test.go b/database/schedule/count_repo_test.go new file mode 100644 index 000000000..35c530d7c --- /dev/null +++ b/database/schedule/count_repo_test.go @@ -0,0 +1,109 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSchedule_Engine_CountSchedulesForRepo(t *testing.T) { + _repo := testRepo() + _repo.SetID(1) + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + + _scheduleOne := testSchedule() + _scheduleOne.SetID(1) + _scheduleOne.SetRepoID(1) + _scheduleOne.SetName("nightly") + _scheduleOne.SetEntry("0 0 * * *") + _scheduleOne.SetCreatedAt(1) + _scheduleOne.SetCreatedBy("user1") + _scheduleOne.SetUpdatedAt(1) + _scheduleOne.SetUpdatedBy("user2") + _scheduleOne.SetBranch("main") + + _scheduleTwo := testSchedule() + _scheduleTwo.SetID(2) + _scheduleTwo.SetRepoID(2) + _scheduleTwo.SetName("hourly") + _scheduleTwo.SetEntry("0 * * * *") + _scheduleTwo.SetCreatedAt(1) + _scheduleTwo.SetCreatedBy("user1") + _scheduleTwo.SetUpdatedAt(1) + _scheduleTwo.SetUpdatedBy("user2") + _scheduleTwo.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "schedules" WHERE repo_id = $1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSchedule(context.TODO(), _scheduleOne) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + _, err = _sqlite.CreateSchedule(context.TODO(), _scheduleTwo) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 1, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 1, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountSchedulesForRepo(context.TODO(), _repo) + + if test.failure { + if err == nil { + t.Errorf("CountSchedulesForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountSchedulesForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountSchedulesForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/schedule/count_test.go b/database/schedule/count_test.go new file mode 100644 index 000000000..c98ba7224 --- /dev/null +++ b/database/schedule/count_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSchedule_Engine_CountSchedules(t *testing.T) { + _scheduleOne := testSchedule() + _scheduleOne.SetID(1) + _scheduleOne.SetRepoID(1) + _scheduleOne.SetName("nightly") + _scheduleOne.SetEntry("0 0 * * *") + _scheduleOne.SetCreatedAt(1) + _scheduleOne.SetCreatedBy("user1") + _scheduleOne.SetUpdatedAt(1) + _scheduleOne.SetUpdatedBy("user2") + _scheduleOne.SetBranch("main") + + _scheduleTwo := testSchedule() + _scheduleTwo.SetID(2) + _scheduleTwo.SetRepoID(2) + _scheduleTwo.SetName("hourly") + _scheduleTwo.SetEntry("0 * * * *") + _scheduleTwo.SetCreatedAt(1) + _scheduleTwo.SetCreatedBy("user1") + _scheduleTwo.SetUpdatedAt(1) + _scheduleTwo.SetUpdatedBy("user2") + _scheduleTwo.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "schedules"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSchedule(context.TODO(), _scheduleOne) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + _, err = _sqlite.CreateSchedule(context.TODO(), _scheduleTwo) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountSchedules(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("CountSchedules for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountSchedules for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountSchedules for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/schedule/create.go b/database/schedule/create.go new file mode 100644 index 000000000..8a5907263 --- /dev/null +++ b/database/schedule/create.go @@ -0,0 +1,36 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with update.go +package schedule + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreateSchedule creates a new schedule in the database. +func (e *engine) CreateSchedule(ctx context.Context, s *library.Schedule) (*library.Schedule, error) { + e.logger.WithFields(logrus.Fields{ + "schedule": s.GetName(), + }).Tracef("creating schedule %s in the database", s.GetName()) + + // cast the library type to database type + schedule := database.ScheduleFromLibrary(s) + + // validate the necessary fields are populated + err := schedule.Validate() + if err != nil { + return nil, err + } + + // send query to the database + result := e.client.Table(constants.TableSchedule).Create(schedule) + + return schedule.ToLibrary(), result.Error +} diff --git a/database/schedule/create_test.go b/database/schedule/create_test.go new file mode 100644 index 000000000..272e91a41 --- /dev/null +++ b/database/schedule/create_test.go @@ -0,0 +1,83 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSchedule_Engine_CreateSchedule(t *testing.T) { + _schedule := testSchedule() + _schedule.SetID(1) + _schedule.SetRepoID(1) + _schedule.SetName("nightly") + _schedule.SetEntry("0 0 * * *") + _schedule.SetCreatedAt(1) + _schedule.SetCreatedBy("user1") + _schedule.SetUpdatedAt(1) + _schedule.SetUpdatedBy("user2") + _schedule.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`INSERT INTO "schedules" +("repo_id","active","name","entry","created_at","created_by","updated_at","updated_by","scheduled_at","branch","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING "id"`). + WithArgs(1, false, "nightly", "0 0 * * *", 1, "user1", 1, "user2", nil, "main", 1). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CreateSchedule(context.TODO(), _schedule) + + if test.failure { + if err == nil { + t.Errorf("CreateSchedule for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateSchedule for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _schedule) { + t.Errorf("CreateSchedule for %s returned %s, want %s", test.name, got, _schedule) + } + }) + } +} diff --git a/database/schedule/delete.go b/database/schedule/delete.go new file mode 100644 index 000000000..765b31120 --- /dev/null +++ b/database/schedule/delete.go @@ -0,0 +1,29 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// DeleteSchedule deletes an existing schedule from the database. +func (e *engine) DeleteSchedule(ctx context.Context, s *library.Schedule) error { + e.logger.WithFields(logrus.Fields{ + "schedule": s.GetName(), + }).Tracef("deleting schedule %s in the database", s.GetName()) + + // cast the library type to database type + schedule := database.ScheduleFromLibrary(s) + + // send query to the database + return e.client. + Table(constants.TableSchedule). + Delete(schedule). + Error +} diff --git a/database/schedule/delete_test.go b/database/schedule/delete_test.go new file mode 100644 index 000000000..8dff8ad78 --- /dev/null +++ b/database/schedule/delete_test.go @@ -0,0 +1,78 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSchedule_Engine_DeleteSchedule(t *testing.T) { + _schedule := testSchedule() + _schedule.SetID(1) + _schedule.SetRepoID(1) + _schedule.SetName("nightly") + _schedule.SetEntry("0 0 * * *") + _schedule.SetCreatedAt(1) + _schedule.SetCreatedBy("user1") + _schedule.SetUpdatedAt(1) + _schedule.SetUpdatedBy("user2") + _schedule.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`DELETE FROM "schedules" WHERE "schedules"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSchedule(context.TODO(), _schedule) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeleteSchedule(context.TODO(), _schedule) + + if test.failure { + if err == nil { + t.Errorf("DeleteSchedule for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeleteSchedule for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/schedule/get.go b/database/schedule/get.go new file mode 100644 index 000000000..e4540f0f0 --- /dev/null +++ b/database/schedule/get.go @@ -0,0 +1,32 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetSchedule gets a schedule by ID from the database. +func (e *engine) GetSchedule(ctx context.Context, id int64) (*library.Schedule, error) { + e.logger.Tracef("getting schedule %d from the database", id) + + // variable to store query results + s := new(database.Schedule) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSchedule). + Where("id = ?", id). + Take(s). + Error + if err != nil { + return nil, err + } + + return s.ToLibrary(), nil +} diff --git a/database/schedule/get_repo.go b/database/schedule/get_repo.go new file mode 100644 index 000000000..d0ddc4271 --- /dev/null +++ b/database/schedule/get_repo.go @@ -0,0 +1,38 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetScheduleForRepo gets a schedule by repo ID and name from the database. +func (e *engine) GetScheduleForRepo(ctx context.Context, r *library.Repo, name string) (*library.Schedule, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + "schedule": name, + }).Tracef("getting schedule %s/%s from the database", r.GetFullName(), name) + + // variable to store query results + s := new(database.Schedule) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSchedule). + Where("repo_id = ?", r.GetID()). + Where("name = ?", name). + Take(s). + Error + if err != nil { + return nil, err + } + + return s.ToLibrary(), nil +} diff --git a/database/schedule/get_repo_test.go b/database/schedule/get_repo_test.go new file mode 100644 index 000000000..998b58fc3 --- /dev/null +++ b/database/schedule/get_repo_test.go @@ -0,0 +1,96 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSchedule_Engine_GetScheduleForRepo(t *testing.T) { + _repo := testRepo() + _repo.SetID(1) + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + + _schedule := testSchedule() + _schedule.SetID(1) + _schedule.SetRepoID(1) + _schedule.SetName("nightly") + _schedule.SetEntry("0 0 * * *") + _schedule.SetCreatedAt(1) + _schedule.SetCreatedBy("user1") + _schedule.SetUpdatedAt(1) + _schedule.SetUpdatedBy("user2") + _schedule.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "active", "name", "entry", "created_at", "created_by", "updated_at", "updated_by", "scheduled_at", "branch"}, + ).AddRow(1, 1, false, "nightly", "0 0 * * *", 1, "user1", 1, "user2", nil, "main") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "schedules" WHERE repo_id = $1 AND name = $2 LIMIT 1`).WithArgs(1, "nightly").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSchedule(context.TODO(), _schedule) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Schedule + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _schedule, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _schedule, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetScheduleForRepo(context.TODO(), _repo, "nightly") + + if test.failure { + if err == nil { + t.Errorf("GetScheduleForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetScheduleForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetScheduleForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/schedule/get_test.go b/database/schedule/get_test.go new file mode 100644 index 000000000..bc4a61f75 --- /dev/null +++ b/database/schedule/get_test.go @@ -0,0 +1,90 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSchedule_Engine_GetSchedule(t *testing.T) { + _schedule := testSchedule() + _schedule.SetID(1) + _schedule.SetRepoID(1) + _schedule.SetName("nightly") + _schedule.SetEntry("0 0 * * *") + _schedule.SetCreatedAt(1) + _schedule.SetCreatedBy("user1") + _schedule.SetUpdatedAt(1) + _schedule.SetUpdatedBy("user2") + _schedule.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "active", "name", "entry", "created_at", "created_by", "updated_at", "updated_by", "scheduled_at", "branch"}, + ).AddRow(1, 1, false, "nightly", "0 0 * * *", 1, "user1", 1, "user2", nil, "main") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "schedules" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSchedule(context.TODO(), _schedule) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Schedule + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _schedule, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _schedule, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetSchedule(context.TODO(), 1) + + if test.failure { + if err == nil { + t.Errorf("GetSchedule for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetSchedule for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetSchedule for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/schedule/index.go b/database/schedule/index.go new file mode 100644 index 000000000..6b88199fa --- /dev/null +++ b/database/schedule/index.go @@ -0,0 +1,26 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import "context" + +const ( + // CreateRepoIDIndex represents a query to create an + // index on the schedules table for the repo_id column. + CreateRepoIDIndex = ` +CREATE INDEX +IF NOT EXISTS +schedules_repo_id +ON schedules (repo_id); +` +) + +// CreateScheduleIndexes creates the indexes for the schedules table in the database. +func (e *engine) CreateScheduleIndexes(ctx context.Context) error { + e.logger.Tracef("creating indexes for schedules table in the database") + + // create the repo_id column index for the schedules table + return e.client.Exec(CreateRepoIDIndex).Error +} diff --git a/database/schedule/index_test.go b/database/schedule/index_test.go new file mode 100644 index 000000000..145fd5ac7 --- /dev/null +++ b/database/schedule/index_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSchedule_Engine_CreateScheduleIndexes(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateScheduleIndexes(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("CreateScheduleIndexes for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateScheduleIndexes for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/schedule/interface.go b/database/schedule/interface.go new file mode 100644 index 000000000..402499ab1 --- /dev/null +++ b/database/schedule/interface.go @@ -0,0 +1,51 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + + "github.com/go-vela/types/library" +) + +// ScheduleInterface represents the Vela interface for schedule +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type ScheduleInterface interface { + // Schedule Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreateScheduleIndexes defines a function that creates the indexes for the schedules table. + CreateScheduleIndexes(context.Context) error + // CreateScheduleTable defines a function that creates the schedules table. + CreateScheduleTable(context.Context, string) error + + // Schedule Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CountSchedules defines a function that gets the count of all schedules. + CountSchedules(context.Context) (int64, error) + // CountSchedulesForRepo defines a function that gets the count of schedules by repo ID. + CountSchedulesForRepo(context.Context, *library.Repo) (int64, error) + // CreateSchedule defines a function that creates a new schedule. + CreateSchedule(context.Context, *library.Schedule) (*library.Schedule, error) + // DeleteSchedule defines a function that deletes an existing schedule. + DeleteSchedule(context.Context, *library.Schedule) error + // GetSchedule defines a function that gets a schedule by ID. + GetSchedule(context.Context, int64) (*library.Schedule, error) + // GetScheduleForRepo defines a function that gets a schedule by repo ID and name. + GetScheduleForRepo(context.Context, *library.Repo, string) (*library.Schedule, error) + // ListActiveSchedules defines a function that gets a list of all active schedules. + ListActiveSchedules(context.Context) ([]*library.Schedule, error) + // ListSchedules defines a function that gets a list of all schedules. + ListSchedules(context.Context) ([]*library.Schedule, error) + // ListSchedulesForRepo defines a function that gets a list of schedules by repo ID. + ListSchedulesForRepo(context.Context, *library.Repo, int, int) ([]*library.Schedule, int64, error) + // UpdateSchedule defines a function that updates an existing schedule. + UpdateSchedule(context.Context, *library.Schedule, bool) (*library.Schedule, error) +} diff --git a/database/schedule/list.go b/database/schedule/list.go new file mode 100644 index 000000000..fbba87ca2 --- /dev/null +++ b/database/schedule/list.go @@ -0,0 +1,53 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListSchedules gets a list of all schedules from the database. +func (e *engine) ListSchedules(ctx context.Context) ([]*library.Schedule, error) { + e.logger.Trace("listing all schedules from the database") + + // variables to store query results and return value + count := int64(0) + s := new([]database.Schedule) + schedules := []*library.Schedule{} + + // count the results + count, err := e.CountSchedules(ctx) + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return schedules, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableSchedule). + Find(&s). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, schedule := range *s { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := schedule + + // convert query result to API type + schedules = append(schedules, tmp.ToLibrary()) + } + + return schedules, nil +} diff --git a/database/schedule/list_active.go b/database/schedule/list_active.go new file mode 100644 index 000000000..0afd4ddc5 --- /dev/null +++ b/database/schedule/list_active.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListActiveSchedules gets a list of all active schedules from the database. +func (e *engine) ListActiveSchedules(ctx context.Context) ([]*library.Schedule, error) { + e.logger.Trace("listing all active schedules from the database") + + // variables to store query results and return value + count := int64(0) + s := new([]database.Schedule) + schedules := []*library.Schedule{} + + // count the results + count, err := e.CountActiveSchedules(ctx) + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return schedules, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableSchedule). + Where("active = ?", true). + Find(&s). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, schedule := range *s { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := schedule + + // convert query result to API type + schedules = append(schedules, tmp.ToLibrary()) + } + + return schedules, nil +} diff --git a/database/schedule/list_active_test.go b/database/schedule/list_active_test.go new file mode 100644 index 000000000..2bce7ed06 --- /dev/null +++ b/database/schedule/list_active_test.go @@ -0,0 +1,114 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSchedule_Engine_ListActiveSchedules(t *testing.T) { + _scheduleOne := testSchedule() + _scheduleOne.SetID(1) + _scheduleOne.SetRepoID(1) + _scheduleOne.SetActive(true) + _scheduleOne.SetName("nightly") + _scheduleOne.SetEntry("0 0 * * *") + _scheduleOne.SetCreatedAt(1) + _scheduleOne.SetCreatedBy("user1") + _scheduleOne.SetUpdatedAt(1) + _scheduleOne.SetUpdatedBy("user2") + _scheduleOne.SetBranch("main") + + _scheduleTwo := testSchedule() + _scheduleTwo.SetID(2) + _scheduleTwo.SetRepoID(2) + _scheduleTwo.SetActive(false) + _scheduleTwo.SetName("hourly") + _scheduleTwo.SetEntry("0 * * * *") + _scheduleTwo.SetCreatedAt(1) + _scheduleTwo.SetCreatedBy("user1") + _scheduleTwo.SetUpdatedAt(1) + _scheduleTwo.SetUpdatedBy("user2") + _scheduleTwo.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "schedules" WHERE active = $1`).WithArgs(true).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "active", "name", "entry", "created_at", "created_by", "updated_at", "updated_by", "scheduled_at", "branch"}). + AddRow(1, 1, true, "nightly", "0 0 * * *", 1, "user1", 1, "user2", nil, "main") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "schedules" WHERE active = $1`).WithArgs(true).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSchedule(context.TODO(), _scheduleOne) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + _, err = _sqlite.CreateSchedule(context.TODO(), _scheduleTwo) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Schedule + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Schedule{_scheduleOne}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Schedule{_scheduleOne}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListActiveSchedules(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("ListActiveSchedules for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListActiveSchedules for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListActiveSchedules for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/schedule/list_repo.go b/database/schedule/list_repo.go new file mode 100644 index 000000000..9d0cfed5e --- /dev/null +++ b/database/schedule/list_repo.go @@ -0,0 +1,64 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListSchedulesForRepo gets a list of schedules by repo ID from the database. +func (e *engine) ListSchedulesForRepo(ctx context.Context, r *library.Repo, page, perPage int) ([]*library.Schedule, int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("listing schedules for repo %s from the database", r.GetFullName()) + + // variables to store query results and return value + count := int64(0) + s := new([]database.Schedule) + schedules := []*library.Schedule{} + + // count the results + count, err := e.CountSchedulesForRepo(ctx, r) + if err != nil { + return nil, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return schedules, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableSchedule). + Where("repo_id = ?", r.GetID()). + Order("id DESC"). + Limit(perPage). + Offset(offset). + Find(&s). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, schedule := range *s { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := schedule + + // convert query result to library type + schedules = append(schedules, tmp.ToLibrary()) + } + + return schedules, count, nil +} diff --git a/database/schedule/list_repo_test.go b/database/schedule/list_repo_test.go new file mode 100644 index 000000000..2d573448d --- /dev/null +++ b/database/schedule/list_repo_test.go @@ -0,0 +1,118 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSchedule_Engine_ListSchedulesForRepo(t *testing.T) { + _repo := testRepo() + _repo.SetID(1) + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + + _scheduleOne := testSchedule() + _scheduleOne.SetID(1) + _scheduleOne.SetRepoID(1) + _scheduleOne.SetName("nightly") + _scheduleOne.SetEntry("0 0 * * *") + _scheduleOne.SetCreatedAt(1) + _scheduleOne.SetCreatedBy("user1") + _scheduleOne.SetUpdatedAt(1) + _scheduleOne.SetUpdatedBy("user2") + _scheduleOne.SetBranch("main") + + _scheduleTwo := testSchedule() + _scheduleTwo.SetID(2) + _scheduleTwo.SetRepoID(2) + _scheduleTwo.SetName("hourly") + _scheduleTwo.SetEntry("0 * * * *") + _scheduleTwo.SetCreatedAt(1) + _scheduleTwo.SetCreatedBy("user1") + _scheduleTwo.SetUpdatedAt(1) + _scheduleTwo.SetUpdatedBy("user2") + _scheduleTwo.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "schedules" WHERE repo_id = $1`).WithArgs(1).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "active", "name", "entry", "created_at", "created_by", "updated_at", "updated_by", "scheduled_at", "branch"}). + AddRow(1, 1, false, "nightly", "0 0 * * *", 1, "user1", 1, "user2", nil, "main") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "schedules" WHERE repo_id = $1 ORDER BY id DESC LIMIT 10`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSchedule(context.TODO(), _scheduleOne) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + _, err = _sqlite.CreateSchedule(context.TODO(), _scheduleTwo) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Schedule + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Schedule{_scheduleOne}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Schedule{_scheduleOne}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListSchedulesForRepo(context.TODO(), _repo, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListSchedulesForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListSchedulesForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListSchedulesForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/schedule/list_test.go b/database/schedule/list_test.go new file mode 100644 index 000000000..5b4095724 --- /dev/null +++ b/database/schedule/list_test.go @@ -0,0 +1,113 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSchedule_Engine_ListSchedules(t *testing.T) { + _scheduleOne := testSchedule() + _scheduleOne.SetID(1) + _scheduleOne.SetRepoID(1) + _scheduleOne.SetName("nightly") + _scheduleOne.SetEntry("0 0 * * *") + _scheduleOne.SetCreatedAt(1) + _scheduleOne.SetCreatedBy("user1") + _scheduleOne.SetUpdatedAt(1) + _scheduleOne.SetUpdatedBy("user2") + _scheduleOne.SetBranch("main") + + _scheduleTwo := testSchedule() + _scheduleTwo.SetID(2) + _scheduleTwo.SetRepoID(2) + _scheduleTwo.SetName("hourly") + _scheduleTwo.SetEntry("0 * * * *") + _scheduleTwo.SetCreatedAt(1) + _scheduleTwo.SetCreatedBy("user1") + _scheduleTwo.SetUpdatedAt(1) + _scheduleTwo.SetUpdatedBy("user2") + _scheduleTwo.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "schedules"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "active", "name", "entry", "created_at", "created_by", "updated_at", "updated_by", "scheduled_at", "branch"}). + AddRow(1, 1, false, "nightly", "0 0 * * *", 1, "user1", 1, "user2", nil, "main"). + AddRow(2, 2, false, "hourly", "0 * * * *", 1, "user1", 1, "user2", nil, "main") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "schedules"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSchedule(context.TODO(), _scheduleOne) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + _, err = _sqlite.CreateSchedule(context.TODO(), _scheduleTwo) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Schedule + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Schedule{_scheduleOne, _scheduleTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Schedule{_scheduleOne, _scheduleTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListSchedules(context.TODO()) + + if test.failure { + if err == nil { + t.Errorf("ListSchedules for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListSchedules for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListSchedules for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/schedule/opts.go b/database/schedule/opts.go new file mode 100644 index 000000000..cb2c89cfe --- /dev/null +++ b/database/schedule/opts.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Schedules. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Schedules. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the schedule engine + e.client = client + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Schedules. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the schedule engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Schedules. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the schedule engine + e.config.SkipCreation = skipCreation + + return nil + } +} + +// WithContext sets the context in the database engine for Schedules. +func WithContext(ctx context.Context) EngineOpt { + return func(e *engine) error { + e.ctx = ctx + + return nil + } +} diff --git a/database/schedule/opts_test.go b/database/schedule/opts_test.go new file mode 100644 index 000000000..6159801e3 --- /dev/null +++ b/database/schedule/opts_test.go @@ -0,0 +1,161 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestSchedule_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestSchedule_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestSchedule_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} diff --git a/database/schedule/schedule.go b/database/schedule/schedule.go new file mode 100644 index 000000000..269278f52 --- /dev/null +++ b/database/schedule/schedule.go @@ -0,0 +1,83 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the ScheduleInterface interface. + config struct { + // specifies to skip creating tables and indexes for the Schedule engine + SkipCreation bool + } + + // engine represents the schedule functionality that implements the ScheduleInterface interface. + engine struct { + // engine configuration settings used in schedule functions + config *config + + ctx context.Context + + // gorm.io/gorm database client used in schedule functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in schedule functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with schedules in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new Schedule engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating schedule database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of schedules table and indexes in the database") + + return e, nil + } + + // create the schedules table + err := e.CreateScheduleTable(e.ctx, e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableSchedule, err) + } + + // create the indexes for the schedules table + err = e.CreateScheduleIndexes(e.ctx) + if err != nil { + return nil, fmt.Errorf("unable to create indexes for %s table: %w", constants.TableSchedule, err) + } + + return e, nil +} diff --git a/database/schedule/schedule_test.go b/database/schedule/schedule_test.go new file mode 100644 index 000000000..b5916b3e3 --- /dev/null +++ b/database/schedule/schedule_test.go @@ -0,0 +1,239 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "database/sql/driver" + "reflect" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestSchedule_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + logger: logger, + skipCreation: false, + want: &engine{ + ctx: context.TODO(), + client: _postgres, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + logger: logger, + skipCreation: false, + want: &engine{ + ctx: context.TODO(), + client: _sqlite, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithContext(context.TODO()), + WithClient(test.client), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithContext(context.TODO()), + WithClient(_postgres), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres schedule engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithContext(context.TODO()), + WithClient(_sqlite), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite schedule engine: %v", err) + } + + return _engine +} + +// testSchedule is a test helper function to create an API Schedule type with all fields set to their zero values. +func testSchedule() *library.Schedule { + return &library.Schedule{ + ID: new(int64), + RepoID: new(int64), + Active: new(bool), + Name: new(string), + Entry: new(string), + CreatedAt: new(int64), + CreatedBy: new(string), + UpdatedAt: new(int64), + UpdatedBy: new(string), + ScheduledAt: new(int64), + Branch: new(string), + } +} + +// testRepo is a test helper function to create a library Repo type with all fields set to their zero values. +func testRepo() *library.Repo { + return &library.Repo{ + ID: new(int64), + UserID: new(int64), + BuildLimit: new(int64), + Timeout: new(int64), + Counter: new(int), + PipelineType: new(string), + Hash: new(string), + Org: new(string), + Name: new(string), + FullName: new(string), + Link: new(string), + Clone: new(string), + Branch: new(string), + Visibility: new(string), + PreviousName: new(string), + Private: new(bool), + Trusted: new(bool), + Active: new(bool), + AllowPull: new(bool), + AllowPush: new(bool), + AllowDeploy: new(bool), + AllowTag: new(bool), + AllowComment: new(bool), + } +} + +// This will be used with the github.com/DATA-DOG/go-sqlmock library to compare values +// that are otherwise not easily compared. These typically would be values generated +// before adding or updating them in the database. +// +// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime +type NowTimestamp struct{} + +// Match satisfies sqlmock.Argument interface. +func (t NowTimestamp) Match(v driver.Value) bool { + ts, ok := v.(int64) + if !ok { + return false + } + now := time.Now().Unix() + + return now-ts < 10 +} diff --git a/database/schedule/table.go b/database/schedule/table.go new file mode 100644 index 000000000..9ebccf864 --- /dev/null +++ b/database/schedule/table.go @@ -0,0 +1,70 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres schedules table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +schedules ( + id SERIAL PRIMARY KEY, + repo_id INTEGER, + active BOOLEAN, + name VARCHAR(100), + entry VARCHAR(100), + created_at INTEGER, + created_by VARCHAR(250), + updated_at INTEGER, + updated_by VARCHAR(250), + scheduled_at INTEGER, + branch VARCHAR(250), + UNIQUE(repo_id, name) +); +` + + // CreateSqliteTable represents a query to create the Sqlite schedules table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +schedules ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_id INTEGER, + active BOOLEAN, + name TEXT, + entry TEXT, + created_at INTEGER, + created_by TEXT, + updated_at INTEGER, + updated_by TEXT, + scheduled_at INTEGER, + branch TEXT, + UNIQUE(repo_id, name) +); +` +) + +// CreateScheduleTable creates the schedules table in the database. +func (e *engine) CreateScheduleTable(ctx context.Context, driver string) error { + e.logger.Tracef("creating schedules table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the schedules table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the schedules table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/schedule/table_test.go b/database/schedule/table_test.go new file mode 100644 index 000000000..c0d785ed1 --- /dev/null +++ b/database/schedule/table_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSchedule_Engine_CreateScheduleTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateScheduleTable(context.TODO(), test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateScheduleTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateScheduleTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/schedule/update.go b/database/schedule/update.go new file mode 100644 index 000000000..23ab855ba --- /dev/null +++ b/database/schedule/update.go @@ -0,0 +1,42 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// UpdateSchedule updates an existing schedule in the database. +func (e *engine) UpdateSchedule(ctx context.Context, s *library.Schedule, fields bool) (*library.Schedule, error) { + e.logger.WithFields(logrus.Fields{ + "schedule": s.GetName(), + }).Tracef("updating schedule %s in the database", s.GetName()) + + // cast the library type to database type + schedule := database.ScheduleFromLibrary(s) + + // validate the necessary fields are populated + err := schedule.Validate() + if err != nil { + return nil, err + } + + // If "fields" is true, update entire record; otherwise, just update scheduled_at (part of processSchedule) + // + // we do this because Gorm will automatically set `updated_at` with the Save function + // and the `updated_at` field should reflect the last time a user updated the record, rather than the scheduler + if fields { + err = e.client.Table(constants.TableSchedule).Save(schedule).Error + } else { + err = e.client.Table(constants.TableSchedule).Model(schedule).UpdateColumn("scheduled_at", s.GetScheduledAt()).Error + } + + return schedule.ToLibrary(), err +} diff --git a/database/schedule/update_test.go b/database/schedule/update_test.go new file mode 100644 index 000000000..235ccfc43 --- /dev/null +++ b/database/schedule/update_test.go @@ -0,0 +1,169 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSchedule_Engine_UpdateSchedule_Config(t *testing.T) { + _repo := testRepo() + _repo.SetID(1) + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + + _schedule := testSchedule() + _schedule.SetID(1) + _schedule.SetRepoID(1) + _schedule.SetName("nightly") + _schedule.SetEntry("0 0 * * *") + _schedule.SetCreatedAt(1) + _schedule.SetCreatedBy("user1") + _schedule.SetUpdatedAt(1) + _schedule.SetUpdatedBy("user2") + _schedule.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`UPDATE "schedules" +SET "repo_id"=$1,"active"=$2,"name"=$3,"entry"=$4,"created_at"=$5,"created_by"=$6,"updated_at"=$7,"updated_by"=$8,"scheduled_at"=$9,"branch"=$10 +WHERE "id" = $11`). + WithArgs(1, false, "nightly", "0 0 * * *", 1, "user1", NowTimestamp{}, "user2", nil, "main", 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSchedule(context.TODO(), _schedule) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.UpdateSchedule(context.TODO(), _schedule, true) + _schedule.SetUpdatedAt(got.GetUpdatedAt()) + + if test.failure { + if err == nil { + t.Errorf("UpdateSchedule for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateSchedule for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _schedule) { + t.Errorf("UpdateSchedule for %s returned %s, want %s", test.name, got, _schedule) + } + }) + } +} + +func TestSchedule_Engine_UpdateSchedule_NotConfig(t *testing.T) { + _repo := testRepo() + _repo.SetID(1) + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + + _schedule := testSchedule() + _schedule.SetID(1) + _schedule.SetRepoID(1) + _schedule.SetName("nightly") + _schedule.SetEntry("0 0 * * *") + _schedule.SetCreatedAt(1) + _schedule.SetCreatedBy("user1") + _schedule.SetUpdatedAt(1) + _schedule.SetUpdatedBy("user2") + _schedule.SetScheduledAt(1) + _schedule.SetBranch("main") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`UPDATE "schedules" SET "scheduled_at"=$1 WHERE "id" = $2`). + WithArgs(1, 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSchedule(context.TODO(), _schedule) + if err != nil { + t.Errorf("unable to create test schedule for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.UpdateSchedule(context.TODO(), _schedule, false) + + if test.failure { + if err == nil { + t.Errorf("UpdateSchedule for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateSchedule for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _schedule) { + t.Errorf("CreateSchedule for %s returned %s, want %s", test.name, got, _schedule) + } + }) + } +} diff --git a/database/secret/count.go b/database/secret/count.go new file mode 100644 index 000000000..0926c875c --- /dev/null +++ b/database/secret/count.go @@ -0,0 +1,25 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/constants" +) + +// CountSecrets gets the count of all secrets from the database. +func (e *engine) CountSecrets() (int64, error) { + e.logger.Tracef("getting count of all secrets from the database") + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSecret). + Count(&s). + Error + + return s, err +} diff --git a/database/secret/count_org.go b/database/secret/count_org.go new file mode 100644 index 000000000..751389800 --- /dev/null +++ b/database/secret/count_org.go @@ -0,0 +1,32 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" +) + +// CountSecretsForOrg gets the count of secrets by org name from the database. +func (e *engine) CountSecretsForOrg(org string, filters map[string]interface{}) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": org, + "type": constants.SecretOrg, + }).Tracef("getting count of secrets for org %s from the database", org) + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSecret). + Where("type = ?", constants.SecretOrg). + Where("org = ?", org). + Where(filters). + Count(&s). + Error + + return s, err +} diff --git a/database/secret/count_org_test.go b/database/secret/count_org_test.go new file mode 100644 index 000000000..6ce684c5e --- /dev/null +++ b/database/secret/count_org_test.go @@ -0,0 +1,109 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" +) + +func TestSecret_Engine_CountSecretsForOrg(t *testing.T) { + // setup types + _secretOne := testSecret() + _secretOne.SetID(1) + _secretOne.SetOrg("foo") + _secretOne.SetRepo("*") + _secretOne.SetName("baz") + _secretOne.SetValue("bar") + _secretOne.SetType("org") + _secretOne.SetCreatedAt(1) + _secretOne.SetCreatedBy("user") + _secretOne.SetUpdatedAt(1) + _secretOne.SetUpdatedBy("user2") + + _secretTwo := testSecret() + _secretTwo.SetID(2) + _secretTwo.SetOrg("bar") + _secretTwo.SetRepo("*") + _secretTwo.SetName("foo") + _secretTwo.SetValue("baz") + _secretTwo.SetType("org") + _secretTwo.SetCreatedAt(1) + _secretTwo.SetCreatedBy("user") + _secretTwo.SetUpdatedAt(1) + _secretTwo.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "secrets" WHERE type = $1 AND org = $2`). + WithArgs(constants.SecretOrg, "foo").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretOne) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretTwo) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 1, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 1, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountSecretsForOrg("foo", filters) + + if test.failure { + if err == nil { + t.Errorf("CountSecretsForOrg for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountSecretsForOrg for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountSecretsForOrg for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/count_repo.go b/database/secret/count_repo.go new file mode 100644 index 000000000..58100ebc6 --- /dev/null +++ b/database/secret/count_repo.go @@ -0,0 +1,35 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CountSecretsForRepo gets the count of secrets by org and repo name from the database. +func (e *engine) CountSecretsForRepo(r *library.Repo, filters map[string]interface{}) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + "type": constants.SecretRepo, + }).Tracef("getting count of secrets for repo %s from the database", r.GetFullName()) + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSecret). + Where("type = ?", constants.SecretRepo). + Where("org = ?", r.GetOrg()). + Where("repo = ?", r.GetName()). + Where(filters). + Count(&s). + Error + + return s, err +} diff --git a/database/secret/count_repo_test.go b/database/secret/count_repo_test.go new file mode 100644 index 000000000..8db30f4c5 --- /dev/null +++ b/database/secret/count_repo_test.go @@ -0,0 +1,120 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/go-vela/types/constants" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSecret_Engine_CountSecretsForRepo(t *testing.T) { + // setup types + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + _repo.SetPipelineType("yaml") + + _secretOne := testSecret() + _secretOne.SetID(1) + _secretOne.SetOrg("foo") + _secretOne.SetRepo("bar") + _secretOne.SetName("baz") + _secretOne.SetValue("foob") + _secretOne.SetType("repo") + _secretOne.SetCreatedAt(1) + _secretOne.SetCreatedBy("user") + _secretOne.SetUpdatedAt(1) + _secretOne.SetUpdatedBy("user2") + + _secretTwo := testSecret() + _secretTwo.SetID(2) + _secretTwo.SetOrg("bar") + _secretTwo.SetRepo("foo") + _secretTwo.SetName("foob") + _secretTwo.SetValue("baz") + _secretTwo.SetType("repo") + _secretTwo.SetCreatedAt(1) + _secretTwo.SetCreatedBy("user") + _secretTwo.SetUpdatedAt(1) + _secretTwo.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "secrets" WHERE type = $1 AND org = $2 AND repo = $3`). + WithArgs(constants.SecretRepo, "foo", "bar").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretOne) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretTwo) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 1, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 1, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountSecretsForRepo(_repo, filters) + + if test.failure { + if err == nil { + t.Errorf("CountSecretsForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountSecretsForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountSecretsForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/count_team.go b/database/secret/count_team.go new file mode 100644 index 000000000..30c7a4255 --- /dev/null +++ b/database/secret/count_team.go @@ -0,0 +1,68 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "strings" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" +) + +// CountSecretsForTeam gets the count of secrets by org and team name from the database. +func (e *engine) CountSecretsForTeam(org, team string, filters map[string]interface{}) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": org, + "team": team, + "type": constants.SecretShared, + }).Tracef("getting count of secrets for team %s/%s from the database", org, team) + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSecret). + Where("type = ?", constants.SecretShared). + Where("org = ?", org). + Where("team = ?", team). + Where(filters). + Count(&s). + Error + + return s, err +} + +// CountSecretsForTeams gets the count of secrets by teams within an org from the database. +func (e *engine) CountSecretsForTeams(org string, teams []string, filters map[string]interface{}) (int64, error) { + // lower case team names for not case-sensitive values from the SCM i.e. GitHub + // + // iterate through the list of teams provided + for index, team := range teams { + // ensure the team name is lower case + teams[index] = strings.ToLower(team) + } + + e.logger.WithFields(logrus.Fields{ + "org": org, + "teams": teams, + "type": constants.SecretShared, + }).Tracef("getting count of secrets for teams %s in org %s from the database", teams, org) + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSecret). + Where("type = ?", constants.SecretShared). + Where("org = ?", org). + Where("LOWER(team) IN (?)", teams). + Where(filters). + Count(&s). + Error + + return s, err +} diff --git a/database/secret/count_team_test.go b/database/secret/count_team_test.go new file mode 100644 index 000000000..50bad4ae4 --- /dev/null +++ b/database/secret/count_team_test.go @@ -0,0 +1,216 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/go-vela/types/constants" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSecret_Engine_CountSecretsForTeam(t *testing.T) { + // setup types + _secretOne := testSecret() + _secretOne.SetID(1) + _secretOne.SetOrg("foo") + _secretOne.SetTeam("bar") + _secretOne.SetName("baz") + _secretOne.SetValue("foob") + _secretOne.SetType("shared") + _secretOne.SetCreatedAt(1) + _secretOne.SetCreatedBy("user") + _secretOne.SetUpdatedAt(1) + _secretOne.SetUpdatedBy("user2") + + _secretTwo := testSecret() + _secretTwo.SetID(2) + _secretTwo.SetOrg("bar") + _secretTwo.SetTeam("foo") + _secretTwo.SetName("foob") + _secretTwo.SetValue("baz") + _secretTwo.SetType("shared") + _secretTwo.SetCreatedAt(1) + _secretTwo.SetCreatedBy("user") + _secretTwo.SetUpdatedAt(1) + _secretTwo.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "secrets" WHERE type = $1 AND org = $2 AND team = $3`). + WithArgs(constants.SecretShared, "foo", "bar").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretOne) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretTwo) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 1, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 1, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountSecretsForTeam("foo", "bar", filters) + + if test.failure { + if err == nil { + t.Errorf("CountSecretsForTeam for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountSecretsForTeam for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountSecretsForTeam for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +func TestSecret_Engine_CountSecretsForTeams(t *testing.T) { + // setup types + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + _repo.SetPipelineType("yaml") + + _secretOne := testSecret() + _secretOne.SetID(1) + _secretOne.SetOrg("foo") + _secretOne.SetTeam("bar") + _secretOne.SetName("baz") + _secretOne.SetValue("foob") + _secretOne.SetType("shared") + _secretOne.SetCreatedAt(1) + _secretOne.SetCreatedBy("user") + _secretOne.SetUpdatedAt(1) + _secretOne.SetUpdatedBy("user2") + + _secretTwo := testSecret() + _secretTwo.SetID(2) + _secretTwo.SetOrg("bar") + _secretTwo.SetTeam("foo") + _secretTwo.SetName("foob") + _secretTwo.SetValue("baz") + _secretTwo.SetType("shared") + _secretTwo.SetCreatedAt(1) + _secretTwo.SetCreatedBy("user") + _secretTwo.SetUpdatedAt(1) + _secretTwo.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "secrets" WHERE type = $1 AND org = $2 AND LOWER(team) IN ($3,$4)`). + WithArgs(constants.SecretShared, "foo", "foo", "bar").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretOne) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretTwo) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 1, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 1, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountSecretsForTeams("foo", []string{"foo", "bar"}, filters) + + if test.failure { + if err == nil { + t.Errorf("CountSecretsForTeams for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountSecretsForTeams for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountSecretsForTeams for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/count_test.go b/database/secret/count_test.go new file mode 100644 index 000000000..d3b3f3c48 --- /dev/null +++ b/database/secret/count_test.go @@ -0,0 +1,105 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSecret_Engine_CountSecrets(t *testing.T) { + // setup types + _secretOne := testSecret() + _secretOne.SetID(1) + _secretOne.SetOrg("foo") + _secretOne.SetRepo("bar") + _secretOne.SetName("baz") + _secretOne.SetValue("foob") + _secretOne.SetType("repo") + _secretOne.SetCreatedAt(1) + _secretOne.SetCreatedBy("user") + _secretOne.SetUpdatedAt(1) + _secretOne.SetUpdatedBy("user2") + + _secretTwo := testSecret() + _secretTwo.SetID(2) + _secretTwo.SetOrg("bar") + _secretTwo.SetRepo("foo") + _secretTwo.SetName("foob") + _secretTwo.SetValue("baz") + _secretTwo.SetType("repo") + _secretTwo.SetCreatedAt(1) + _secretTwo.SetCreatedBy("user") + _secretTwo.SetUpdatedAt(1) + _secretTwo.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "secrets"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretOne) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretTwo) + if err != nil { + t.Errorf("unable to create test repo for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountSecrets() + + if test.failure { + if err == nil { + t.Errorf("CountSecrets for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountSecrets for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountSecrets for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/create.go b/database/secret/create.go new file mode 100644 index 000000000..734281cf1 --- /dev/null +++ b/database/secret/create.go @@ -0,0 +1,82 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with update.go +package secret + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreateSecret creates a new secret in the database. +func (e *engine) CreateSecret(s *library.Secret) (*library.Secret, error) { + // handle the secret based off the type + switch s.GetType() { + case constants.SecretShared: + e.logger.WithFields(logrus.Fields{ + "org": s.GetOrg(), + "team": s.GetTeam(), + "secret": s.GetName(), + "type": s.GetType(), + }).Tracef("creating secret %s/%s/%s/%s in the database", s.GetType(), s.GetOrg(), s.GetTeam(), s.GetName()) + default: + e.logger.WithFields(logrus.Fields{ + "org": s.GetOrg(), + "repo": s.GetRepo(), + "secret": s.GetName(), + "type": s.GetType(), + }).Tracef("creating secret %s/%s/%s/%s in the database", s.GetType(), s.GetOrg(), s.GetRepo(), s.GetName()) + } + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#SecretFromLibrary + secret := database.SecretFromLibrary(s) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Validate + err := secret.Validate() + if err != nil { + return nil, err + } + + // encrypt the fields for the secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Encrypt + err = secret.Encrypt(e.config.EncryptionKey) + if err != nil { + switch s.GetType() { + case constants.SecretShared: + return nil, fmt.Errorf("unable to encrypt secret %s/%s/%s/%s: %w", s.GetType(), s.GetOrg(), s.GetTeam(), s.GetName(), err) + default: + return nil, fmt.Errorf("unable to encrypt secret %s/%s/%s/%s: %w", s.GetType(), s.GetOrg(), s.GetRepo(), s.GetName(), err) + } + } + + // create secret record + result := e.client.Table(constants.TableSecret).Create(secret.Nullify()) + + if result.Error != nil { + return nil, result.Error + } + + // decrypt the fields for the secret to return + err = secret.Decrypt(e.config.EncryptionKey) + if err != nil { + switch s.GetType() { + case constants.SecretShared: + return nil, fmt.Errorf("unable to decrypt secret %s/%s/%s/%s: %w", s.GetType(), s.GetOrg(), s.GetTeam(), s.GetName(), err) + default: + return nil, fmt.Errorf("unable to decrypt secret %s/%s/%s/%s: %w", s.GetType(), s.GetOrg(), s.GetRepo(), s.GetName(), err) + } + } + + return secret.ToLibrary(), nil +} diff --git a/database/secret/create_test.go b/database/secret/create_test.go new file mode 100644 index 000000000..ec4fa38e3 --- /dev/null +++ b/database/secret/create_test.go @@ -0,0 +1,150 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSecret_Engine_CreateSecret(t *testing.T) { + // setup types + _secretRepo := testSecret() + _secretRepo.SetID(1) + _secretRepo.SetOrg("foo") + _secretRepo.SetRepo("bar") + _secretRepo.SetName("baz") + _secretRepo.SetValue("foob") + _secretRepo.SetType("repo") + _secretRepo.SetCreatedAt(1) + _secretRepo.SetCreatedBy("user") + _secretRepo.SetUpdatedAt(1) + _secretRepo.SetUpdatedBy("user2") + + _secretOrg := testSecret() + _secretOrg.SetID(2) + _secretOrg.SetOrg("foo") + _secretOrg.SetRepo("*") + _secretOrg.SetName("bar") + _secretOrg.SetValue("baz") + _secretOrg.SetType("org") + _secretOrg.SetCreatedAt(1) + _secretOrg.SetCreatedBy("user") + _secretOrg.SetUpdatedAt(1) + _secretOrg.SetUpdatedBy("user2") + + _secretShared := testSecret() + _secretShared.SetID(3) + _secretShared.SetOrg("foo") + _secretShared.SetTeam("bar") + _secretShared.SetName("baz") + _secretShared.SetValue("foob") + _secretShared.SetType("shared") + _secretShared.SetCreatedAt(1) + _secretShared.SetCreatedBy("user") + _secretShared.SetUpdatedAt(1) + _secretShared.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the repo secrets query + _mock.ExpectQuery(`INSERT INTO "secrets" +("org","repo","team","name","value","type","images","events","allow_command","created_at","created_by","updated_at","updated_by","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING "id"`). + WithArgs("foo", "bar", nil, "baz", AnyArgument{}, "repo", nil, nil, false, 1, "user", 1, "user2", 1). + WillReturnRows(_rows) + + // ensure the mock expects the org secrets query + _mock.ExpectQuery(`INSERT INTO "secrets" +("org","repo","team","name","value","type","images","events","allow_command","created_at","created_by","updated_at","updated_by","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING "id"`). + WithArgs("foo", "*", nil, "bar", AnyArgument{}, "org", nil, nil, false, 1, "user", 1, "user2", 2). + WillReturnRows(_rows) + + // ensure the mock expects the shared secrets query + _mock.ExpectQuery(`INSERT INTO "secrets" +("org","repo","team","name","value","type","images","events","allow_command","created_at","created_by","updated_at","updated_by","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING "id"`). + WithArgs("foo", nil, "bar", "baz", AnyArgument{}, "shared", nil, nil, false, 1, "user", 1, "user2", 3). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + secret *library.Secret + }{ + { + failure: false, + name: "postgres with repo", + database: _postgres, + secret: _secretRepo, + }, + { + failure: false, + name: "postgres with org", + database: _postgres, + secret: _secretOrg, + }, + { + failure: false, + name: "postgres with shared", + database: _postgres, + secret: _secretShared, + }, + { + failure: false, + name: "sqlite3 with repo", + database: _sqlite, + secret: _secretRepo, + }, + { + failure: false, + name: "sqlite3 with org", + database: _sqlite, + secret: _secretOrg, + }, + { + failure: false, + name: "sqlite3 with shared", + database: _sqlite, + secret: _secretShared, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CreateSecret(test.secret) + + if test.failure { + if err == nil { + t.Errorf("CreateSecret for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateSecret for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.secret) { + t.Errorf("CreateSecret is %s, want %s", got, test.secret) + } + }) + } +} diff --git a/database/secret/delete.go b/database/secret/delete.go new file mode 100644 index 000000000..a9207bbd9 --- /dev/null +++ b/database/secret/delete.go @@ -0,0 +1,46 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// DeleteSecret deletes an existing secret from the database. +func (e *engine) DeleteSecret(s *library.Secret) error { + // handle the secret based off the type + // + //nolint:dupl // ignore similar code with update.go + switch s.GetType() { + case constants.SecretShared: + e.logger.WithFields(logrus.Fields{ + "org": s.GetOrg(), + "team": s.GetTeam(), + "secret": s.GetName(), + "type": s.GetType(), + }).Tracef("deleting secret %s/%s/%s/%s from the database", s.GetType(), s.GetOrg(), s.GetTeam(), s.GetName()) + default: + e.logger.WithFields(logrus.Fields{ + "org": s.GetOrg(), + "repo": s.GetRepo(), + "secret": s.GetName(), + "type": s.GetType(), + }).Tracef("deleting secret %s/%s/%s/%s from the database", s.GetType(), s.GetOrg(), s.GetRepo(), s.GetName()) + } + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#SecretFromLibrary + secret := database.SecretFromLibrary(s) + + // send query to the database + return e.client. + Table(constants.TableSecret). + Delete(secret). + Error +} diff --git a/database/secret/delete_test.go b/database/secret/delete_test.go new file mode 100644 index 000000000..46def2c55 --- /dev/null +++ b/database/secret/delete_test.go @@ -0,0 +1,151 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSecret_Engine_DeleteSecret(t *testing.T) { + // setup types + _secretRepo := testSecret() + _secretRepo.SetID(1) + _secretRepo.SetOrg("foo") + _secretRepo.SetRepo("bar") + _secretRepo.SetName("baz") + _secretRepo.SetValue("foob") + _secretRepo.SetType("repo") + _secretRepo.SetCreatedAt(1) + _secretRepo.SetCreatedBy("user") + _secretRepo.SetUpdatedAt(1) + _secretRepo.SetUpdatedBy("user2") + + _secretOrg := testSecret() + _secretOrg.SetID(2) + _secretOrg.SetOrg("foo") + _secretOrg.SetRepo("*") + _secretOrg.SetName("bar") + _secretOrg.SetValue("baz") + _secretOrg.SetType("org") + _secretOrg.SetCreatedAt(1) + _secretOrg.SetCreatedBy("user") + _secretOrg.SetUpdatedAt(1) + _secretOrg.SetUpdatedBy("user2") + + _secretShared := testSecret() + _secretShared.SetID(3) + _secretShared.SetOrg("foo") + _secretShared.SetTeam("bar") + _secretShared.SetName("baz") + _secretShared.SetValue("foob") + _secretShared.SetType("shared") + _secretShared.SetCreatedAt(1) + _secretShared.SetCreatedBy("user") + _secretShared.SetUpdatedAt(1) + _secretShared.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the repo query + _mock.ExpectExec(`DELETE FROM "secrets" WHERE "secrets"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // ensure the mock expects the org query + _mock.ExpectExec(`DELETE FROM "secrets" WHERE "secrets"."id" = $1`). + WithArgs(2). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // ensure the mock expects the shared query + _mock.ExpectExec(`DELETE FROM "secrets" WHERE "secrets"."id" = $1`). + WithArgs(3). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretRepo) + if err != nil { + t.Errorf("unable to create test repo secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretOrg) + if err != nil { + t.Errorf("unable to create test org secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretShared) + if err != nil { + t.Errorf("unable to create test shared secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + secret *library.Secret + }{ + { + failure: false, + name: "postgres with repo", + database: _postgres, + secret: _secretRepo, + }, + { + failure: false, + name: "postgres with org", + database: _postgres, + secret: _secretOrg, + }, + { + failure: false, + name: "postgres with shared", + database: _postgres, + secret: _secretShared, + }, + { + failure: false, + name: "sqlite3 with repo", + database: _sqlite, + secret: _secretRepo, + }, + { + failure: false, + name: "sqlite3 with org", + database: _sqlite, + secret: _secretOrg, + }, + { + failure: false, + name: "sqlite3 with shared", + database: _sqlite, + secret: _secretShared, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeleteSecret(test.secret) + + if test.failure { + if err == nil { + t.Errorf("DeleteSecret for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeleteSecret for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/secret/get.go b/database/secret/get.go new file mode 100644 index 000000000..67579c000 --- /dev/null +++ b/database/secret/get.go @@ -0,0 +1,52 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetSecret gets a secret by ID from the database. +func (e *engine) GetSecret(id int64) (*library.Secret, error) { + e.logger.Tracef("getting secret %d from the database", id) + + // variable to store query results + s := new(database.Secret) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSecret). + Where("id = ?", id). + Take(s). + Error + if err != nil { + return nil, err + } + + // decrypt the fields for the secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt + err = s.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted secrets + e.logger.Errorf("unable to decrypt secret %d: %v", id, err) + + // return the unencrypted secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + return s.ToLibrary(), nil + } + + // return the decrypted secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + return s.ToLibrary(), nil +} diff --git a/database/secret/get_org.go b/database/secret/get_org.go new file mode 100644 index 000000000..f7f3e9adc --- /dev/null +++ b/database/secret/get_org.go @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetSecretForOrg gets a secret by org name from the database. +func (e *engine) GetSecretForOrg(org, name string) (*library.Secret, error) { + e.logger.WithFields(logrus.Fields{ + "org": org, + "secret": name, + "type": constants.SecretOrg, + }).Tracef("getting org secret %s/%s from the database", org, name) + + // variable to store query results + s := new(database.Secret) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSecret). + Where("type = ?", constants.SecretOrg). + Where("org = ?", org). + Where("name = ?", name). + Take(s). + Error + if err != nil { + return nil, err + } + + // decrypt the fields for the secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt + err = s.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted secrets + e.logger.Errorf("unable to decrypt org secret %s/%s: %v", org, name, err) + + // return the unencrypted secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + return s.ToLibrary(), nil + } + + // return the decrypted secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + return s.ToLibrary(), nil +} diff --git a/database/secret/get_org_test.go b/database/secret/get_org_test.go new file mode 100644 index 000000000..7747838b8 --- /dev/null +++ b/database/secret/get_org_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" +) + +func TestSecret_Engine_GetSecretForOrg(t *testing.T) { + // setup types + _secret := testSecret() + _secret.SetID(1) + _secret.SetOrg("foo") + _secret.SetRepo("*") + _secret.SetName("baz") + _secret.SetValue("bar") + _secret.SetType("org") + _secret.SetCreatedAt(1) + _secret.SetCreatedBy("user") + _secret.SetUpdatedAt(1) + _secret.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}). + AddRow(1, "org", "foo", "*", "", "baz", "bar", nil, nil, false, 1, "user", 1, "user2") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "secrets" WHERE type = $1 AND org = $2 AND name = $3 LIMIT 1`). + WithArgs(constants.SecretOrg, "foo", "baz").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secret) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Secret + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _secret, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _secret, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetSecretForOrg("foo", "baz") + + if test.failure { + if err == nil { + t.Errorf("GetSecretForOrg for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetSecretForOrg for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetSecretForOrg for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/get_repo.go b/database/secret/get_repo.go new file mode 100644 index 000000000..6d7ca9dd9 --- /dev/null +++ b/database/secret/get_repo.go @@ -0,0 +1,61 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetSecretForRepo gets a secret by org and repo name from the database. +func (e *engine) GetSecretForRepo(name string, r *library.Repo) (*library.Secret, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + "secret": name, + "type": constants.SecretRepo, + }).Tracef("getting repo secret %s/%s from the database", r.GetFullName(), name) + + // variable to store query results + s := new(database.Secret) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSecret). + Where("type = ?", constants.SecretRepo). + Where("org = ?", r.GetOrg()). + Where("repo = ?", r.GetName()). + Where("name = ?", name). + Take(s). + Error + if err != nil { + return nil, err + } + + // decrypt the fields for the secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt + err = s.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted secrets + e.logger.Errorf("unable to decrypt repo secret %s/%s: %v", r.GetFullName(), name, err) + + // return the unencrypted secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + return s.ToLibrary(), nil + } + + // return the decrypted secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + return s.ToLibrary(), nil +} diff --git a/database/secret/get_repo_test.go b/database/secret/get_repo_test.go new file mode 100644 index 000000000..05d872373 --- /dev/null +++ b/database/secret/get_repo_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" +) + +func TestSecret_Engine_GetSecretForRepo(t *testing.T) { + // setup types + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + _repo.SetPipelineType("yaml") + + _secret := testSecret() + _secret.SetID(1) + _secret.SetOrg("foo") + _secret.SetRepo("bar") + _secret.SetName("baz") + _secret.SetValue("foob") + _secret.SetType("repo") + _secret.SetCreatedAt(1) + _secret.SetCreatedBy("user") + _secret.SetUpdatedAt(1) + _secret.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}). + AddRow(1, "repo", "foo", "bar", "", "baz", "foob", nil, nil, false, 1, "user", 1, "user2") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "secrets" WHERE type = $1 AND org = $2 AND repo = $3 AND name = $4 LIMIT 1`). + WithArgs(constants.SecretRepo, "foo", "bar", "baz").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secret) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Secret + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _secret, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _secret, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetSecretForRepo("baz", _repo) + + if test.failure { + if err == nil { + t.Errorf("GetSecretForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetSecretForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetSecretForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/get_team.go b/database/secret/get_team.go new file mode 100644 index 000000000..306923ee1 --- /dev/null +++ b/database/secret/get_team.go @@ -0,0 +1,61 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetSecretForTeam gets a secret by org and team name from the database. +func (e *engine) GetSecretForTeam(org, team, name string) (*library.Secret, error) { + e.logger.WithFields(logrus.Fields{ + "org": org, + "team": team, + "secret": name, + "type": constants.SecretShared, + }).Tracef("getting shared secret %s/%s/%s from the database", org, team, name) + + // variable to store query results + s := new(database.Secret) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableSecret). + Where("type = ?", constants.SecretShared). + Where("org = ?", org). + Where("team = ?", team). + Where("name = ?", name). + Take(s). + Error + if err != nil { + return nil, err + } + + // decrypt the fields for the secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt + err = s.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted secrets + e.logger.Errorf("unable to decrypt shared secret %s/%s/%s: %v", org, team, name, err) + + // return the unencrypted secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + return s.ToLibrary(), nil + } + + // return the decrypted secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + return s.ToLibrary(), nil +} diff --git a/database/secret/get_team_test.go b/database/secret/get_team_test.go new file mode 100644 index 000000000..217415189 --- /dev/null +++ b/database/secret/get_team_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" +) + +func TestSecret_Engine_GetSecretForTeam(t *testing.T) { + // setup types + _secret := testSecret() + _secret.SetID(1) + _secret.SetOrg("foo") + _secret.SetTeam("bar") + _secret.SetName("baz") + _secret.SetValue("foob") + _secret.SetType("shared") + _secret.SetCreatedAt(1) + _secret.SetCreatedBy("user") + _secret.SetUpdatedAt(1) + _secret.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}). + AddRow(1, "shared", "foo", "", "bar", "baz", "foob", nil, nil, false, 1, "user", 1, "user2") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "secrets" WHERE type = $1 AND org = $2 AND team = $3 AND name = $4 LIMIT 1`). + WithArgs(constants.SecretShared, "foo", "bar", "baz").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secret) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Secret + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _secret, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _secret, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetSecretForTeam("foo", "bar", "baz") + + if test.failure { + if err == nil { + t.Errorf("GetSecretForTeam for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetSecretForTeam for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetSecretForTeam for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/get_test.go b/database/secret/get_test.go new file mode 100644 index 000000000..e19e89291 --- /dev/null +++ b/database/secret/get_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSecret_Engine_GetSecret(t *testing.T) { + // setup types + _secret := testSecret() + _secret.SetID(1) + _secret.SetOrg("foo") + _secret.SetRepo("bar") + _secret.SetName("baz") + _secret.SetValue("foob") + _secret.SetType("repo") + _secret.SetCreatedAt(1) + _secret.SetCreatedBy("user") + _secret.SetUpdatedAt(1) + _secret.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}). + AddRow(1, "repo", "foo", "bar", "", "baz", "foob", nil, nil, false, 1, "user", 1, "user2") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "secrets" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secret) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Secret + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _secret, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _secret, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetSecret(1) + + if test.failure { + if err == nil { + t.Errorf("GetSecret for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetSecret for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetSecret for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/index.go b/database/secret/index.go new file mode 100644 index 000000000..7b6a2047a --- /dev/null +++ b/database/secret/index.go @@ -0,0 +1,52 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +const ( + // CreateTypeOrgRepo represents a query to create an + // index on the secrets table for the type, org and repo columns. + CreateTypeOrgRepo = ` +CREATE INDEX +IF NOT EXISTS +secrets_type_org_repo +ON secrets (type, org, repo); +` + // CreateTypeOrgTeam represents a query to create an + // index on the secrets table for the type, org and team columns. + CreateTypeOrgTeam = ` +CREATE INDEX +IF NOT EXISTS +secrets_type_org_team +ON secrets (type, org, team); +` + // CreateTypeOrg represents a query to create an + // index on the secrets table for the type, and org columns. + CreateTypeOrg = ` +CREATE INDEX +IF NOT EXISTS +secrets_type_org +ON secrets (type, org); +` +) + +// CreateSecretIndexes creates the indexes for the secrets table in the database. +func (e *engine) CreateSecretIndexes() error { + e.logger.Tracef("creating indexes for secrets table in the database") + + // create the type, org and repo columns index for the secrets table + err := e.client.Exec(CreateTypeOrgRepo).Error + if err != nil { + return err + } + + // create the type, org and team columns index for the secrets table + err = e.client.Exec(CreateTypeOrgTeam).Error + if err != nil { + return err + } + + // create the type and org columns index for the secrets table + return e.client.Exec(CreateTypeOrg).Error +} diff --git a/database/secret/index_test.go b/database/secret/index_test.go new file mode 100644 index 000000000..67a64d09d --- /dev/null +++ b/database/secret/index_test.go @@ -0,0 +1,61 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSecret_Engine_CreateSecretIndexes(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreateTypeOrgRepo).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateTypeOrgTeam).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateTypeOrg).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateSecretIndexes() + + if test.failure { + if err == nil { + t.Errorf("CreateSecretIndexes for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateSecretIndexes for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/secret/interface.go b/database/secret/interface.go new file mode 100644 index 000000000..dfb134d9b --- /dev/null +++ b/database/secret/interface.go @@ -0,0 +1,63 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/library" +) + +// SecretInterface represents the Vela interface for secret +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type SecretInterface interface { + // Secret Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreateSecretIndexes defines a function that creates the indexes for the secrets table. + CreateSecretIndexes() error + // CreateSecretTable defines a function that creates the secrets table. + CreateSecretTable(string) error + + // Secret Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CountSecrets defines a function that gets the count of all secrets. + CountSecrets() (int64, error) + // CountSecretsForOrg defines a function that gets the count of secrets by org name. + CountSecretsForOrg(string, map[string]interface{}) (int64, error) + // CountSecretsForRepo defines a function that gets the count of secrets by org and repo name. + CountSecretsForRepo(*library.Repo, map[string]interface{}) (int64, error) + // CountSecretsForTeam defines a function that gets the count of secrets by org and team name. + CountSecretsForTeam(string, string, map[string]interface{}) (int64, error) + // CountSecretsForTeams defines a function that gets the count of secrets by teams within an org. + CountSecretsForTeams(string, []string, map[string]interface{}) (int64, error) + // CreateSecret defines a function that creates a new secret. + CreateSecret(*library.Secret) (*library.Secret, error) + // DeleteSecret defines a function that deletes an existing secret. + DeleteSecret(*library.Secret) error + // GetSecret defines a function that gets a secret by ID. + GetSecret(int64) (*library.Secret, error) + // GetSecretForOrg defines a function that gets a secret by org name. + GetSecretForOrg(string, string) (*library.Secret, error) + // GetSecretForRepo defines a function that gets a secret by org and repo name. + GetSecretForRepo(string, *library.Repo) (*library.Secret, error) + // GetSecretForTeam defines a function that gets a secret by org and team name. + GetSecretForTeam(string, string, string) (*library.Secret, error) + // ListSecrets defines a function that gets a list of all secrets. + ListSecrets() ([]*library.Secret, error) + // ListSecretsForOrg defines a function that gets a list of secrets by org name. + ListSecretsForOrg(string, map[string]interface{}, int, int) ([]*library.Secret, int64, error) + // ListSecretsForRepo defines a function that gets a list of secrets by org and repo name. + ListSecretsForRepo(*library.Repo, map[string]interface{}, int, int) ([]*library.Secret, int64, error) + // ListSecretsForTeam defines a function that gets a list of secrets by org and team name. + ListSecretsForTeam(string, string, map[string]interface{}, int, int) ([]*library.Secret, int64, error) + // ListSecretsForTeams defines a function that gets a list of secrets by teams within an org. + ListSecretsForTeams(string, []string, map[string]interface{}, int, int) ([]*library.Secret, int64, error) + // UpdateSecret defines a function that updates an existing secret. + UpdateSecret(*library.Secret) (*library.Secret, error) +} diff --git a/database/secret/list.go b/database/secret/list.go new file mode 100644 index 000000000..fd01dc295 --- /dev/null +++ b/database/secret/list.go @@ -0,0 +1,67 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListSecrets gets a list of all secrets from the database. +func (e *engine) ListSecrets() ([]*library.Secret, error) { + e.logger.Trace("listing all secrets from the database") + + // variables to store query results and return value + count := int64(0) + s := new([]database.Secret) + secrets := []*library.Secret{} + + // count the results + count, err := e.CountSecrets() + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return secrets, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableSecret). + Find(&s). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, secret := range *s { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := secret + + // decrypt the fields for the secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt + err = tmp.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted secrets + e.logger.Errorf("unable to decrypt secret %d: %v", tmp.ID.Int64, err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + secrets = append(secrets, tmp.ToLibrary()) + } + + return secrets, nil +} diff --git a/database/secret/list_org.go b/database/secret/list_org.go new file mode 100644 index 000000000..27078a0f3 --- /dev/null +++ b/database/secret/list_org.go @@ -0,0 +1,82 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListSecretsForOrg gets a list of secrets by org name from the database. +// +//nolint:lll // ignore long line length due to variable names +func (e *engine) ListSecretsForOrg(org string, filters map[string]interface{}, page, perPage int) ([]*library.Secret, int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": org, + "type": constants.SecretOrg, + }).Tracef("listing secrets for org %s from the database", org) + + // variables to store query results and return values + count := int64(0) + s := new([]database.Secret) + secrets := []*library.Secret{} + + // count the results + count, err := e.CountSecretsForOrg(org, filters) + if err != nil { + return secrets, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return secrets, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableSecret). + Where("type = ?", constants.SecretOrg). + Where("org = ?", org). + Where(filters). + Order("id DESC"). + Limit(perPage). + Offset(offset). + Find(&s). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, secret := range *s { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := secret + + // decrypt the fields for the secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt + err = tmp.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted secrets + e.logger.Errorf("unable to decrypt secret %d: %v", tmp.ID.Int64, err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + secrets = append(secrets, tmp.ToLibrary()) + } + + return secrets, count, nil +} diff --git a/database/secret/list_org_test.go b/database/secret/list_org_test.go new file mode 100644 index 000000000..23acd7ead --- /dev/null +++ b/database/secret/list_org_test.go @@ -0,0 +1,120 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" +) + +func TestSecret_Engine_ListSecretsForOrg(t *testing.T) { + // setup types + _secretOne := testSecret() + _secretOne.SetID(1) + _secretOne.SetOrg("foo") + _secretOne.SetRepo("*") + _secretOne.SetName("baz") + _secretOne.SetValue("bar") + _secretOne.SetType("org") + _secretOne.SetCreatedAt(1) + _secretOne.SetCreatedBy("user") + _secretOne.SetUpdatedAt(1) + _secretOne.SetUpdatedBy("user2") + + _secretTwo := testSecret() + _secretTwo.SetID(2) + _secretTwo.SetOrg("foo") + _secretTwo.SetRepo("*") + _secretTwo.SetName("bar") + _secretTwo.SetValue("baz") + _secretTwo.SetType("org") + _secretTwo.SetCreatedAt(1) + _secretTwo.SetCreatedBy("user") + _secretTwo.SetUpdatedAt(1) + _secretTwo.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected name count query result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the name count query + _mock.ExpectQuery(`SELECT count(*) FROM "secrets" WHERE type = $1 AND org = $2`). + WithArgs(constants.SecretOrg, "foo").WillReturnRows(_rows) + + // create expected name query result in mock + _rows = sqlmock.NewRows( + []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}). + AddRow(2, "org", "foo", "*", "", "bar", "baz", nil, nil, false, 1, "user", 1, "user2"). + AddRow(1, "org", "foo", "*", "", "baz", "bar", nil, nil, false, 1, "user", 1, "user2") + + // ensure the mock expects the name query + _mock.ExpectQuery(`SELECT * FROM "secrets" WHERE type = $1 AND org = $2 ORDER BY id DESC LIMIT 10`). + WithArgs(constants.SecretOrg, "foo").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretOne) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretTwo) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Secret + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Secret{_secretTwo, _secretOne}, + }, + { + failure: false, + name: "sqlite", + database: _sqlite, + want: []*library.Secret{_secretTwo, _secretOne}, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListSecretsForOrg("foo", filters, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListSecretsForOrg for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListSecretsForOrg for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListSecretsForOrg for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/list_repo.go b/database/secret/list_repo.go new file mode 100644 index 000000000..c89ba1500 --- /dev/null +++ b/database/secret/list_repo.go @@ -0,0 +1,84 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListSecretsForRepo gets a list of secrets by org name from the database. +// +//nolint:lll // ignore long line length due to variable names +func (e *engine) ListSecretsForRepo(r *library.Repo, filters map[string]interface{}, page, perPage int) ([]*library.Secret, int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + "type": constants.SecretRepo, + }).Tracef("listing secrets for repo %s from the database", r.GetFullName()) + + // variables to store query results and return values + count := int64(0) + s := new([]database.Secret) + secrets := []*library.Secret{} + + // count the results + count, err := e.CountSecretsForRepo(r, filters) + if err != nil { + return secrets, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return secrets, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableSecret). + Where("type = ?", constants.SecretRepo). + Where("org = ?", r.GetOrg()). + Where("repo = ?", r.GetName()). + Where(filters). + Order("id DESC"). + Limit(perPage). + Offset(offset). + Find(&s). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, secret := range *s { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := secret + + // decrypt the fields for the secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt + err = tmp.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted secrets + e.logger.Errorf("unable to decrypt secret %d: %v", tmp.ID.Int64, err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + secrets = append(secrets, tmp.ToLibrary()) + } + + return secrets, count, nil +} diff --git a/database/secret/list_repo_test.go b/database/secret/list_repo_test.go new file mode 100644 index 000000000..db26d2543 --- /dev/null +++ b/database/secret/list_repo_test.go @@ -0,0 +1,131 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/go-vela/types/constants" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSecret_Engine_ListSecretsForRepo(t *testing.T) { + // setup types + _repo := testRepo() + _repo.SetID(1) + _repo.SetUserID(1) + _repo.SetHash("baz") + _repo.SetOrg("foo") + _repo.SetName("bar") + _repo.SetFullName("foo/bar") + _repo.SetVisibility("public") + _repo.SetPipelineType("yaml") + + _secretOne := testSecret() + _secretOne.SetID(1) + _secretOne.SetOrg("foo") + _secretOne.SetRepo("bar") + _secretOne.SetName("baz") + _secretOne.SetValue("foob") + _secretOne.SetType("repo") + _secretOne.SetCreatedAt(1) + _secretOne.SetCreatedBy("user") + _secretOne.SetUpdatedAt(1) + _secretOne.SetUpdatedBy("user2") + + _secretTwo := testSecret() + _secretTwo.SetID(2) + _secretTwo.SetOrg("foo") + _secretTwo.SetRepo("bar") + _secretTwo.SetName("foob") + _secretTwo.SetValue("baz") + _secretTwo.SetType("repo") + _secretTwo.SetCreatedAt(1) + _secretTwo.SetCreatedBy("user") + _secretTwo.SetUpdatedAt(1) + _secretTwo.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected name count query result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the name count query + _mock.ExpectQuery(`SELECT count(*) FROM "secrets" WHERE type = $1 AND org = $2 AND repo = $3`). + WithArgs(constants.SecretRepo, "foo", "bar").WillReturnRows(_rows) + + // create expected name query result in mock + _rows = sqlmock.NewRows( + []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}). + AddRow(2, "repo", "foo", "bar", "", "foob", "baz", nil, nil, false, 1, "user", 1, "user2"). + AddRow(1, "repo", "foo", "bar", "", "baz", "foob", nil, nil, false, 1, "user", 1, "user2") + + // ensure the mock expects the name query + _mock.ExpectQuery(`SELECT * FROM "secrets" WHERE type = $1 AND org = $2 AND repo = $3 ORDER BY id DESC LIMIT 10`). + WithArgs(constants.SecretRepo, "foo", "bar").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretOne) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretTwo) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Secret + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Secret{_secretTwo, _secretOne}, + }, + { + failure: false, + name: "sqlite", + database: _sqlite, + want: []*library.Secret{_secretTwo, _secretOne}, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListSecretsForRepo(_repo, filters, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListSecretsForRepo for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListSecretsForRepo for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListSecretsForRepo for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/list_team.go b/database/secret/list_team.go new file mode 100644 index 000000000..0897fe949 --- /dev/null +++ b/database/secret/list_team.go @@ -0,0 +1,162 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "strings" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListSecretsForTeam gets a list of secrets by org and team name from the database. +// +//nolint:lll // ignore long line length due to variable names +func (e *engine) ListSecretsForTeam(org, team string, filters map[string]interface{}, page, perPage int) ([]*library.Secret, int64, error) { + e.logger.WithFields(logrus.Fields{ + "org": org, + "team": team, + "type": constants.SecretShared, + }).Tracef("listing secrets for team %s/%s from the database", org, team) + + // variables to store query results and return values + count := int64(0) + s := new([]database.Secret) + secrets := []*library.Secret{} + + // count the results + count, err := e.CountSecretsForTeam(org, team, filters) + if err != nil { + return secrets, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return secrets, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableSecret). + Where("type = ?", constants.SecretShared). + Where("org = ?", org). + Where("team = ?", team). + Where(filters). + Order("id DESC"). + Limit(perPage). + Offset(offset). + Find(&s). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, secret := range *s { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := secret + + // decrypt the fields for the secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt + err = tmp.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted secrets + e.logger.Errorf("unable to decrypt secret %d: %v", tmp.ID.Int64, err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + secrets = append(secrets, tmp.ToLibrary()) + } + + return secrets, count, nil +} + +// ListSecretsForTeams gets a list of secrets by teams within an org from the database. +func (e *engine) ListSecretsForTeams(org string, teams []string, filters map[string]interface{}, page, perPage int) ([]*library.Secret, int64, error) { + // iterate through the list of teams provided + for index, team := range teams { + // ensure the team name is lower case + teams[index] = strings.ToLower(team) + } + + e.logger.WithFields(logrus.Fields{ + "org": org, + "teams": teams, + "type": constants.SecretShared, + }).Tracef("listing secrets for teams %s in org %s from the database", teams, org) + + // variables to store query results and return values + count := int64(0) + s := new([]database.Secret) + secrets := []*library.Secret{} + + // count the results + count, err := e.CountSecretsForTeams(org, teams, filters) + if err != nil { + return secrets, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return secrets, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableSecret). + Where("type = ?", constants.SecretShared). + Where("org = ?", org). + Where("LOWER(team) IN (?)", teams). + Where(filters). + Order("id DESC"). + Limit(perPage). + Offset(offset). + Find(&s). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, secret := range *s { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := secret + + // decrypt the fields for the secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt + err = tmp.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted secrets + e.logger.Errorf("unable to decrypt secret %d: %v", tmp.ID.Int64, err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.ToLibrary + secrets = append(secrets, tmp.ToLibrary()) + } + + return secrets, count, nil +} diff --git a/database/secret/list_team_test.go b/database/secret/list_team_test.go new file mode 100644 index 000000000..546877659 --- /dev/null +++ b/database/secret/list_team_test.go @@ -0,0 +1,227 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/go-vela/types/constants" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSecret_Engine_ListSecretsForTeam(t *testing.T) { + // setup types + _secretOne := testSecret() + _secretOne.SetID(1) + _secretOne.SetOrg("foo") + _secretOne.SetTeam("bar") + _secretOne.SetName("baz") + _secretOne.SetValue("foob") + _secretOne.SetType("shared") + _secretOne.SetCreatedAt(1) + _secretOne.SetCreatedBy("user") + _secretOne.SetUpdatedAt(1) + _secretOne.SetUpdatedBy("user2") + + _secretTwo := testSecret() + _secretTwo.SetID(2) + _secretTwo.SetOrg("foo") + _secretTwo.SetTeam("bar") + _secretTwo.SetName("foob") + _secretTwo.SetValue("baz") + _secretTwo.SetType("shared") + _secretTwo.SetCreatedAt(1) + _secretTwo.SetCreatedBy("user") + _secretTwo.SetUpdatedAt(1) + _secretTwo.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected name count query result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the name count query + _mock.ExpectQuery(`SELECT count(*) FROM "secrets" WHERE type = $1 AND org = $2 AND team = $3`). + WithArgs(constants.SecretShared, "foo", "bar").WillReturnRows(_rows) + + // create expected name query result in mock + _rows = sqlmock.NewRows( + []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}). + AddRow(2, "shared", "foo", "", "bar", "foob", "baz", nil, nil, false, 1, "user", 1, "user2"). + AddRow(1, "shared", "foo", "", "bar", "baz", "foob", nil, nil, false, 1, "user", 1, "user2") + + // ensure the mock expects the name query + _mock.ExpectQuery(`SELECT * FROM "secrets" WHERE type = $1 AND org = $2 AND team = $3 ORDER BY id DESC LIMIT 10`). + WithArgs(constants.SecretShared, "foo", "bar").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretOne) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretTwo) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Secret + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Secret{_secretTwo, _secretOne}, + }, + { + failure: false, + name: "sqlite", + database: _sqlite, + want: []*library.Secret{_secretTwo, _secretOne}, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListSecretsForTeam("foo", "bar", filters, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListSecretsForTeam for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListSecretsForTeam for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListSecretsForTeam for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +func TestSecret_Engine_ListSecretsForTeams(t *testing.T) { + // setup types + _secretOne := testSecret() + _secretOne.SetID(1) + _secretOne.SetOrg("foo") + _secretOne.SetTeam("bar") + _secretOne.SetName("baz") + _secretOne.SetValue("foob") + _secretOne.SetType("shared") + _secretOne.SetCreatedAt(1) + _secretOne.SetCreatedBy("user") + _secretOne.SetUpdatedAt(1) + _secretOne.SetUpdatedBy("user2") + + _secretTwo := testSecret() + _secretTwo.SetID(2) + _secretTwo.SetOrg("foo") + _secretTwo.SetTeam("bar") + _secretTwo.SetName("foob") + _secretTwo.SetValue("baz") + _secretTwo.SetType("shared") + _secretTwo.SetCreatedAt(1) + _secretTwo.SetCreatedBy("user") + _secretTwo.SetUpdatedAt(1) + _secretTwo.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected name count query result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the name count query + _mock.ExpectQuery(`SELECT count(*) FROM "secrets" WHERE type = $1 AND org = $2 AND LOWER(team) IN ($3,$4)`). + WithArgs(constants.SecretShared, "foo", "foo", "bar").WillReturnRows(_rows) + + // create expected name query result in mock + _rows = sqlmock.NewRows( + []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}). + AddRow(2, "shared", "foo", "", "bar", "foob", "baz", nil, nil, false, 1, "user", 1, "user2"). + AddRow(1, "shared", "foo", "", "bar", "baz", "foob", nil, nil, false, 1, "user", 1, "user2") + + // ensure the mock expects the name query + _mock.ExpectQuery(`SELECT * FROM "secrets" WHERE type = $1 AND org = $2 AND LOWER(team) IN ($3,$4) ORDER BY id DESC LIMIT 10`). + WithArgs(constants.SecretShared, "foo", "foo", "bar").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretOne) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretTwo) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Secret + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Secret{_secretTwo, _secretOne}, + }, + { + failure: false, + name: "sqlite", + database: _sqlite, + want: []*library.Secret{_secretTwo, _secretOne}, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListSecretsForTeams("foo", []string{"foo", "bar"}, filters, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListSecretsForTeams for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListSecretsForTeams for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListSecretsForTeams for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/list_test.go b/database/secret/list_test.go new file mode 100644 index 000000000..d0f4d9768 --- /dev/null +++ b/database/secret/list_test.go @@ -0,0 +1,115 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSecret_Engine_ListSecrets(t *testing.T) { + // setup types + _secretOne := testSecret() + _secretOne.SetID(1) + _secretOne.SetOrg("foo") + _secretOne.SetRepo("bar") + _secretOne.SetName("baz") + _secretOne.SetValue("foob") + _secretOne.SetType("repo") + _secretOne.SetCreatedAt(1) + _secretOne.SetCreatedBy("user") + _secretOne.SetUpdatedAt(1) + _secretOne.SetUpdatedBy("user2") + + _secretTwo := testSecret() + _secretTwo.SetID(2) + _secretTwo.SetOrg("foo") + _secretTwo.SetRepo("bar") + _secretTwo.SetName("foob") + _secretTwo.SetValue("baz") + _secretTwo.SetType("repo") + _secretTwo.SetCreatedAt(1) + _secretTwo.SetCreatedBy("user") + _secretTwo.SetUpdatedAt(1) + _secretTwo.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "secrets"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "type", "org", "repo", "team", "name", "value", "images", "events", "allow_command", "created_at", "created_by", "updated_at", "updated_by"}). + AddRow(1, "repo", "foo", "bar", "", "baz", "foob", nil, nil, false, 1, "user", 1, "user2"). + AddRow(2, "repo", "foo", "bar", "", "foob", "baz", nil, nil, false, 1, "user", 1, "user2") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "secrets"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretOne) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretTwo) + if err != nil { + t.Errorf("unable to create test secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Secret + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Secret{_secretOne, _secretTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Secret{_secretOne, _secretTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListSecrets() + + if test.failure { + if err == nil { + t.Errorf("ListSecrets for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListSecrets for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListSecrets for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/secret/opts.go b/database/secret/opts.go new file mode 100644 index 000000000..0e6e7935f --- /dev/null +++ b/database/secret/opts.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Secrets. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Secrets. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the secret engine + e.client = client + + return nil + } +} + +// WithEncryptionKey sets the encryption key in the database engine for Secrets. +func WithEncryptionKey(key string) EngineOpt { + return func(e *engine) error { + // set the encryption key in the secret engine + e.config.EncryptionKey = key + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Secrets. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the secret engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Secrets. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the secret engine + e.config.SkipCreation = skipCreation + + return nil + } +} diff --git a/database/secret/opts_test.go b/database/secret/opts_test.go new file mode 100644 index 000000000..0e123abdb --- /dev/null +++ b/database/secret/opts_test.go @@ -0,0 +1,210 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestSecret_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestSecret_EngineOpt_WithEncryptionKey(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + key string + want string + }{ + { + failure: false, + name: "encryption key set", + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + want: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + }, + { + failure: false, + name: "encryption key not set", + key: "", + want: "", + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithEncryptionKey(test.key)(e) + + if test.failure { + if err == nil { + t.Errorf("WithEncryptionKey for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithEncryptionKey returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.EncryptionKey, test.want) { + t.Errorf("WithEncryptionKey is %v, want %v", e.config.EncryptionKey, test.want) + } + }) + } +} + +func TestSecret_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestSecret_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} diff --git a/database/secret/secret.go b/database/secret/secret.go new file mode 100644 index 000000000..fa342f5d5 --- /dev/null +++ b/database/secret/secret.go @@ -0,0 +1,82 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the SecretInterface interface. + config struct { + // specifies the encryption key to use for the Secret engine + EncryptionKey string + // specifies to skip creating tables and indexes for the Secret engine + SkipCreation bool + } + + // engine represents the secret functionality that implements the SecretInterface interface. + engine struct { + // engine configuration settings used in secret functions + config *config + + // gorm.io/gorm database client used in secret functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in secret functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with secrets in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new Secret engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating secret database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of secrets table and indexes in the database") + + return e, nil + } + + // create the secrets table + err := e.CreateSecretTable(e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableSecret, err) + } + + // create the indexes for the secrets table + err = e.CreateSecretIndexes() + if err != nil { + return nil, fmt.Errorf("unable to create indexes for %s table: %w", constants.TableSecret, err) + } + + return e, nil +} diff --git a/database/secret/secret_test.go b/database/secret/secret_test.go new file mode 100644 index 000000000..0d6a61c25 --- /dev/null +++ b/database/secret/secret_test.go @@ -0,0 +1,255 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "database/sql/driver" + "reflect" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestSecret_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateTypeOrgRepo).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateTypeOrgTeam).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateTypeOrg).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", SkipCreation: false}, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", SkipCreation: false}, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithClient(test.client), + WithEncryptionKey(test.key), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateTypeOrgRepo).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateTypeOrgTeam).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateTypeOrg).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres secret engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite secret engine: %v", err) + } + + return _engine +} + +// testRepo is a test helper function to create a library +// Repo type with all fields set to their zero values. +func testRepo() *library.Repo { + return &library.Repo{ + ID: new(int64), + UserID: new(int64), + BuildLimit: new(int64), + Timeout: new(int64), + Counter: new(int), + PipelineType: new(string), + Hash: new(string), + Org: new(string), + Name: new(string), + FullName: new(string), + Link: new(string), + Clone: new(string), + Branch: new(string), + Visibility: new(string), + PreviousName: new(string), + Private: new(bool), + Trusted: new(bool), + Active: new(bool), + AllowPull: new(bool), + AllowPush: new(bool), + AllowDeploy: new(bool), + AllowTag: new(bool), + AllowComment: new(bool), + } +} + +// testSecret is a test helper function to create a library +// Secret type with all fields set to their zero values. +func testSecret() *library.Secret { + return &library.Secret{ + ID: new(int64), + Org: new(string), + Repo: new(string), + Team: new(string), + Name: new(string), + Value: new(string), + Type: new(string), + Images: new([]string), + Events: new([]string), + AllowCommand: new(bool), + CreatedAt: new(int64), + CreatedBy: new(string), + UpdatedAt: new(int64), + UpdatedBy: new(string), + } +} + +// This will be used with the github.com/DATA-DOG/go-sqlmock library to compare values +// that are otherwise not easily compared. These typically would be values generated +// before adding or updating them in the database. +// +// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime +type AnyArgument struct{} + +// Match satisfies sqlmock.Argument interface. +func (a AnyArgument) Match(_ driver.Value) bool { + return true +} + +// NowTimestamp is used to test whether timestamps get updated correctly to the current time with lenience. +type NowTimestamp struct{} + +// Match satisfies sqlmock.Argument interface. +func (t NowTimestamp) Match(v driver.Value) bool { + ts, ok := v.(int64) + if !ok { + return false + } + now := time.Now().Unix() + + return now-ts < 10 +} diff --git a/database/secret/table.go b/database/secret/table.go new file mode 100644 index 000000000..b36b6c4e9 --- /dev/null +++ b/database/secret/table.go @@ -0,0 +1,74 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import "github.com/go-vela/types/constants" + +const ( + // CreatePostgresTable represents a query to create the Postgres secrets table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +secrets ( + id SERIAL PRIMARY KEY, + type VARCHAR(100), + org VARCHAR(250), + repo VARCHAR(250), + team VARCHAR(250), + name VARCHAR(250), + value BYTEA, + images VARCHAR(1000), + events VARCHAR(1000), + allow_command BOOLEAN, + created_at INTEGER, + created_by VARCHAR(250), + updated_at INTEGER, + updated_by VARCHAR(250), + UNIQUE(type, org, repo, name), + UNIQUE(type, org, team, name) +); +` + + // CreateSqliteTable represents a query to create the Sqlite secrets table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +secrets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT, + org TEXT, + repo TEXT, + team TEXT, + name TEXT, + value TEXT, + images TEXT, + events TEXT, + allow_command BOOLEAN, + created_at INTEGER, + created_by TEXT, + updated_at INTEGER, + updated_by TEXT, + UNIQUE(type, org, repo, name), + UNIQUE(type, org, team, name) +); +` +) + +// CreateSecretTable creates the secrets table in the database. +func (e *engine) CreateSecretTable(driver string) error { + e.logger.Tracef("creating secrets table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the secrets table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the secrets table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/secret/table_test.go b/database/secret/table_test.go new file mode 100644 index 000000000..055d84fa6 --- /dev/null +++ b/database/secret/table_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestSecret_Engine_CreateSecretTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateSecretTable(test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateSecretTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateSecretTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/secret/update.go b/database/secret/update.go new file mode 100644 index 000000000..823ede644 --- /dev/null +++ b/database/secret/update.go @@ -0,0 +1,79 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code with create.go +package secret + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// UpdateSecret updates an existing secret in the database. +func (e *engine) UpdateSecret(s *library.Secret) (*library.Secret, error) { + // handle the secret based off the type + switch s.GetType() { + case constants.SecretShared: + e.logger.WithFields(logrus.Fields{ + "org": s.GetOrg(), + "team": s.GetTeam(), + "secret": s.GetName(), + "type": s.GetType(), + }).Tracef("updating secret %s/%s/%s/%s in the database", s.GetType(), s.GetOrg(), s.GetTeam(), s.GetName()) + default: + e.logger.WithFields(logrus.Fields{ + "org": s.GetOrg(), + "repo": s.GetRepo(), + "secret": s.GetName(), + "type": s.GetType(), + }).Tracef("updating secret %s/%s/%s/%s in the database", s.GetType(), s.GetOrg(), s.GetRepo(), s.GetName()) + } + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#SecretFromLibrary + secret := database.SecretFromLibrary(s) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Validate + err := secret.Validate() + if err != nil { + return nil, err + } + + // encrypt the fields for the secret + // + // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Encrypt + err = secret.Encrypt(e.config.EncryptionKey) + if err != nil { + switch s.GetType() { + case constants.SecretShared: + return nil, fmt.Errorf("unable to encrypt secret %s/%s/%s/%s: %w", s.GetType(), s.GetOrg(), s.GetTeam(), s.GetName(), err) + default: + return nil, fmt.Errorf("unable to encrypt secret %s/%s/%s/%s: %w", s.GetType(), s.GetOrg(), s.GetRepo(), s.GetName(), err) + } + } + + err = e.client.Table(constants.TableSecret).Save(secret.Nullify()).Error + if err != nil { + return nil, err + } + + err = secret.Decrypt(e.config.EncryptionKey) + if err != nil { + switch s.GetType() { + case constants.SecretShared: + return nil, fmt.Errorf("unable to decrypt secret %s/%s/%s/%s: %w", s.GetType(), s.GetOrg(), s.GetTeam(), s.GetName(), err) + default: + return nil, fmt.Errorf("unable to decrypt secret %s/%s/%s/%s: %w", s.GetType(), s.GetOrg(), s.GetRepo(), s.GetName(), err) + } + } + + return secret.ToLibrary(), nil +} diff --git a/database/secret/update_test.go b/database/secret/update_test.go new file mode 100644 index 000000000..247b9d229 --- /dev/null +++ b/database/secret/update_test.go @@ -0,0 +1,163 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package secret + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestSecret_Engine_UpdateSecret(t *testing.T) { + // setup types + _secretRepo := testSecret() + _secretRepo.SetID(1) + _secretRepo.SetOrg("foo") + _secretRepo.SetRepo("bar") + _secretRepo.SetName("baz") + _secretRepo.SetValue("foob") + _secretRepo.SetType("repo") + _secretRepo.SetCreatedAt(1) + _secretRepo.SetCreatedBy("user") + _secretRepo.SetUpdatedAt(1) + _secretRepo.SetUpdatedBy("user2") + + _secretOrg := testSecret() + _secretOrg.SetID(2) + _secretOrg.SetOrg("foo") + _secretOrg.SetRepo("*") + _secretOrg.SetName("bar") + _secretOrg.SetValue("baz") + _secretOrg.SetType("org") + _secretOrg.SetCreatedAt(1) + _secretOrg.SetCreatedBy("user") + _secretOrg.SetUpdatedAt(1) + _secretOrg.SetUpdatedBy("user2") + + _secretShared := testSecret() + _secretShared.SetID(3) + _secretShared.SetOrg("foo") + _secretShared.SetTeam("bar") + _secretShared.SetName("baz") + _secretShared.SetValue("foob") + _secretShared.SetType("shared") + _secretShared.SetCreatedAt(1) + _secretShared.SetCreatedBy("user") + _secretShared.SetUpdatedAt(1) + _secretShared.SetUpdatedBy("user2") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the repo query + _mock.ExpectExec(`UPDATE "secrets" +SET "org"=$1,"repo"=$2,"team"=$3,"name"=$4,"value"=$5,"type"=$6,"images"=$7,"events"=$8,"allow_command"=$9,"created_at"=$10,"created_by"=$11,"updated_at"=$12,"updated_by"=$13 +WHERE "id" = $14`). + WithArgs("foo", "bar", nil, "baz", AnyArgument{}, "repo", nil, nil, false, 1, "user", AnyArgument{}, "user2", 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // ensure the mock expects the org query + _mock.ExpectExec(`UPDATE "secrets" +SET "org"=$1,"repo"=$2,"team"=$3,"name"=$4,"value"=$5,"type"=$6,"images"=$7,"events"=$8,"allow_command"=$9,"created_at"=$10,"created_by"=$11,"updated_at"=$12,"updated_by"=$13 +WHERE "id" = $14`). + WithArgs("foo", "*", nil, "bar", AnyArgument{}, "org", nil, nil, false, 1, "user", AnyArgument{}, "user2", 2). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // ensure the mock expects the shared query + _mock.ExpectExec(`UPDATE "secrets" +SET "org"=$1,"repo"=$2,"team"=$3,"name"=$4,"value"=$5,"type"=$6,"images"=$7,"events"=$8,"allow_command"=$9,"created_at"=$10,"created_by"=$11,"updated_at"=$12,"updated_by"=$13 +WHERE "id" = $14`). + WithArgs("foo", nil, "bar", "baz", AnyArgument{}, "shared", nil, nil, false, 1, "user", NowTimestamp{}, "user2", 3). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateSecret(_secretRepo) + if err != nil { + t.Errorf("unable to create test repo secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretOrg) + if err != nil { + t.Errorf("unable to create test org secret for sqlite: %v", err) + } + + _, err = _sqlite.CreateSecret(_secretShared) + if err != nil { + t.Errorf("unable to create test shared secret for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + secret *library.Secret + }{ + { + failure: false, + name: "postgres with repo", + database: _postgres, + secret: _secretRepo, + }, + { + failure: false, + name: "postgres with org", + database: _postgres, + secret: _secretOrg, + }, + { + failure: false, + name: "postgres with shared", + database: _postgres, + secret: _secretShared, + }, + { + failure: false, + name: "sqlite3 with repo", + database: _sqlite, + secret: _secretRepo, + }, + { + failure: false, + name: "sqlite3 with org", + database: _sqlite, + secret: _secretOrg, + }, + { + failure: false, + name: "sqlite3 with shared", + database: _sqlite, + secret: _secretShared, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.UpdateSecret(test.secret) + got.SetUpdatedAt(test.secret.GetUpdatedAt()) + + if test.failure { + if err == nil { + t.Errorf("UpdateSecret for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateSecret for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.secret) { + t.Errorf("UpdateSecret for %s is %s, want %s", test.name, got, test.secret) + } + }) + } +} diff --git a/database/service.go b/database/service.go deleted file mode 100644 index 314f434e1..000000000 --- a/database/service.go +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package database - -import ( - "github.com/go-vela/types/library" -) - -// Service represents the interface for Vela integrating -// with the different supported Database backends. -type Service interface { - // Database Interface Functions - - // Driver defines a function that outputs - // the configured database driver. - Driver() string - - // Build Database Interface Functions - - // GetBuild defines a function that - // gets a build by number and repo ID. - GetBuild(int, *library.Repo) (*library.Build, error) - // GetLastBuild defines a function that - // gets the last build ran by repo ID. - GetLastBuild(*library.Repo) (*library.Build, error) - // GetLastBuildByBranch defines a function that - // gets the last build ran by repo ID and branch. - GetLastBuildByBranch(*library.Repo, string) (*library.Build, error) - // GetBuildCount defines a function that - // gets the count of builds. - GetBuildCount() (int64, error) - // GetBuildCountByStatus defines a function that - // gets a the count of builds by status. - GetBuildCountByStatus(string) (int64, error) - // GetBuildList defines a function that gets - // a list of all builds. - GetBuildList() ([]*library.Build, error) - // GetDeploymentBuildList defines a function that gets - // a list of builds related to a deployment. - GetDeploymentBuildList(string) ([]*library.Build, error) - // GetRepoBuildList defines a function that - // gets a list of builds by repo ID. - GetRepoBuildList(*library.Repo, map[string]interface{}, int, int) ([]*library.Build, int64, error) - // GetOrgBuildList defines a function that - // gets a list of builds by org. - GetOrgBuildList(string, map[string]interface{}, int, int) ([]*library.Build, int64, error) - // GetRepoBuildCount defines a function that - // gets the count of builds by repo ID. - GetRepoBuildCount(*library.Repo, map[string]interface{}) (int64, error) - // GetOrgBuildCount defines a function that - // gets the count of builds by org. - GetOrgBuildCount(string, map[string]interface{}) (int64, error) - // GetPendingAndRunningBuilds defines a function that - // gets the list of pending and running builds. - GetPendingAndRunningBuilds(string) ([]*library.BuildQueue, error) - // CreateBuild defines a function that - // creates a new build. - CreateBuild(*library.Build) error - // UpdateBuild defines a function that - // updates a build. - UpdateBuild(*library.Build) error - // DeleteBuild defines a function that - // deletes a build by unique ID. - DeleteBuild(int64) error - - // Hook Database Interface Functions - - // GetHook defines a function that - // gets a webhook by number and repo ID. - GetHook(int, *library.Repo) (*library.Hook, error) - // GetLastHook defines a function that - // gets the last hook by repo ID. - GetLastHook(*library.Repo) (*library.Hook, error) - // GetHookList defines a function that gets - // a list of all webhooks. - GetHookList() ([]*library.Hook, error) - // GetRepoHookList defines a function that - // gets a list of webhooks by repo ID. - GetRepoHookList(*library.Repo, int, int) ([]*library.Hook, error) - // GetRepoHookCount defines a function that - // gets the count of webhooks by repo ID. - GetRepoHookCount(*library.Repo) (int64, error) - // CreateHook defines a function that - // creates a new webhook. - CreateHook(*library.Hook) error - // UpdateHook defines a function that - // updates a webhook. - UpdateHook(*library.Hook) error - // DeleteHook defines a function that - // deletes a webhook by unique ID. - DeleteHook(int64) error - - // Log Database Interface Functions - - // GetStepLog defines a function that - // gets a step log by unique ID. - GetStepLog(int64) (*library.Log, error) - // GetServiceLog defines a function that - // gets a service log by unique ID. - GetServiceLog(int64) (*library.Log, error) - // GetBuildLogs defines a function that - // gets a list of logs by build ID. - GetBuildLogs(int64) ([]*library.Log, error) - // CreateLog defines a function that - // creates a new log. - CreateLog(*library.Log) error - // UpdateLog defines a function that - // updates a log. - UpdateLog(*library.Log) error - // DeleteLog defines a function that - // deletes a log by unique ID. - DeleteLog(int64) error - - // Repo Database Interface Functions - - // GetRepo defines a function that - // gets a repo by org and name. - GetRepo(string, string) (*library.Repo, error) - // GetRepoList defines a function that - // gets a list of all repos. - GetRepoList() ([]*library.Repo, error) - // GetOrgRepoList defines a function that - // gets a list of all repos by org excluding repos specified. - GetOrgRepoList(string, map[string]string, int, int) ([]*library.Repo, error) - // GetOrgRepoCount defines a function that - // gets the count of repos for an org. - GetOrgRepoCount(string, map[string]string) (int64, error) - // GetRepoCount defines a function that - // gets the count of repos. - GetRepoCount() (int64, error) - // GetUserRepoList defines a function - // that gets a list of repos by user ID. - GetUserRepoList(*library.User, int, int) ([]*library.Repo, error) - // GetUserRepoCount defines a function that - // gets the count of repos for a user. - GetUserRepoCount(*library.User) (int64, error) - // CreateRepo defines a function that - // creates a new repo. - CreateRepo(*library.Repo) error - // UpdateRepo defines a function that - // updates a repo. - UpdateRepo(*library.Repo) error - // DeleteRepo defines a function that - // deletes a repo by unique ID. - DeleteRepo(int64) error - - // Secret Database Interface Functions - - // GetSecret defines a function that gets a secret - // by type, org, name (repo or team) and secret name. - GetSecret(string, string, string, string) (*library.Secret, error) - // GetSecretList defines a function that - // gets a list of all secrets. - GetSecretList() ([]*library.Secret, error) - // GetTypeSecretList defines a function that gets a list - // of secrets by type, owner, and name (repo or team). - GetTypeSecretList(string, string, string, int, int, []string) ([]*library.Secret, error) - // GetTypeSecretCount defines a function that gets a count - // of secrets by type, owner, and name (repo or team). - GetTypeSecretCount(string, string, string, []string) (int64, error) - // CreateSecret defines a function that - // creates a new secret. - CreateSecret(*library.Secret) error - // UpdateSecret defines a function that - // updates a secret. - UpdateSecret(*library.Secret) error - // DeleteSecret defines a function that - // deletes a secret by unique ID. - DeleteSecret(int64) error - - // Step Database Interface Functions - - // GetStep defines a function that - // gets a step by number and build ID. - GetStep(int, *library.Build) (*library.Step, error) - // GetStepList defines a function that - // gets a list of all steps. - GetStepList() ([]*library.Step, error) - // GetBuildStepList defines a function that - // gets a list of steps by build ID. - GetBuildStepList(*library.Build, int, int) ([]*library.Step, error) - // GetBuildStepCount defines a function that - // gets the count of steps by build ID. - GetBuildStepCount(*library.Build) (int64, error) - // GetStepImageCount defines a function that - // gets a list of all step images and the - // count of their occurrence. - GetStepImageCount() (map[string]float64, error) - // GetStepStatusCount defines a function that - // gets a list of all step statuses and the - // count of their occurrence. - GetStepStatusCount() (map[string]float64, error) - // CreateStep defines a function that - // creates a new step. - CreateStep(*library.Step) error - // UpdateStep defines a function that - // updates a step. - UpdateStep(*library.Step) error - // DeleteStep defines a function that - // deletes a step by unique ID. - DeleteStep(int64) error - - // Service Database Interface Functions - - // GetService defines a function that - // gets a step by number and build ID. - GetService(int, *library.Build) (*library.Service, error) - // GetServiceList defines a function that - // gets a list of all steps. - GetServiceList() ([]*library.Service, error) - // GetBuildServiceList defines a function - // that gets a list of steps by build ID. - GetBuildServiceList(*library.Build, int, int) ([]*library.Service, error) - // GetBuildServiceCount defines a function - // that gets the count of steps by build ID. - GetBuildServiceCount(*library.Build) (int64, error) - // GetServiceImageCount defines a function that - // gets a list of all service images and the - // count of their occurrence. - GetServiceImageCount() (map[string]float64, error) - // GetServiceStatusCount defines a function that - // gets a list of all service statuses and the - // count of their occurrence. - GetServiceStatusCount() (map[string]float64, error) - // CreateService defines a function that - // creates a new step. - CreateService(*library.Service) error - // UpdateService defines a function that - // updates a step. - UpdateService(*library.Service) error - // DeleteService defines a function that - // deletes a step by unique ID. - DeleteService(int64) error - - // User Database Interface Functions - - // GetUser defines a function that - // gets a user by unique ID. - GetUser(int64) (*library.User, error) - // GetUserName defines a function that - // gets a user by name. - GetUserName(string) (*library.User, error) - // GetUserList defines a function that - // gets a list of all users. - GetUserList() ([]*library.User, error) - // GetUserCount defines a function that - // gets the count of users. - GetUserCount() (int64, error) - // GetUserLiteList defines a function - // that gets a lite list of users. - GetUserLiteList(int, int) ([]*library.User, error) - // CreateUser defines a function that - // creates a new user. - CreateUser(*library.User) error - // UpdateUser defines a function that - // updates a user. - UpdateUser(*library.User) error - // DeleteUser defines a function that - // deletes a user by unique ID. - DeleteUser(int64) error - - // Worker Database Interface Functions - - // GetWorker defines a function that - // gets a worker by hostname. - GetWorker(string) (*library.Worker, error) - // GetWorkerAddress defines a function that - // gets a worker by address. - GetWorkerByAddress(string) (*library.Worker, error) - // GetWorkerList defines a function that - // gets a list of all workers. - GetWorkerList() ([]*library.Worker, error) - // GetWorkerCount defines a function that - // gets the count of workers. - GetWorkerCount() (int64, error) - // CreateWorker defines a function that - // creates a new worker. - CreateWorker(*library.Worker) error - // UpdateWorker defines a function that - // updates a worker by unique ID. - UpdateWorker(*library.Worker) error - // DeleteWorker defines a function that - // deletes a worker by hostname. - DeleteWorker(int64) error -} diff --git a/database/service/clean.go b/database/service/clean.go new file mode 100644 index 000000000..e8bcac16d --- /dev/null +++ b/database/service/clean.go @@ -0,0 +1,35 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "time" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CleanServices updates services to an error with a created timestamp prior to a defined moment. +func (e *engine) CleanServices(msg string, before int64) (int64, error) { + logrus.Tracef("cleaning pending or running steps in the database created prior to %d", before) + + s := new(library.Service) + s.SetStatus(constants.StatusError) + s.SetError(msg) + s.SetFinished(time.Now().UTC().Unix()) + + service := database.ServiceFromLibrary(s) + + // send query to the database + result := e.client. + Table(constants.TableService). + Where("created < ?", before). + Where("status = 'running' OR status = 'pending'"). + Updates(service) + + return result.RowsAffected, result.Error +} diff --git a/database/service/clean_test.go b/database/service/clean_test.go new file mode 100644 index 000000000..17301e119 --- /dev/null +++ b/database/service/clean_test.go @@ -0,0 +1,130 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestService_Engine_CleanService(t *testing.T) { + // setup types + _serviceOne := testService() + _serviceOne.SetID(1) + _serviceOne.SetRepoID(1) + _serviceOne.SetBuildID(1) + _serviceOne.SetNumber(1) + _serviceOne.SetName("foo") + _serviceOne.SetImage("bar") + _serviceOne.SetCreated(1) + _serviceOne.SetStatus("running") + + _serviceTwo := testService() + _serviceTwo.SetID(2) + _serviceTwo.SetRepoID(1) + _serviceTwo.SetBuildID(1) + _serviceTwo.SetNumber(2) + _serviceTwo.SetName("foo") + _serviceTwo.SetImage("bar") + _serviceTwo.SetCreated(1) + _serviceTwo.SetStatus("pending") + + _serviceThree := testService() + _serviceThree.SetID(3) + _serviceThree.SetRepoID(1) + _serviceThree.SetBuildID(1) + _serviceThree.SetNumber(3) + _serviceThree.SetName("foo") + _serviceThree.SetImage("bar") + _serviceThree.SetCreated(1) + _serviceThree.SetStatus("success") + + _serviceFour := testService() + _serviceFour.SetID(4) + _serviceFour.SetRepoID(1) + _serviceFour.SetBuildID(1) + _serviceFour.SetNumber(4) + _serviceFour.SetName("foo") + _serviceFour.SetImage("bar") + _serviceFour.SetCreated(5) + _serviceFour.SetStatus("pending") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the name query + _mock.ExpectExec(`UPDATE "services" SET "status"=$1,"error"=$2,"finished"=$3 WHERE created < $4 AND (status = 'running' OR status = 'pending')`). + WithArgs("error", "msg", NowTimestamp{}, 3). + WillReturnResult(sqlmock.NewResult(1, 2)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateService(_serviceOne) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + _, err = _sqlite.CreateService(_serviceTwo) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + _, err = _sqlite.CreateService(_serviceThree) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + _, err = _sqlite.CreateService(_serviceFour) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CleanServices("msg", 3) + + if test.failure { + if err == nil { + t.Errorf("CleanServices for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CleanServices for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CleanServices for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/service/count.go b/database/service/count.go new file mode 100644 index 000000000..08b488933 --- /dev/null +++ b/database/service/count.go @@ -0,0 +1,25 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/go-vela/types/constants" +) + +// CountServices gets the count of all services from the database. +func (e *engine) CountServices() (int64, error) { + e.logger.Tracef("getting count of all services from the database") + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableService). + Count(&s). + Error + + return s, err +} diff --git a/database/service/count_build.go b/database/service/count_build.go new file mode 100644 index 000000000..0cd773570 --- /dev/null +++ b/database/service/count_build.go @@ -0,0 +1,31 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CountServicesForBuild gets the count of services by build ID from the database. +func (e *engine) CountServicesForBuild(b *library.Build, filters map[string]interface{}) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "build": b.GetNumber(), + }).Tracef("getting count of services for build %d from the database", b.GetNumber()) + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableService). + Where("build_id = ?", b.GetID()). + Where(filters). + Count(&s). + Error + + return s, err +} diff --git a/database/service/count_build_test.go b/database/service/count_build_test.go new file mode 100644 index 000000000..160dfa5f8 --- /dev/null +++ b/database/service/count_build_test.go @@ -0,0 +1,104 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestService_Engine_CountServicesForBuild(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + + _serviceOne := testService() + _serviceOne.SetID(1) + _serviceOne.SetRepoID(1) + _serviceOne.SetBuildID(1) + _serviceOne.SetNumber(1) + _serviceOne.SetName("foo") + _serviceOne.SetImage("bar") + + _serviceTwo := testService() + _serviceTwo.SetID(2) + _serviceTwo.SetRepoID(1) + _serviceTwo.SetBuildID(2) + _serviceTwo.SetNumber(1) + _serviceTwo.SetName("foo") + _serviceTwo.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "services" WHERE build_id = $1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateService(_serviceOne) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + _, err = _sqlite.CreateService(_serviceTwo) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 1, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 1, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountServicesForBuild(_build, filters) + + if test.failure { + if err == nil { + t.Errorf("CountServicesForBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountServicesForBuild for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountServicesForBuild for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/service/count_test.go b/database/service/count_test.go new file mode 100644 index 000000000..4152c18e9 --- /dev/null +++ b/database/service/count_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestService_Engine_CountServices(t *testing.T) { + // setup types + _serviceOne := testService() + _serviceOne.SetID(1) + _serviceOne.SetRepoID(1) + _serviceOne.SetBuildID(1) + _serviceOne.SetNumber(1) + _serviceOne.SetName("foo") + _serviceOne.SetImage("bar") + + _serviceTwo := testService() + _serviceTwo.SetID(2) + _serviceTwo.SetRepoID(1) + _serviceTwo.SetBuildID(2) + _serviceTwo.SetNumber(1) + _serviceTwo.SetName("foo") + _serviceTwo.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "services"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateService(_serviceOne) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + _, err = _sqlite.CreateService(_serviceTwo) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountServices() + + if test.failure { + if err == nil { + t.Errorf("CountServices for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountServices for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountServices for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/service/create.go b/database/service/create.go new file mode 100644 index 000000000..f748be077 --- /dev/null +++ b/database/service/create.go @@ -0,0 +1,37 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreateService creates a new service in the database. +func (e *engine) CreateService(s *library.Service) (*library.Service, error) { + e.logger.WithFields(logrus.Fields{ + "service": s.GetNumber(), + }).Tracef("creating service %s in the database", s.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#ServiceFromLibrary + service := database.ServiceFromLibrary(s) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Service.Validate + err := service.Validate() + if err != nil { + return nil, err + } + + // send query to the database + result := e.client.Table(constants.TableService).Create(service) + + return service.ToLibrary(), result.Error +} diff --git a/database/service/create_test.go b/database/service/create_test.go new file mode 100644 index 000000000..26771f31c --- /dev/null +++ b/database/service/create_test.go @@ -0,0 +1,80 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestService_Engine_CreateService(t *testing.T) { + // setup types + _service := testService() + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetNumber(1) + _service.SetName("foo") + _service.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`INSERT INTO "services" +("build_id","repo_id","number","name","image","status","error","exit_code","created","started","finished","host","runtime","distribution","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING "id"`). + WithArgs(1, 1, 1, "foo", "bar", nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CreateService(_service) + + if test.failure { + if err == nil { + t.Errorf("CreateService for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateService for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _service) { + t.Errorf("CreateService for %s returned %s, want %s", test.name, got, _service) + } + }) + } +} diff --git a/database/service/delete.go b/database/service/delete.go new file mode 100644 index 000000000..86c0c21b2 --- /dev/null +++ b/database/service/delete.go @@ -0,0 +1,30 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// DeleteService deletes an existing service from the database. +func (e *engine) DeleteService(s *library.Service) error { + e.logger.WithFields(logrus.Fields{ + "service": s.GetNumber(), + }).Tracef("deleting service %s from the database", s.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#ServiceFromLibrary + service := database.ServiceFromLibrary(s) + + // send query to the database + return e.client. + Table(constants.TableService). + Delete(service). + Error +} diff --git a/database/service/delete_test.go b/database/service/delete_test.go new file mode 100644 index 000000000..04d08fdc8 --- /dev/null +++ b/database/service/delete_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestService_Engine_DeleteService(t *testing.T) { + // setup types + _service := testService() + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetNumber(1) + _service.SetName("foo") + _service.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`DELETE FROM "services" WHERE "services"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateService(_service) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeleteService(_service) + + if test.failure { + if err == nil { + t.Errorf("DeleteService for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeleteService for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/service/get.go b/database/service/get.go new file mode 100644 index 000000000..7678168b2 --- /dev/null +++ b/database/service/get.go @@ -0,0 +1,34 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetService gets a service by ID from the database. +func (e *engine) GetService(id int64) (*library.Service, error) { + e.logger.Tracef("getting service %d from the database", id) + + // variable to store query results + s := new(database.Service) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableService). + Where("id = ?", id). + Take(s). + Error + if err != nil { + return nil, err + } + + // return the service + // + // https://pkg.go.dev/github.com/go-vela/types/database#Service.ToLibrary + return s.ToLibrary(), nil +} diff --git a/database/service/get_build.go b/database/service/get_build.go new file mode 100644 index 000000000..5321cc728 --- /dev/null +++ b/database/service/get_build.go @@ -0,0 +1,39 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetServiceForBuild gets a service by number and build ID from the database. +func (e *engine) GetServiceForBuild(b *library.Build, number int) (*library.Service, error) { + e.logger.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "service": number, + }).Tracef("getting service %d from the database", number) + + // variable to store query results + s := new(database.Service) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableService). + Where("build_id = ?", b.GetID()). + Where("number = ?", number). + Take(s). + Error + if err != nil { + return nil, err + } + + // return the service + // + // https://pkg.go.dev/github.com/go-vela/types/database#Service.ToLibrary + return s.ToLibrary(), nil +} diff --git a/database/service/get_build_test.go b/database/service/get_build_test.go new file mode 100644 index 000000000..4b6a629de --- /dev/null +++ b/database/service/get_build_test.go @@ -0,0 +1,92 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestService_Engine_GetServiceForBuild(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + + _service := testService() + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetNumber(1) + _service.SetName("foo") + _service.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "name", "image", "stage", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}). + AddRow(1, 1, 1, 1, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", "") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "services" WHERE build_id = $1 AND number = $2 LIMIT 1`).WithArgs(1, 1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateService(_service) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Service + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _service, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _service, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetServiceForBuild(_build, 1) + + if test.failure { + if err == nil { + t.Errorf("GetServiceForBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetServiceForBuild for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetServiceForBuild for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/service/get_test.go b/database/service/get_test.go new file mode 100644 index 000000000..1c2734b36 --- /dev/null +++ b/database/service/get_test.go @@ -0,0 +1,87 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestService_Engine_GetService(t *testing.T) { + // setup types + _service := testService() + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetNumber(1) + _service.SetName("foo") + _service.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "name", "image", "stage", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}). + AddRow(1, 1, 1, 1, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", "") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "services" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateService(_service) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Service + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _service, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _service, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetService(1) + + if test.failure { + if err == nil { + t.Errorf("GetService for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetService for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetService for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/service/interface.go b/database/service/interface.go new file mode 100644 index 000000000..0a05c626d --- /dev/null +++ b/database/service/interface.go @@ -0,0 +1,51 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/go-vela/types/library" +) + +// ServiceInterface represents the Vela interface for service +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type ServiceInterface interface { + // Service Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreateServiceTable defines a function that creates the services table. + CreateServiceTable(string) error + + // Service Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CleanServices defines a function that sets running or pending services to error status before a given created time. + CleanServices(string, int64) (int64, error) + // CountServices defines a function that gets the count of all services. + CountServices() (int64, error) + // CountServicesForBuild defines a function that gets the count of services by build ID. + CountServicesForBuild(*library.Build, map[string]interface{}) (int64, error) + // CreateService defines a function that creates a new service. + CreateService(*library.Service) (*library.Service, error) + // DeleteService defines a function that deletes an existing service. + DeleteService(*library.Service) error + // GetService defines a function that gets a service by ID. + GetService(int64) (*library.Service, error) + // GetServiceForBuild defines a function that gets a service by number and build ID. + GetServiceForBuild(*library.Build, int) (*library.Service, error) + // ListServices defines a function that gets a list of all services. + ListServices() ([]*library.Service, error) + // ListServicesForBuild defines a function that gets a list of services by build ID. + ListServicesForBuild(*library.Build, map[string]interface{}, int, int) ([]*library.Service, int64, error) + // ListServiceImageCount defines a function that gets a list of all service images and the count of their occurrence. + ListServiceImageCount() (map[string]float64, error) + // ListServiceStatusCount defines a function that gets a list of all service statuses and the count of their occurrence. + ListServiceStatusCount() (map[string]float64, error) + // UpdateService defines a function that updates an existing service. + UpdateService(*library.Service) (*library.Service, error) +} diff --git a/database/service/list.go b/database/service/list.go new file mode 100644 index 000000000..c7b1b6b13 --- /dev/null +++ b/database/service/list.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListServices gets a list of all services from the database. +func (e *engine) ListServices() ([]*library.Service, error) { + e.logger.Trace("listing all services from the database") + + // variables to store query results and return value + count := int64(0) + w := new([]database.Service) + services := []*library.Service{} + + // count the results + count, err := e.CountServices() + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return services, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableService). + Find(&w). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, service := range *w { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := service + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Service.ToLibrary + services = append(services, tmp.ToLibrary()) + } + + return services, nil +} diff --git a/database/service/list_build.go b/database/service/list_build.go new file mode 100644 index 000000000..caffb2d5c --- /dev/null +++ b/database/service/list_build.go @@ -0,0 +1,65 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListServicesForBuild gets a list of all services from the database. +func (e *engine) ListServicesForBuild(b *library.Build, filters map[string]interface{}, page int, perPage int) ([]*library.Service, int64, error) { + e.logger.WithFields(logrus.Fields{ + "build": b.GetNumber(), + }).Tracef("listing services for build %d from the database", b.GetNumber()) + + // variables to store query results and return value + count := int64(0) + s := new([]database.Service) + services := []*library.Service{} + + // count the results + count, err := e.CountServicesForBuild(b, filters) + if err != nil { + return services, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return services, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableService). + Where("build_id = ?", b.GetID()). + Where(filters). + Order("id DESC"). + Limit(perPage). + Offset(offset). + Find(&s). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, service := range *s { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := service + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Service.ToLibrary + services = append(services, tmp.ToLibrary()) + } + + return services, count, nil +} diff --git a/database/service/list_build_test.go b/database/service/list_build_test.go new file mode 100644 index 000000000..1f0a860f4 --- /dev/null +++ b/database/service/list_build_test.go @@ -0,0 +1,114 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestService_Engine_ListServicesForBuild(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + + _serviceOne := testService() + _serviceOne.SetID(1) + _serviceOne.SetRepoID(1) + _serviceOne.SetBuildID(1) + _serviceOne.SetNumber(1) + _serviceOne.SetName("foo") + _serviceOne.SetImage("bar") + + _serviceTwo := testService() + _serviceTwo.SetID(2) + _serviceTwo.SetRepoID(1) + _serviceTwo.SetBuildID(1) + _serviceTwo.SetNumber(2) + _serviceTwo.SetName("foo") + _serviceTwo.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "services" WHERE build_id = $1`).WithArgs(1).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "name", "image", "stage", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}). + AddRow(2, 1, 1, 2, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", ""). + AddRow(1, 1, 1, 1, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", "") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "services" WHERE build_id = $1 ORDER BY id DESC LIMIT 10`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateService(_serviceOne) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + _, err = _sqlite.CreateService(_serviceTwo) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Service + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Service{_serviceTwo, _serviceOne}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Service{_serviceTwo, _serviceOne}, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListServicesForBuild(_build, filters, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListServicesForBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListServicesForBuild for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListServicesForBuild for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/service/list_image.go b/database/service/list_image.go new file mode 100644 index 000000000..6625420dc --- /dev/null +++ b/database/service/list_image.go @@ -0,0 +1,44 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "database/sql" + + "github.com/go-vela/types/constants" +) + +// ListServiceImageCount gets a list of all service images and the count of their occurrence from the database. +func (e *engine) ListServiceImageCount() (map[string]float64, error) { + e.logger.Tracef("getting count of all images for services from the database") + + // variables to store query results and return value + s := []struct { + Image sql.NullString + Count sql.NullInt32 + }{} + images := make(map[string]float64) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableService). + Select("image", " count(image) as count"). + Group("image"). + Find(&s). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, value := range s { + // check if the image returned is not empty + if value.Image.Valid { + images[value.Image.String] = float64(value.Count.Int32) + } + } + + return images, nil +} diff --git a/database/service/list_image_test.go b/database/service/list_image_test.go new file mode 100644 index 000000000..8f0d588d0 --- /dev/null +++ b/database/service/list_image_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestService_Engine_ListServiceImageCount(t *testing.T) { + // setup types + _serviceOne := testService() + _serviceOne.SetID(1) + _serviceOne.SetRepoID(1) + _serviceOne.SetBuildID(1) + _serviceOne.SetNumber(1) + _serviceOne.SetName("foo") + _serviceOne.SetImage("bar") + + _serviceTwo := testService() + _serviceTwo.SetID(2) + _serviceTwo.SetRepoID(1) + _serviceTwo.SetBuildID(1) + _serviceTwo.SetNumber(2) + _serviceTwo.SetName("foo") + _serviceTwo.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"image", "count"}).AddRow("bar", 2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT "image", count(image) as count FROM "services" GROUP BY "image"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateService(_serviceOne) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + _, err = _sqlite.CreateService(_serviceTwo) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want map[string]float64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: map[string]float64{"bar": 2}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: map[string]float64{"bar": 2}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListServiceImageCount() + + if test.failure { + if err == nil { + t.Errorf("ListServiceImageCount for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListServiceImageCount for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListServiceImageCount for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/service/list_status.go b/database/service/list_status.go new file mode 100644 index 000000000..01aedeac1 --- /dev/null +++ b/database/service/list_status.go @@ -0,0 +1,50 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "database/sql" + + "github.com/go-vela/types/constants" +) + +// ListServiceStatusCount gets a list of all service statuses and the count of their occurrence from the database. +func (e *engine) ListServiceStatusCount() (map[string]float64, error) { + e.logger.Tracef("getting count of all statuses for services from the database") + + // variables to store query results and return value + s := []struct { + Status sql.NullString + Count sql.NullInt32 + }{} + statuses := map[string]float64{ + "pending": 0, + "failure": 0, + "killed": 0, + "running": 0, + "success": 0, + } + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableService). + Select("status", " count(status) as count"). + Group("status"). + Find(&s). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, value := range s { + // check if the status returned is not empty + if value.Status.Valid { + statuses[value.Status.String] = float64(value.Count.Int32) + } + } + + return statuses, nil +} diff --git a/database/service/list_status_test.go b/database/service/list_status_test.go new file mode 100644 index 000000000..d1cb46649 --- /dev/null +++ b/database/service/list_status_test.go @@ -0,0 +1,114 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestService_Engine_ListServiceStatusCount(t *testing.T) { + // setup types + _serviceOne := testService() + _serviceOne.SetID(1) + _serviceOne.SetRepoID(1) + _serviceOne.SetBuildID(1) + _serviceOne.SetNumber(1) + _serviceOne.SetName("foo") + _serviceOne.SetImage("bar") + + _serviceTwo := testService() + _serviceTwo.SetID(2) + _serviceTwo.SetRepoID(1) + _serviceTwo.SetBuildID(1) + _serviceTwo.SetNumber(2) + _serviceTwo.SetName("foo") + _serviceTwo.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"status", "count"}). + AddRow("pending", 0). + AddRow("failure", 0). + AddRow("killed", 0). + AddRow("running", 0). + AddRow("success", 0) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT "status", count(status) as count FROM "services" GROUP BY "status"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateService(_serviceOne) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + _, err = _sqlite.CreateService(_serviceTwo) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want map[string]float64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: map[string]float64{ + "pending": 0, + "failure": 0, + "killed": 0, + "running": 0, + "success": 0, + }, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: map[string]float64{ + "pending": 0, + "failure": 0, + "killed": 0, + "running": 0, + "success": 0, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListServiceStatusCount() + + if test.failure { + if err == nil { + t.Errorf("ListServiceStatusCount for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListServiceStatusCount for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListServiceStatusCount for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/service/list_test.go b/database/service/list_test.go new file mode 100644 index 000000000..a7351a8b6 --- /dev/null +++ b/database/service/list_test.go @@ -0,0 +1,107 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestService_Engine_ListServices(t *testing.T) { + // setup types + _serviceOne := testService() + _serviceOne.SetID(1) + _serviceOne.SetRepoID(1) + _serviceOne.SetBuildID(1) + _serviceOne.SetNumber(1) + _serviceOne.SetName("foo") + _serviceOne.SetImage("bar") + + _serviceTwo := testService() + _serviceTwo.SetID(2) + _serviceTwo.SetRepoID(1) + _serviceTwo.SetBuildID(2) + _serviceTwo.SetNumber(1) + _serviceTwo.SetName("bar") + _serviceTwo.SetImage("foo") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "services"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "name", "image", "stage", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}). + AddRow(1, 1, 1, 1, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", ""). + AddRow(2, 1, 2, 1, "bar", "foo", "", "", "", 0, 0, 0, 0, "", "", "") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "services"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateService(_serviceOne) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + _, err = _sqlite.CreateService(_serviceTwo) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Service + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Service{_serviceOne, _serviceTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Service{_serviceOne, _serviceTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListServices() + + if test.failure { + if err == nil { + t.Errorf("ListServices for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListServices for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListServices for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/service/opts.go b/database/service/opts.go new file mode 100644 index 000000000..2201b1abd --- /dev/null +++ b/database/service/opts.go @@ -0,0 +1,44 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Services. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Services. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the service engine + e.client = client + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Services. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the service engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Services. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the service engine + e.config.SkipCreation = skipCreation + + return nil + } +} diff --git a/database/service/opts_test.go b/database/service/opts_test.go new file mode 100644 index 000000000..62ce5adf8 --- /dev/null +++ b/database/service/opts_test.go @@ -0,0 +1,161 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestService_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestService_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestService_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} diff --git a/database/service/service.go b/database/service/service.go new file mode 100644 index 000000000..548164c0f --- /dev/null +++ b/database/service/service.go @@ -0,0 +1,74 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the ServiceInterface interface. + config struct { + // specifies to skip creating tables and indexes for the Service engine + SkipCreation bool + } + + // engine represents the service functionality that implements the ServiceInterface interface. + engine struct { + // engine configuration settings used in service functions + config *config + + // gorm.io/gorm database client used in service functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in service functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with services in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new Service engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating service database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of services table in the database") + + return e, nil + } + + // create the services table + err := e.CreateServiceTable(e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableService, err) + } + + return e, nil +} diff --git a/database/service/service_test.go b/database/service/service_test.go new file mode 100644 index 000000000..c9d658769 --- /dev/null +++ b/database/service/service_test.go @@ -0,0 +1,246 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "database/sql/driver" + "reflect" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestService_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithClient(test.client), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres service engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite service engine: %v", err) + } + + return _engine +} + +// testBuild is a test helper function to create a library +// Build type with all fields set to their zero values. +func testBuild() *library.Build { + return &library.Build{ + ID: new(int64), + RepoID: new(int64), + PipelineID: new(int64), + Number: new(int), + Parent: new(int), + Event: new(string), + EventAction: new(string), + Status: new(string), + Error: new(string), + Enqueued: new(int64), + Created: new(int64), + Started: new(int64), + Finished: new(int64), + Deploy: new(string), + Clone: new(string), + Source: new(string), + Title: new(string), + Message: new(string), + Commit: new(string), + Sender: new(string), + Author: new(string), + Email: new(string), + Link: new(string), + Branch: new(string), + Ref: new(string), + BaseRef: new(string), + HeadRef: new(string), + Host: new(string), + Runtime: new(string), + Distribution: new(string), + } +} + +// testService is a test helper function to create a library +// Service type with all fields set to their zero values. +func testService() *library.Service { + return &library.Service{ + ID: new(int64), + BuildID: new(int64), + RepoID: new(int64), + Number: new(int), + Name: new(string), + Image: new(string), + Status: new(string), + Error: new(string), + ExitCode: new(int), + Created: new(int64), + Started: new(int64), + Finished: new(int64), + Host: new(string), + Runtime: new(string), + Distribution: new(string), + } +} + +// This will be used with the github.com/DATA-DOG/go-sqlmock library to compare values +// that are otherwise not easily compared. These typically would be values generated +// before adding or updating them in the database. +// +// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime + +// NowTimestamp is used to test whether timestamps get updated correctly to the current time with lenience. +type NowTimestamp struct{} + +// Match satisfies sqlmock.Argument interface. +func (t NowTimestamp) Match(v driver.Value) bool { + ts, ok := v.(int64) + if !ok { + return false + } + now := time.Now().Unix() + + return now-ts < 10 +} diff --git a/database/service/table.go b/database/service/table.go new file mode 100644 index 000000000..48f8dc712 --- /dev/null +++ b/database/service/table.go @@ -0,0 +1,76 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres services table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +services ( + id SERIAL PRIMARY KEY, + repo_id INTEGER, + build_id INTEGER, + number INTEGER, + name VARCHAR(250), + image VARCHAR(500), + status VARCHAR(250), + error VARCHAR(500), + exit_code INTEGER, + created INTEGER, + started INTEGER, + finished INTEGER, + host VARCHAR(250), + runtime VARCHAR(250), + distribution VARCHAR(250), + UNIQUE(build_id, number) +); +` + + // CreateSqliteTable represents a query to create the Sqlite services table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +services ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_id INTEGER, + build_id INTEGER, + number INTEGER, + name TEXT, + image TEXT, + status TEXT, + error TEXT, + exit_code INTEGER, + created INTEGER, + started INTEGER, + finished INTEGER, + host TEXT, + runtime TEXT, + distribution TEXT, + UNIQUE(build_id, number) +); +` +) + +// CreateServiceTable creates the services table in the database. +func (e *engine) CreateServiceTable(driver string) error { + e.logger.Tracef("creating services table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the services table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the services table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/service/table_test.go b/database/service/table_test.go new file mode 100644 index 000000000..1b4d1b7de --- /dev/null +++ b/database/service/table_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestService_Engine_CreateServiceTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateServiceTable(test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateServiceTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateServiceTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/service/update.go b/database/service/update.go new file mode 100644 index 000000000..d9b0bd595 --- /dev/null +++ b/database/service/update.go @@ -0,0 +1,37 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// UpdateService updates an existing service in the database. +func (e *engine) UpdateService(s *library.Service) (*library.Service, error) { + e.logger.WithFields(logrus.Fields{ + "service": s.GetNumber(), + }).Tracef("updating service %s in the database", s.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#ServiceFromLibrary + service := database.ServiceFromLibrary(s) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Service.Validate + err := service.Validate() + if err != nil { + return nil, err + } + + // send query to the database + result := e.client.Table(constants.TableService).Save(service) + + return service.ToLibrary(), result.Error +} diff --git a/database/service/update_test.go b/database/service/update_test.go new file mode 100644 index 000000000..12979dce9 --- /dev/null +++ b/database/service/update_test.go @@ -0,0 +1,80 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package service + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestService_Engine_UpdateService(t *testing.T) { + // setup types + _service := testService() + _service.SetID(1) + _service.SetRepoID(1) + _service.SetBuildID(1) + _service.SetNumber(1) + _service.SetName("foo") + _service.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`UPDATE "services" SET "build_id"=$1,"repo_id"=$2,"number"=$3,"name"=$4,"image"=$5,"status"=$6,"error"=$7,"exit_code"=$8,"created"=$9,"started"=$10,"finished"=$11,"host"=$12,"runtime"=$13,"distribution"=$14 WHERE "id" = $15`). + WithArgs(1, 1, 1, "foo", "bar", nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateService(_service) + if err != nil { + t.Errorf("unable to create test service for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.UpdateService(_service) + + if test.failure { + if err == nil { + t.Errorf("UpdateService for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateService for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _service) { + t.Errorf("UpdateService for %s returned %s, want %s", test.name, got, _service) + } + }) + } +} diff --git a/database/setup.go b/database/setup.go deleted file mode 100644 index e8f3b0516..000000000 --- a/database/setup.go +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package database - -import ( - "fmt" - "strings" - "time" - - "github.com/go-vela/server/database/postgres" - "github.com/go-vela/server/database/sqlite" - "github.com/go-vela/types/constants" - "github.com/sirupsen/logrus" -) - -// Setup represents the configuration necessary for -// creating a Vela service capable of integrating -// with a configured database system. -type Setup struct { - // Database Configuration - - // specifies the driver to use for the database client - Driver string - // specifies the address to use for the database client - Address string - // specifies the level of compression to use for the database client - CompressionLevel int - // specifies the connection duration to use for the database client - ConnectionLife time.Duration - // specifies the maximum idle connections for the database client - ConnectionIdle int - // specifies the maximum open connections for the database client - ConnectionOpen int - // specifies the encryption key to use for the database client - EncryptionKey string - // specifies to skip creating tables and indexes for the database client - SkipCreation bool -} - -// Postgres creates and returns a Vela service capable of -// integrating with a Postgres database system. -func (s *Setup) Postgres() (Service, error) { - logrus.Trace("creating postgres database client from setup") - - // create new Postgres database service - // - // https://pkg.go.dev/github.com/go-vela/server/database/postgres?tab=doc#New - return postgres.New( - postgres.WithAddress(s.Address), - postgres.WithCompressionLevel(s.CompressionLevel), - postgres.WithConnectionLife(s.ConnectionLife), - postgres.WithConnectionIdle(s.ConnectionIdle), - postgres.WithConnectionOpen(s.ConnectionOpen), - postgres.WithEncryptionKey(s.EncryptionKey), - postgres.WithSkipCreation(s.SkipCreation), - ) -} - -// Sqlite creates and returns a Vela service capable of -// integrating with a Sqlite database system. -func (s *Setup) Sqlite() (Service, error) { - logrus.Trace("creating sqlite database client from setup") - - // create new Sqlite database service - // - // https://pkg.go.dev/github.com/go-vela/server/database/sqlite?tab=doc#New - return sqlite.New( - sqlite.WithAddress(s.Address), - sqlite.WithCompressionLevel(s.CompressionLevel), - sqlite.WithConnectionLife(s.ConnectionLife), - sqlite.WithConnectionIdle(s.ConnectionIdle), - sqlite.WithConnectionOpen(s.ConnectionOpen), - sqlite.WithEncryptionKey(s.EncryptionKey), - sqlite.WithSkipCreation(s.SkipCreation), - ) -} - -// Validate verifies the necessary fields for the -// provided configuration are populated correctly. -func (s *Setup) Validate() error { - logrus.Trace("validating database setup for client") - - // verify a database driver was provided - if len(s.Driver) == 0 { - return fmt.Errorf("no database driver provided") - } - - // verify a database address was provided - if len(s.Address) == 0 { - return fmt.Errorf("no database address provided") - } - - // check if the database address has a trailing slash - if strings.HasSuffix(s.Address, "/") { - return fmt.Errorf("database address must not have trailing slash") - } - - // verify a database encryption key was provided - if len(s.EncryptionKey) == 0 { - return fmt.Errorf("no database encryption key provided") - } - - // verify the database compression level is valid - switch s.CompressionLevel { - case constants.CompressionNegOne: - fallthrough - case constants.CompressionZero: - fallthrough - case constants.CompressionOne: - fallthrough - case constants.CompressionTwo: - fallthrough - case constants.CompressionThree: - fallthrough - case constants.CompressionFour: - fallthrough - case constants.CompressionFive: - fallthrough - case constants.CompressionSix: - fallthrough - case constants.CompressionSeven: - fallthrough - case constants.CompressionEight: - fallthrough - case constants.CompressionNine: - break - default: - // nolint:lll // ignoring line length due to error message - return fmt.Errorf("database compression level must be between %d and %d - provided level: %d", constants.CompressionNegOne, constants.CompressionNine, s.CompressionLevel) - } - - // enforce AES-256 for the encryption key - explicitly check for 32 characters in the key - // - // nolint: gomnd // ignore magic number - if len(s.EncryptionKey) != 32 { - // nolint: lll // ignore long line length due to long error message - return fmt.Errorf("database encryption key must have 32 characters - provided length: %d", len(s.EncryptionKey)) - } - - // setup is valid - return nil -} diff --git a/database/sqlite/build.go b/database/sqlite/build.go deleted file mode 100644 index 5a3347b09..000000000 --- a/database/sqlite/build.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "errors" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetBuild gets a build by number and repo ID from the database. -// -// nolint: dupl // ignore similar code with hook -func (c *client) GetBuild(number int, r *library.Repo) (*library.Build, error) { - c.Logger.WithFields(logrus.Fields{ - "build": number, - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("getting build %s/%d from the database", r.GetFullName(), number) - - // variable to store query results - b := new(database.Build) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableBuild). - Raw(dml.SelectRepoBuild, r.GetID(), number). - Scan(b) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return b.ToLibrary(), result.Error -} - -// GetLastBuild gets the last build by repo ID from the database. -func (c *client) GetLastBuild(r *library.Repo) (*library.Build, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("getting last build for repo %s from the database", r.GetFullName()) - - // variable to store query results - b := new(database.Build) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableBuild). - Raw(dml.SelectLastRepoBuild, r.GetID()). - Scan(b) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - // the record will not exist if it's a new repo - return nil, nil - } - - return b.ToLibrary(), result.Error -} - -// GetLastBuildByBranch gets the last build by repo ID and branch from the database. -func (c *client) GetLastBuildByBranch(r *library.Repo, branch string) (*library.Build, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("getting last build for repo %s on branch %s from the database", r.GetFullName(), branch) - - // variable to store query results - b := new(database.Build) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableBuild). - Raw(dml.SelectLastRepoBuildByBranch, r.GetID(), branch). - Scan(b) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - // the record will not exist if it's a new repo - return nil, nil - } - - return b.ToLibrary(), result.Error -} - -// GetPendingAndRunningBuilds returns the list of pending -// and running builds within the given timeframe. -func (c *client) GetPendingAndRunningBuilds(after string) ([]*library.BuildQueue, error) { - c.Logger.Trace("getting pending and running builds from the database") - - // variable to store query results - b := new([]database.BuildQueue) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableBuild). - Raw(dml.SelectPendingAndRunningBuilds, after). - Scan(b) - - // variable we want to return - builds := []*library.BuildQueue{} - - // iterate through all query results - for _, build := range *b { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := build - - // convert query result to library type - builds = append(builds, tmp.ToLibrary()) - } - - return builds, result.Error -} - -// CreateBuild creates a new build in the database. -func (c *client) CreateBuild(b *library.Build) error { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("creating build %d in the database", b.GetNumber()) - - // cast to database type - build := database.BuildFromLibrary(b) - - // validate the necessary fields are populated - err := build.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Sqlite. - Table(constants.TableBuild). - Create(build.Crop()).Error -} - -// UpdateBuild updates a build in the database. -func (c *client) UpdateBuild(b *library.Build) error { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("updating build %d in the database", b.GetNumber()) - - // cast to database type - build := database.BuildFromLibrary(b) - - // validate the necessary fields are populated - err := build.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Sqlite. - Table(constants.TableBuild). - Save(build.Crop()).Error -} - -// DeleteBuild deletes a build by unique ID from the database. -func (c *client) DeleteBuild(id int64) error { - c.Logger.Tracef("deleting build %d in the database", id) - - // send query to the database - return c.Sqlite. - Table(constants.TableBuild). - Exec(dml.DeleteBuild, id).Error -} diff --git a/database/sqlite/build_count.go b/database/sqlite/build_count.go deleted file mode 100644 index 2a363ddcf..000000000 --- a/database/sqlite/build_count.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetBuildCount gets a count of all builds from the database. -func (c *client) GetBuildCount() (int64, error) { - c.Logger.Trace("getting count of builds from the database") - - // variable to store query results - var b int64 - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableBuild). - Raw(dml.SelectBuildsCount). - Pluck("count", &b).Error - - return b, err -} - -// GetBuildCountByStatus gets a count of all builds by status from the database. -func (c *client) GetBuildCountByStatus(status string) (int64, error) { - c.Logger.Tracef("getting count of builds by status %s from the database", status) - - // variable to store query results - var b int64 - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableBuild). - Raw(dml.SelectBuildsCountByStatus, status). - Pluck("count", &b).Error - - return b, err -} - -// GetOrgBuildCount gets the count of all builds by repo ID from the database. -func (c *client) GetOrgBuildCount(org string, filters map[string]interface{}) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "org": org, - }).Tracef("getting count of builds for org %s from the database", org) - - // variable to store query results - var b int64 - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableBuild). - Joins("JOIN repos ON builds.repo_id = repos.id and repos.org = ?", org). - Where(filters). - Count(&b).Error - - return b, err -} - -// GetRepoBuildCount gets the count of all builds by repo ID from the database. -func (c *client) GetRepoBuildCount(r *library.Repo, filters map[string]interface{}) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "name": r.GetName(), - }).Tracef("getting count of builds for repo %s from the database", r.GetFullName()) - - // variable to store query results - var b int64 - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableBuild). - Where("repo_id = ?", r.GetID()). - Where(filters). - Count(&b).Error - - return b, err -} diff --git a/database/sqlite/build_count_test.go b/database/sqlite/build_count_test.go deleted file mode 100644 index 9740aa59d..000000000 --- a/database/sqlite/build_count_test.go +++ /dev/null @@ -1,434 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the build table - err = _database.Sqlite.Exec(ddl.CreateBuildTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableBuild, err) - } -} - -func TestSqlite_Client_GetBuildCount(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - // create the builds in the database - err := _database.CreateBuild(_buildOne) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - err = _database.CreateBuild(_buildTwo) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - got, err := _database.GetBuildCount() - - if test.failure { - if err == nil { - t.Errorf("GetBuildCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetBuildCountByStatus(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetStatus("running") - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetStatus("running") - _buildTwo.SetDeployPayload(nil) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - // create the builds in the database - err := _database.CreateBuild(_buildOne) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - err = _database.CreateBuild(_buildTwo) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - got, err := _database.GetBuildCountByStatus("running") - - if test.failure { - if err == nil { - t.Errorf("GetBuildCountByStatus should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildCountByStatus returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildCountByStatus is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetOrgBuildCount(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - filters := map[string]interface{}{} - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // create the repo in the database - err := _database.CreateRepo(_repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - // create the builds in the database - err = _database.CreateBuild(_buildOne) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - err = _database.CreateBuild(_buildTwo) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - got, err := _database.GetOrgBuildCount("foo", filters) - - if test.failure { - if err == nil { - t.Errorf("GetOrgBuildCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgBuildCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgBuildCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetOrgBuildCountByEvent(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetEvent("push") - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetEvent("push") - _buildTwo.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - filters := map[string]interface{}{ - "event": "push", - } - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // create the repo in the database - err := _database.CreateRepo(_repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - // create the builds in the database - err = _database.CreateBuild(_buildOne) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - err = _database.CreateBuild(_buildTwo) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - got, err := _database.GetOrgBuildCount("foo", filters) - - if test.failure { - if err == nil { - t.Errorf("GetOrgBuildCountByEvent should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgBuildCountByEvent returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgBuildCountByEvent is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetRepoBuildCount(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - filters := map[string]interface{}{} - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // create the repo in the database - err := _database.CreateRepo(_repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - // create the builds in the database - err = _database.CreateBuild(_buildOne) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - err = _database.CreateBuild(_buildTwo) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - got, err := _database.GetRepoBuildCount(_repo, filters) - - if test.failure { - if err == nil { - t.Errorf("GetRepoBuildCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoBuildCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoBuildCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/build_list.go b/database/sqlite/build_list.go deleted file mode 100644 index e5ca79cc0..000000000 --- a/database/sqlite/build_list.go +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetBuildList gets a list of all builds from the database. -func (c *client) GetBuildList() ([]*library.Build, error) { - c.Logger.Trace("listing builds from the database") - - // variable to store query results - b := new([]database.Build) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableBuild). - Raw(dml.ListBuilds). - Scan(b).Error - - // variable we want to return - builds := []*library.Build{} - // iterate through all query results - for _, build := range *b { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := build - - // convert query result to library type - builds = append(builds, tmp.ToLibrary()) - } - - return builds, err -} - -// GetDeploymentBuildList gets a list of all builds from the database. -func (c *client) GetDeploymentBuildList(deployment string) ([]*library.Build, error) { - c.Logger.WithFields(logrus.Fields{ - "deployment": deployment, - }).Tracef("listing builds for deployment %s from the database", deployment) - - // variable to store query results - b := new([]database.Build) - filters := map[string]string{} - if len(deployment) > 0 { - filters["source"] = deployment - } - // send query to the database and store result in variable - // - // nolint: gomnd // ignore magic number - err := c.Sqlite. - Table(constants.TableBuild). - Select("*"). - Where(filters). - Limit(3). - Order("number DESC"). - Scan(b).Error - - // variable we want to return - builds := []*library.Build{} - // iterate through all query results - for _, build := range *b { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := build - - // convert query result to library type - builds = append(builds, tmp.ToLibrary()) - } - - return builds, err -} - -// GetOrgBuildList gets a list of all builds by org name from the database. -// -// nolint: lll // ignore long line length due to variable names -func (c *client) GetOrgBuildList(org string, filters map[string]interface{}, page int, perPage int) ([]*library.Build, int64, error) { - c.Logger.WithFields(logrus.Fields{ - "org": org, - }).Tracef("listing builds for org %s from the database", org) - - // variable to store query results - b := new([]database.Build) - builds := []*library.Build{} - count := int64(0) - - // // count the results - count, err := c.GetOrgBuildCount(org, filters) - - if err != nil { - return builds, 0, err - } - - // short-circuit if there are no results - if count == 0 { - return builds, 0, nil - } - - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err = c.Sqlite. - Table(constants.TableBuild). - Select("builds.*"). - Joins("JOIN repos ON builds.repo_id = repos.id AND repos.org = ?", org). - Where(filters). - Order("created DESC"). - Order("id"). - Limit(perPage). - Offset(offset). - Scan(b).Error - - // iterate through all query results - for _, build := range *b { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := build - - // convert query result to library type - builds = append(builds, tmp.ToLibrary()) - } - - return builds, count, err -} - -// GetRepoBuildList gets a list of all builds by repo ID from the database. -// -// nolint: lll // ignore long line length due to variable names -func (c *client) GetRepoBuildList(r *library.Repo, filters map[string]interface{}, page, perPage int) ([]*library.Build, int64, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("listing builds for repo %s from the database", r.GetFullName()) - - // variable to store query results - b := new([]database.Build) - builds := []*library.Build{} - count := int64(0) - - // count the results - count, err := c.GetRepoBuildCount(r, filters) - if err != nil { - return builds, 0, err - } - - // short-circuit if there are no results - if count == 0 { - return builds, 0, nil - } - - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err = c.Sqlite. - Table(constants.TableBuild). - Where("repo_id = ?", r.GetID()). - Where(filters). - Order("number DESC"). - Limit(perPage). - Offset(offset). - Scan(b).Error - - // iterate through all query results - for _, build := range *b { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := build - - // convert query result to library type - builds = append(builds, tmp.ToLibrary()) - } - - return builds, count, err -} diff --git a/database/sqlite/build_list_test.go b/database/sqlite/build_list_test.go deleted file mode 100644 index 1a07c291c..000000000 --- a/database/sqlite/build_list_test.go +++ /dev/null @@ -1,523 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the build table - err = _database.Sqlite.Exec(ddl.CreateBuildTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableBuild, err) - } -} - -func TestSqlite_Client_GetBuildList(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildOne, _buildTwo}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - for _, build := range test.want { - // create the build in the database - err := _database.CreateBuild(build) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - } - - got, err := _database.GetBuildList() - - if test.failure { - if err == nil { - t.Errorf("GetBuildList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetDeploymentBuildList(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetEvent("deployment") - _buildOne.SetDeployPayload(nil) - _buildOne.SetSource("https://github.com/github/octocat/deployments/1") - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildOne.SetEvent("deployment") - _buildTwo.SetDeployPayload(nil) - _buildTwo.SetSource("https://github.com/github/octocat/deployments/1") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildTwo, _buildOne}, - }, - } - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - for _, build := range test.want { - // create the build in the database - err := _database.CreateBuild(build) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - } - - got, err := _database.GetDeploymentBuildList("https://github.com/github/octocat/deployments/1") - - if test.failure { - if err == nil { - t.Errorf("GetDeploymentBuildList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetDeploymentBuildList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetDeploymentBuildList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetOrgBuildList(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetEvent("push") - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildOne.SetEvent("deployment") - _buildTwo.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildOne, _buildTwo}, - }, - } - - filters := map[string]interface{}{} - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // create the repo in the database - err := _database.CreateRepo(_repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - for _, build := range test.want { - // create the build in the database - err := _database.CreateBuild(build) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - } - - got, _, err := _database.GetOrgBuildList("foo", filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetOrgBuildList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgBuildList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgBuildList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetOrgBuildList_NonAdmin(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(2) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - - _repoTwo := testRepo() - _repoTwo.SetID(2) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("private") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildOne}, - }, - } - - filters := map[string]interface{}{} - - repos := []*library.Repo{_repoOne, _repoTwo} - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - for _, repo := range repos { - // create the repo in the database - err := _database.CreateRepo(repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - } - - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - for _, build := range test.want { - // create the build in the database - err := _database.CreateBuild(build) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - } - - got, _, err := _database.GetOrgBuildList("foo", filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetOrgBuildList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgBuildList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgBuildList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetOrgBuildListByEvent(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetEvent("push") - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetEvent("deployment") - _buildTwo.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildOne}, - }, - } - - filters := map[string]interface{}{ - "event": "push", - } - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // create the repo in the database - err := _database.CreateRepo(_repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - for _, build := range []*library.Build{_buildTwo, _buildOne} { - // create the build in the database - err := _database.CreateBuild(build) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - } - - got, _, err := _database.GetOrgBuildList("foo", filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetOrgBuildListByEvent should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgBuildListByEvent returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgBuildListByEvent is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetRepoBuildList(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Build - }{ - { - failure: false, - want: []*library.Build{_buildTwo, _buildOne}, - }, - } - - filters := map[string]interface{}{} - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // create the repo in the database - err := _database.CreateRepo(_repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - for _, build := range test.want { - // create the build in the database - err := _database.CreateBuild(build) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - } - - got, _, err := _database.GetRepoBuildList(_repo, filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetRepoBuildList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoBuildList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoBuildList is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/build_test.go b/database/sqlite/build_test.go deleted file mode 100644 index 4cb95fccb..000000000 --- a/database/sqlite/build_test.go +++ /dev/null @@ -1,538 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/library" -) - -func TestSqlite_Client_GetBuild(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - _build.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Build - }{ - { - failure: false, - want: _build, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the build in the database - err := _database.CreateBuild(test.want) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - } - - got, err := _database.GetBuild(1, _repo) - - // cleanup the builds table - _ = _database.Sqlite.Exec("DELETE FROM builds;") - - if test.failure { - if err == nil { - t.Errorf("GetBuild should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuild returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuild is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetLastBuild(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - _build.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Build - }{ - { - failure: false, - want: _build, - }, - { - failure: false, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the build in the database - err := _database.CreateBuild(test.want) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - } - - got, err := _database.GetLastBuild(_repo) - - // cleanup the builds table - _ = _database.Sqlite.Exec("DELETE FROM builds;") - - if test.failure { - if err == nil { - t.Errorf("GetLastBuild should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetLastBuild returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetLastBuild is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetLastBuildByBranch(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - _build.SetBranch("master") - _build.SetDeployPayload(nil) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Build - }{ - { - failure: false, - want: _build, - }, - { - failure: false, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the build in the database - err := _database.CreateBuild(test.want) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - } - - got, err := _database.GetLastBuildByBranch(_repo, "master") - - // cleanup the builds table - _ = _database.Sqlite.Exec("DELETE FROM builds;") - - if test.failure { - if err == nil { - t.Errorf("GetLastBuildByBranch should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetLastBuildByBranch returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetLastBuildByBranch is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetPendingAndRunningBuilds(t *testing.T) { - // setup types - _buildOne := testBuild() - _buildOne.SetID(1) - _buildOne.SetRepoID(1) - _buildOne.SetNumber(1) - _buildOne.SetStatus("running") - _buildOne.SetCreated(1) - _buildOne.SetDeployPayload(nil) - - _buildTwo := testBuild() - _buildTwo.SetID(2) - _buildTwo.SetRepoID(1) - _buildTwo.SetNumber(2) - _buildTwo.SetStatus("pending") - _buildTwo.SetCreated(1) - _buildTwo.SetDeployPayload(nil) - - _queueOne := new(library.BuildQueue) - _queueOne.SetCreated(1) - _queueOne.SetFullName("foo/bar") - _queueOne.SetNumber(1) - _queueOne.SetStatus("running") - - _queueTwo := new(library.BuildQueue) - _queueTwo.SetCreated(1) - _queueTwo.SetFullName("foo/bar") - _queueTwo.SetNumber(2) - _queueTwo.SetStatus("pending") - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.BuildQueue - }{ - { - failure: false, - want: []*library.BuildQueue{_queueOne, _queueTwo}, - }, - { - failure: false, - want: []*library.BuildQueue{}, - }, - } - - // run tests - for _, test := range tests { - // create the repo in the database - err := _database.CreateRepo(_repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - if len(test.want) > 0 { - // create the builds in the database - err = _database.CreateBuild(_buildOne) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - err = _database.CreateBuild(_buildTwo) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - } - - got, err := _database.GetPendingAndRunningBuilds("0") - - // cleanup the repos table - _ = _database.Sqlite.Exec("DELETE FROM repos;") - // cleanup the builds table - _ = _database.Sqlite.Exec("DELETE FROM builds;") - - if test.failure { - if err == nil { - t.Errorf("GetPendingAndRunningBuilds should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetPendingAndRunningBuilds returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetPendingAndRunningBuilds is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_CreateBuild(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - err := _database.CreateBuild(_build) - - if test.failure { - if err == nil { - t.Errorf("CreateBuild should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateBuild returned err: %v", err) - } - } -} - -func TestSqlite_Client_UpdateBuild(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - // create the build in the database - err = _database.CreateBuild(_build) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - err := _database.UpdateBuild(_build) - - if test.failure { - if err == nil { - t.Errorf("UpdateBuild should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateBuild returned err: %v", err) - } - } -} - -func TestSqlite_Client_DeleteBuild(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the builds table - defer _database.Sqlite.Exec("delete from builds;") - - // create the build in the database - err = _database.CreateBuild(_build) - if err != nil { - t.Errorf("unable to create test build: %v", err) - } - - err = _database.DeleteBuild(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteBuild should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteBuild returned err: %v", err) - } - } -} - -// testBuild is a test helper function to create a -// library Build type with all fields set to their -// zero values. -func testBuild() *library.Build { - i64 := int64(0) - i := 0 - str := "" - - return &library.Build{ - ID: &i64, - RepoID: &i64, - Number: &i, - Parent: &i, - Event: &str, - Status: &str, - Error: &str, - Enqueued: &i64, - Created: &i64, - Started: &i64, - Finished: &i64, - Deploy: &str, - Clone: &str, - Source: &str, - Title: &str, - Message: &str, - Commit: &str, - Sender: &str, - Author: &str, - Email: &str, - Link: &str, - Branch: &str, - Ref: &str, - BaseRef: &str, - HeadRef: &str, - Host: &str, - Runtime: &str, - Distribution: &str, - } -} diff --git a/database/sqlite/ddl/build.go b/database/sqlite/ddl/build.go deleted file mode 100644 index d9ed4ff86..000000000 --- a/database/sqlite/ddl/build.go +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateBuildTable represents a query to - // create the builds table for Vela. - CreateBuildTable = ` -CREATE TABLE -IF NOT EXISTS -builds ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - repo_id INTEGER, - number INTEGER, - parent INTEGER, - event TEXT, - status TEXT, - error TEXT, - enqueued INTEGER, - created INTEGER, - started INTEGER, - finished INTEGER, - deploy TEXT, - deploy_payload TEXT, - clone TEXT, - source TEXT, - title TEXT, - message TEXT, - 'commit' TEXT, - sender TEXT, - author TEXT, - email TEXT, - link TEXT, - branch TEXT, - ref TEXT, - base_ref TEXT, - head_ref TEXT, - host TEXT, - runtime TEXT, - distribution TEXT, - timestamp INTEGER, - UNIQUE(repo_id, number) -); -` - - // CreateBuildRepoIDIndex represents a query to create an - // index on the builds table for the repo_id column. - CreateBuildRepoIDIndex = ` -CREATE INDEX -IF NOT EXISTS -builds_repo_id -ON builds (repo_id); -` - - // CreateBuildStatusIndex represents a query to create an - // index on the builds table for the status column. - CreateBuildStatusIndex = ` -CREATE INDEX -IF NOT EXISTS -builds_status -ON builds (status); -` - - // CreateBuildCreatedIndex represents a query to create an - // index on the builds table for the created column. - CreateBuildCreatedIndex = ` -CREATE INDEX -IF NOT EXISTS -builds_created -ON builds (created); -` -) diff --git a/database/sqlite/ddl/doc.go b/database/sqlite/ddl/doc.go deleted file mode 100644 index fb2942402..000000000 --- a/database/sqlite/ddl/doc.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -// Package ddl provides the Sqlite data definition language (DDL) for Vela. -// -// https://en.wikipedia.org/wiki/Data_definition_language -// -// Usage: -// -// import "github.com/go-vela/server/database/sqlite/ddl" -package ddl diff --git a/database/sqlite/ddl/hook.go b/database/sqlite/ddl/hook.go deleted file mode 100644 index 80b3397bc..000000000 --- a/database/sqlite/ddl/hook.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateHookTable represents a query to - // create the hooks table for Vela. - CreateHookTable = ` -CREATE TABLE -IF NOT EXISTS -hooks ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - repo_id INTEGER, - build_id INTEGER, - number INTEGER, - source_id TEXT, - created INTEGER, - host TEXT, - event TEXT, - branch TEXT, - error TEXT, - status TEXT, - link TEXT, - UNIQUE(repo_id, build_id) -); -` - - // CreateHookRepoIDIndex represents a query to create an - // index on the hooks table for the repo_id column. - CreateHookRepoIDIndex = ` -CREATE INDEX -IF NOT EXISTS -hooks_repo_id -ON hooks (repo_id); -` -) diff --git a/database/sqlite/ddl/log.go b/database/sqlite/ddl/log.go deleted file mode 100644 index 225155bfb..000000000 --- a/database/sqlite/ddl/log.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateLogTable represents a query to - // create the logs table for Vela. - CreateLogTable = ` -CREATE TABLE -IF NOT EXISTS -logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - build_id INTEGER, - repo_id INTEGER, - service_id INTEGER, - step_id INTEGER, - data BLOB, - UNIQUE(step_id), - UNIQUE(service_id) -); -` - - // CreateLogBuildIDIndex represents a query to create an - // index on the logs table for the build_id column. - CreateLogBuildIDIndex = ` -CREATE INDEX -IF NOT EXISTS -logs_build_id -ON logs (build_id); -` -) diff --git a/database/sqlite/ddl/repo.go b/database/sqlite/ddl/repo.go deleted file mode 100644 index 2fcb723c1..000000000 --- a/database/sqlite/ddl/repo.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateRepoTable represents a query to - // create the repos table for Vela. - CreateRepoTable = ` -CREATE TABLE -IF NOT EXISTS -repos ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER, - hash TEXT, - org TEXT, - name TEXT, - full_name TEXT, - link TEXT, - clone TEXT, - branch TEXT, - build_limit INTEGER, - timeout INTEGER, - counter INTEGER, - visibility TEXT, - private BOOLEAN, - trusted BOOLEAN, - active BOOLEAN, - allow_pull BOOLEAN, - allow_push BOOLEAN, - allow_deploy BOOLEAN, - allow_tag BOOLEAN, - allow_comment BOOLEAN, - pipeline_type TEXT, - previous_name TEXT, - UNIQUE(full_name) -); -` - - // CreateRepoOrgNameIndex represents a query to create an - // index on the repos table for the org and name columns. - CreateRepoOrgNameIndex = ` -CREATE INDEX -IF NOT EXISTS -repos_org_name -ON repos (org, name); -` -) diff --git a/database/sqlite/ddl/secret.go b/database/sqlite/ddl/secret.go deleted file mode 100644 index 4348ec1de..000000000 --- a/database/sqlite/ddl/secret.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateSecretTable represents a query to - // create the secrets table for Vela. - CreateSecretTable = ` -CREATE TABLE -IF NOT EXISTS -secrets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - type TEXT, - org TEXT, - repo TEXT, - team TEXT, - name TEXT, - value TEXT, - images TEXT, - events TEXT, - allow_command BOOLEAN, - created_at INTEGER, - created_by TEXT, - updated_at INTEGER, - updated_by TEXT, - UNIQUE(type, org, repo, name), - UNIQUE(type, org, team, name) -); -` - - // CreateSecretTypeOrgRepo represents a query to create an - // index on the secrets table for the type, org and repo columns. - // - // nolint: gosec // ignore false positive - CreateSecretTypeOrgRepo = ` -CREATE INDEX -IF NOT EXISTS -secrets_type_org_repo -ON secrets (type, org, repo); -` - - // CreateSecretTypeOrgTeam represents a query to create an - // index on the secrets table for the type, org and team columns. - // - // nolint: gosec // ignore false positive - CreateSecretTypeOrgTeam = ` -CREATE INDEX -IF NOT EXISTS -secrets_type_org_team -ON secrets (type, org, team); -` - - // CreateSecretTypeOrg represents a query to create an - // index on the secrets table for the type, and org columns. - // - // nolint: gosec // ignore false positive - CreateSecretTypeOrg = ` -CREATE INDEX -IF NOT EXISTS -secrets_type_org -ON secrets (type, org); -` -) diff --git a/database/sqlite/ddl/service.go b/database/sqlite/ddl/service.go deleted file mode 100644 index 3b8f9527e..000000000 --- a/database/sqlite/ddl/service.go +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateServiceTable represents a query to - // create the services table for Vela. - CreateServiceTable = ` -CREATE TABLE -IF NOT EXISTS -services ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - repo_id INTEGER, - build_id INTEGER, - number INTEGER, - name TEXT, - image TEXT, - status TEXT, - error TEXT, - exit_code INTEGER, - created INTEGER, - started INTEGER, - finished INTEGER, - host TEXT, - runtime TEXT, - distribution TEXT, - UNIQUE(build_id, number) -); -` -) diff --git a/database/sqlite/ddl/step.go b/database/sqlite/ddl/step.go deleted file mode 100644 index 7809dcc3f..000000000 --- a/database/sqlite/ddl/step.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateStepTable represents a query to - // create the steps table for Vela. - CreateStepTable = ` -CREATE TABLE -IF NOT EXISTS -steps ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - repo_id INTEGER, - build_id INTEGER, - number INTEGER, - name TEXT, - image TEXT, - stage TEXT, - status TEXT, - error TEXT, - exit_code INTEGER, - created INTEGER, - started INTEGER, - finished INTEGER, - host TEXT, - runtime TEXT, - distribution TEXT, - UNIQUE(build_id, number) -); -` -) diff --git a/database/sqlite/ddl/user.go b/database/sqlite/ddl/user.go deleted file mode 100644 index 3f8d77e90..000000000 --- a/database/sqlite/ddl/user.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateUserTable represents a query to - // create the users table for Vela. - CreateUserTable = ` -CREATE TABLE -IF NOT EXISTS -users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT, - refresh_token TEXT, - token TEXT, - hash TEXT, - favorites TEXT, - active BOOLEAN, - admin BOOLEAN, - UNIQUE(name) -); -` - - // CreateUserRefreshIndex represents a query to create an - // index on the users table for the refresh_token column. - CreateUserRefreshIndex = ` -CREATE INDEX -IF NOT EXISTS -users_refresh -ON users (refresh_token); -` -) diff --git a/database/sqlite/ddl/worker.go b/database/sqlite/ddl/worker.go deleted file mode 100644 index 0e5ba8f4a..000000000 --- a/database/sqlite/ddl/worker.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateWorkerTable represents a query to - // create the workers table for Vela. - CreateWorkerTable = ` -CREATE TABLE -IF NOT EXISTS -workers ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - hostname TEXT, - address TEXT, - routes TEXT, - active TEXT, - last_checked_in INTEGER, - build_limit INTEGER, - UNIQUE(hostname) -); -` - - // CreateWorkerHostnameAddressIndex represents a query to create an - // index on the workers table for the hostname and address columns. - CreateWorkerHostnameAddressIndex = ` -CREATE INDEX -IF NOT EXISTS -workers_hostname_address -ON workers (hostname, address); -` -) diff --git a/database/sqlite/dml/build.go b/database/sqlite/dml/build.go deleted file mode 100644 index ff8863e8c..000000000 --- a/database/sqlite/dml/build.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListBuilds represents a query to - // list all builds in the database. - ListBuilds = ` -SELECT * -FROM builds; -` - - // SelectRepoBuild represents a query to select - // a build for a repo_id in the database. - SelectRepoBuild = ` -SELECT * -FROM builds -WHERE repo_id = ? -AND number = ? -LIMIT 1; -` - - // SelectLastRepoBuild represents a query to select - // the last build for a repo_id in the database. - SelectLastRepoBuild = ` -SELECT * -FROM builds -WHERE repo_id = ? -ORDER BY number DESC -LIMIT 1; -` - // SelectLastRepoBuildByBranch represents a query to - // select the last build for a repo_id and branch name - // in the database. - SelectLastRepoBuildByBranch = ` -SELECT * -FROM builds -WHERE repo_id = ? -AND branch = ? -ORDER BY number DESC -LIMIT 1; -` - - // SelectBuildsCount represents a query to select - // the count of builds in the database. - SelectBuildsCount = ` -SELECT count(*) as count -FROM builds; -` - - // SelectBuildsCountByStatus represents a query to select - // the count of builds for a status in the database. - SelectBuildsCountByStatus = ` -SELECT count(*) as count -FROM builds -WHERE status = ?; -` - - // DeleteBuild represents a query to - // remove a build from the database. - DeleteBuild = ` -DELETE -FROM builds -WHERE id = ?; -` - - // SelectPendingAndRunningBuilds represents a joined query - // between the builds & repos table to select - // the created builds that are in pending or running builds status - // since the specified timeframe. - SelectPendingAndRunningBuilds = ` -SELECT builds.created, builds.number, builds.status, repos.full_name -FROM builds INNER JOIN repos -ON builds.repo_id = repos.id -WHERE builds.created > ? -AND (builds.status = 'running' OR builds.status = 'pending'); -` -) diff --git a/database/sqlite/dml/doc.go b/database/sqlite/dml/doc.go deleted file mode 100644 index fb4a8ecba..000000000 --- a/database/sqlite/dml/doc.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -// Package dml provides the Sqlite data manipulation language (DML) for Vela. -// -// https://en.wikipedia.org/wiki/Data_manipulation_language -// -// Usage: -// -// import "github.com/go-vela/server/database/sqlite/dml" -package dml diff --git a/database/sqlite/dml/hook.go b/database/sqlite/dml/hook.go deleted file mode 100644 index b06d79ad5..000000000 --- a/database/sqlite/dml/hook.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListHooks represents a query to - // list all webhooks in the database. - ListHooks = ` -SELECT * -FROM hooks; -` - - // ListRepoHooks represents a query to list - // all webhooks for a repo_id in the database. - ListRepoHooks = ` -SELECT * -FROM hooks -WHERE repo_id = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectRepoHookCount represents a query to select - // the count of webhooks for a repo_id in the database. - SelectRepoHookCount = ` -SELECT count(*) as count -FROM hooks -WHERE repo_id = ?; -` - - // SelectRepoHook represents a query to select - // a webhook for a repo_id in the database. - SelectRepoHook = ` -SELECT * -FROM hooks -WHERE repo_id = ? -AND number = ? -LIMIT 1; -` - - // SelectLastRepoHook represents a query to select - // the last hook for a repo_id in the database. - SelectLastRepoHook = ` -SELECT * -FROM hooks -WHERE repo_id = ? -ORDER BY number DESC -LIMIT 1; -` - - // DeleteHook represents a query to - // remove a webhook from the database. - DeleteHook = ` -DELETE -FROM hooks -WHERE id = ?; -` -) diff --git a/database/sqlite/dml/log.go b/database/sqlite/dml/log.go deleted file mode 100644 index e3e904cc4..000000000 --- a/database/sqlite/dml/log.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListLogs represents a query to - // list all logs in the database. - ListLogs = ` -SELECT * -FROM logs; -` - - // ListBuildLogs represents a query to list - // all logs for a build_id in the database. - ListBuildLogs = ` -SELECT * -FROM logs -WHERE build_id = ? -ORDER BY step_id ASC; -` - - // SelectStepLog represents a query to select - // a log for a step_id in the database. - SelectStepLog = ` -SELECT * -FROM logs -WHERE step_id = ? -LIMIT 1; -` - - // SelectServiceLog represents a query to select - // a log for a service_id in the database. - SelectServiceLog = ` -SELECT * -FROM logs -WHERE service_id = ? -LIMIT 1; -` - - // DeleteLog represents a query to - // remove a log from the database. - DeleteLog = ` -DELETE -FROM logs -WHERE id = ?; -` -) diff --git a/database/sqlite/dml/repo.go b/database/sqlite/dml/repo.go deleted file mode 100644 index 3de708b3e..000000000 --- a/database/sqlite/dml/repo.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListRepos represents a query to - // list all repos in the database. - ListRepos = ` -SELECT * -FROM repos; -` - - // ListUserRepos represents a query to list - // all repos for a user_id in the database. - ListUserRepos = ` -SELECT * -FROM repos -WHERE user_id = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectRepo represents a query to select a - // repo for an org and name in the database. - SelectRepo = ` -SELECT * -FROM repos -WHERE org = ? -AND name = ? -LIMIT 1; -` - - // SelectUserReposCount represents a query to select - // the count of repos for a user_id in the database. - SelectUserReposCount = ` -SELECT count(*) as count -FROM repos -WHERE user_id = ?; -` - - // SelectReposCount represents a query to select - // the count of repos in the database. - SelectReposCount = ` -SELECT count(*) as count -FROM repos; -` - - // DeleteRepo represents a query to - // remove a repo from the database. - DeleteRepo = ` -DELETE -FROM repos -WHERE id = ?; -` -) diff --git a/database/sqlite/dml/secret.go b/database/sqlite/dml/secret.go deleted file mode 100644 index 9fe094a51..000000000 --- a/database/sqlite/dml/secret.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListSecrets represents a query to - // list all secrets in the database. - // - // nolint: gosec // ignore false positive - ListSecrets = ` -SELECT * -FROM secrets; -` - - // ListOrgSecrets represents a query to list all - // secrets for a type and org in the database. - // - // nolint: gosec // ignore false positive - ListOrgSecrets = ` -SELECT * -FROM secrets -WHERE type = 'org' -AND org = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // ListRepoSecrets represents a query to list all - // secrets for a type, org and repo in the database. - // - // nolint: gosec // ignore false positive - ListRepoSecrets = ` -SELECT * -FROM secrets -WHERE type = 'repo' -AND org = ? -AND repo = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // ListSharedSecrets represents a query to list all - // secrets for a type, org and team in the database. - // - // nolint: gosec // ignore false positive - ListSharedSecrets = ` -SELECT * -FROM secrets -WHERE type = 'shared' -AND org = ? -AND team = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectOrgSecretsCount represents a query to select the - // count of org secrets for an org in the database. - // - // nolint: gosec // ignore false positive - SelectOrgSecretsCount = ` -SELECT count(*) as count -FROM secrets -WHERE type = 'org' -AND org = ?; -` - - // SelectRepoSecretsCount represents a query to select the - // count of repo secrets for an org and repo in the database. - // - // nolint: gosec // ignore false positive - SelectRepoSecretsCount = ` -SELECT count(*) as count -FROM secrets -WHERE type = 'repo' -AND org = ? -AND repo = ?; -` - - // SelectSharedSecretsCount represents a query to select the - // count of shared secrets for an org and repo in the database. - // - // nolint: gosec // ignore false positive - SelectSharedSecretsCount = ` -SELECT count(*) as count -FROM secrets -WHERE type = 'shared' -AND org = ? -AND team = ?; -` - - // SelectOrgSecret represents a query to select a - // secret for an org and name in the database. - // - // nolint: gosec // ignore false positive - SelectOrgSecret = ` -SELECT * -FROM secrets -WHERE type = 'org' -AND org = ? -AND name = ? -LIMIT 1; -` - - // SelectRepoSecret represents a query to select a - // secret for an org, repo and name in the database. - // - // nolint: gosec // ignore false positive - SelectRepoSecret = ` -SELECT * -FROM secrets -WHERE type = 'repo' -AND org = ? -AND repo = ? -AND name = ? -LIMIT 1; -` - - // SelectSharedSecret represents a query to select a - // secret for an org, team and name in the database. - // - // nolint: gosec // ignore false positive - SelectSharedSecret = ` -SELECT * -FROM secrets -WHERE type = 'shared' -AND org = ? -AND team = ? -AND name = ? -LIMIT 1; -` - - // DeleteSecret represents a query to - // remove a secret from the database. - // - // nolint: gosec // ignore false positive - DeleteSecret = ` -DELETE -FROM secrets -WHERE id = ?; -` -) diff --git a/database/sqlite/dml/service.go b/database/sqlite/dml/service.go deleted file mode 100644 index 66e009406..000000000 --- a/database/sqlite/dml/service.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListServices represents a query to - // list all services in the database. - ListServices = ` -SELECT * -FROM services; -` - - // ListBuildServices represents a query to list - // all services for a build_id in the database. - ListBuildServices = ` -SELECT * -FROM services -WHERE build_id = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectBuildServicesCount represents a query to select - // the count of services for a build_id in the database. - SelectBuildServicesCount = ` -SELECT count(*) as count -FROM services -WHERE build_id = ? -` - - // SelectServiceImagesCount represents a query to select - // the count of an images appearances in the database. - SelectServiceImagesCount = ` -SELECT image, count(image) as count -FROM services -GROUP BY image -` - - // SelectServiceStatusesCount represents a query to select - // the count of service status appearances in the database. - SelectServiceStatusesCount = ` -SELECT status, count(status) as count -FROM services -GROUP BY status; -` - - // SelectBuildService represents a query to select a - // service for a build_id and number in the database. - SelectBuildService = ` -SELECT * -FROM services -WHERE build_id = ? -AND number = ? -LIMIT 1; -` - - // DeleteService represents a query to - // remove a service from the database. - DeleteService = ` -DELETE -FROM services -WHERE id = ?; -` -) diff --git a/database/sqlite/dml/step.go b/database/sqlite/dml/step.go deleted file mode 100644 index 862ea2a67..000000000 --- a/database/sqlite/dml/step.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListSteps represents a query to - // list all steps in the database. - ListSteps = ` -SELECT * -FROM steps; -` - - // ListBuildSteps represents a query to list - // all steps for a build_id in the database. - ListBuildSteps = ` -SELECT * -FROM steps -WHERE build_id = ? -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectBuildStepsCount represents a query to select - // the count of steps for a build_id in the database. - SelectBuildStepsCount = ` -SELECT count(*) as count -FROM steps -WHERE build_id = ? -` - - // SelectStepImagesCount represents a query to select - // the count of an images appearances in the database. - SelectStepImagesCount = ` -SELECT image, count(image) as count -FROM steps -GROUP BY image; -` - - // SelectStepStatusesCount represents a query to select - // the count of step status' appearances in the database. - SelectStepStatusesCount = ` -SELECT status, count(status) as count -FROM steps -GROUP BY status; -` - - // SelectBuildStep represents a query to select a - // step for a build_id and number in the database. - SelectBuildStep = ` -SELECT * -FROM steps -WHERE build_id = ? -AND number = ? -LIMIT 1; -` - - // DeleteStep represents a query to - // remove a step from the database. - DeleteStep = ` -DELETE -FROM steps -WHERE id = ?; -` -) diff --git a/database/sqlite/dml/user.go b/database/sqlite/dml/user.go deleted file mode 100644 index 9febce658..000000000 --- a/database/sqlite/dml/user.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListUsers represents a query to - // list all users in the database. - ListUsers = ` -SELECT * -FROM users; -` - - // ListLiteUsers represents a query to - // list all lite users in the database. - ListLiteUsers = ` -SELECT id, name -FROM users -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectUser represents a query to select - // a user for an id in the database. - SelectUser = ` -SELECT * -FROM users -WHERE id = ? -LIMIT 1; -` - - // SelectUserName represents a query to select - // a user for a name in the database. - SelectUserName = ` -SELECT * -FROM users -WHERE name = ? -LIMIT 1; -` - - // SelectUsersCount represents a query to select - // the count of users in the database. - SelectUsersCount = ` -SELECT count(*) as count -FROM users; -` - - // SelectRefreshToken represents a query to select - // a user for a refresh_token in the database. - // - // nolint: gosec // ignore false positive - SelectRefreshToken = ` -SELECT * -FROM users -WHERE refresh_token = ? -LIMIT 1; -` - - // DeleteUser represents a query to - // remove a user from the database. - DeleteUser = ` -DELETE -FROM users -WHERE id = ?; -` -) diff --git a/database/sqlite/dml/worker.go b/database/sqlite/dml/worker.go deleted file mode 100644 index 64967c9b9..000000000 --- a/database/sqlite/dml/worker.go +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListWorkers represents a query to - // list all workers in the database. - ListWorkers = ` -SELECT * -FROM workers; -` - - // SelectWorkersCount represents a query to select the - // count of workers in the database. - SelectWorkersCount = ` -SELECT count(*) as count -FROM workers; -` - - // SelectWorker represents a query to select a - // worker in the database. - SelectWorker = ` -SELECT * -FROM workers -WHERE hostname = ? -LIMIT 1; -` - - // SelectWorkerByAddress represents a query to select a - // worker by address in the database. - SelectWorkerByAddress = ` -SELECT * -FROM workers -WHERE address = ? -LIMIT 1; -` - - // DeleteWorker represents a query to - // remove a worker from the database. - DeleteWorker = ` -DELETE -FROM workers -WHERE id = ?; -` -) diff --git a/database/sqlite/doc.go b/database/sqlite/doc.go deleted file mode 100644 index c0ceaf5aa..000000000 --- a/database/sqlite/doc.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -// Package sqlite provides the ability for Vela to -// integrate with Sqlite as a SQL backend. -// -// Usage: -// -// import "github.com/go-vela/server/database/sqlite" -package sqlite diff --git a/database/sqlite/driver.go b/database/sqlite/driver.go deleted file mode 100644 index 208853dd5..000000000 --- a/database/sqlite/driver.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import "github.com/go-vela/types/constants" - -// Driver outputs the configured database driver. -func (c *client) Driver() string { - return constants.DriverSqlite -} diff --git a/database/sqlite/driver_test.go b/database/sqlite/driver_test.go deleted file mode 100644 index 92a80c129..000000000 --- a/database/sqlite/driver_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/constants" -) - -func TestSqlite_Client_Driver(t *testing.T) { - // setup types - want := constants.DriverSqlite - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // run test - got := _database.Driver() - - if !reflect.DeepEqual(got, want) { - t.Errorf("Driver is %v, want %v", got, want) - } -} diff --git a/database/sqlite/hook.go b/database/sqlite/hook.go deleted file mode 100644 index 5ebd054e3..000000000 --- a/database/sqlite/hook.go +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "errors" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetHook gets a hook by number and repo ID from the database. -// -// nolint: dupl // ignore similar code with build -func (c *client) GetHook(number int, r *library.Repo) (*library.Hook, error) { - c.Logger.WithFields(logrus.Fields{ - "hook": number, - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("getting hook %s/%d from the database", r.GetFullName(), number) - - // variable to store query results - h := new(database.Hook) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableHook). - Raw(dml.SelectRepoHook, r.GetID(), number). - Scan(h) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return h.ToLibrary(), result.Error -} - -// GetLastHook gets the last hook by repo ID from the database. -func (c *client) GetLastHook(r *library.Repo) (*library.Hook, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("getting last hook for repo %s from the database", r.GetFullName()) - - // variable to store query results - h := new(database.Hook) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableHook). - Raw(dml.SelectLastRepoHook, r.GetID()). - Scan(h) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - // the record will not exist if it's a new repo - return nil, nil - } - - return h.ToLibrary(), result.Error -} - -// CreateHook creates a new hook in the database. -func (c *client) CreateHook(h *library.Hook) error { - c.Logger.WithFields(logrus.Fields{ - "hook": h.GetNumber(), - }).Tracef("creating hook %d in the database", h.GetNumber()) - - // cast to database type - hook := database.HookFromLibrary(h) - - // validate the necessary fields are populated - err := hook.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Sqlite. - Table(constants.TableHook). - Create(hook).Error -} - -// UpdateHook updates a hook in the database. -func (c *client) UpdateHook(h *library.Hook) error { - c.Logger.WithFields(logrus.Fields{ - "hook": h.GetNumber(), - }).Tracef("updating hook %d in the database", h.GetNumber()) - - // cast to database type - hook := database.HookFromLibrary(h) - - // validate the necessary fields are populated - err := hook.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Sqlite. - Table(constants.TableHook). - Save(hook).Error -} - -// DeleteHook deletes a hook by unique ID from the database. -func (c *client) DeleteHook(id int64) error { - c.Logger.Tracef("deleting hook %d in the database", id) - - // send query to the database - return c.Sqlite. - Table(constants.TableHook). - Exec(dml.DeleteHook, id).Error -} diff --git a/database/sqlite/hook_count_test.go b/database/sqlite/hook_count_test.go deleted file mode 100644 index 8eea9515d..000000000 --- a/database/sqlite/hook_count_test.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the hook table - err = _database.Sqlite.Exec(ddl.CreateHookTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableHook, err) - } -} - -func TestSqlite_Client_GetRepoHookCount(t *testing.T) { - // setup types - _hookOne := testHook() - _hookOne.SetID(1) - _hookOne.SetRepoID(1) - _hookOne.SetBuildID(1) - _hookOne.SetNumber(1) - _hookOne.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - _hookTwo := testHook() - _hookTwo.SetID(2) - _hookTwo.SetRepoID(1) - _hookTwo.SetBuildID(2) - _hookTwo.SetNumber(2) - _hookTwo.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the hooks table - defer _database.Sqlite.Exec("delete from hooks;") - - // create the hooks in the database - err := _database.CreateHook(_hookOne) - if err != nil { - t.Errorf("unable to create test hook: %v", err) - } - - err = _database.CreateHook(_hookTwo) - if err != nil { - t.Errorf("unable to create test hook: %v", err) - } - - got, err := _database.GetRepoHookCount(_repo) - - if test.failure { - if err == nil { - t.Errorf("GetRepoHookCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoHookCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoHookCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/hook_list.go b/database/sqlite/hook_list.go deleted file mode 100644 index ca388254b..000000000 --- a/database/sqlite/hook_list.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetHookList gets a list of all hooks from the database. -func (c *client) GetHookList() ([]*library.Hook, error) { - c.Logger.Trace("listing hooks from the database") - - // variable to store query results - h := new([]database.Hook) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableHook). - Raw(dml.ListHooks). - Scan(h).Error - - // variable we want to return - hooks := []*library.Hook{} - // iterate through all query results - for _, hook := range *h { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := hook - - // convert query result to library type - hooks = append(hooks, tmp.ToLibrary()) - } - - return hooks, err -} - -// GetRepoHookList gets a list of hooks by repo ID from the database. -func (c *client) GetRepoHookList(r *library.Repo, page, perPage int) ([]*library.Hook, error) { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("listing hooks for repo %s from the database", r.GetFullName()) - - // variable to store query results - h := new([]database.Hook) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableHook). - Raw(dml.ListRepoHooks, r.GetID(), perPage, offset). - Scan(h).Error - - // variable we want to return - hooks := []*library.Hook{} - // iterate through all query results - for _, hook := range *h { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := hook - - // convert query result to library type - hooks = append(hooks, tmp.ToLibrary()) - } - - return hooks, err -} diff --git a/database/sqlite/hook_list_test.go b/database/sqlite/hook_list_test.go deleted file mode 100644 index ed44e5945..000000000 --- a/database/sqlite/hook_list_test.go +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the hook table - err = _database.Sqlite.Exec(ddl.CreateHookTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableHook, err) - } -} - -func TestSqlite_Client_GetHookList(t *testing.T) { - // setup types - _hookOne := testHook() - _hookOne.SetID(1) - _hookOne.SetRepoID(1) - _hookOne.SetBuildID(1) - _hookOne.SetNumber(1) - _hookOne.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - _hookTwo := testHook() - _hookTwo.SetID(2) - _hookTwo.SetRepoID(1) - _hookTwo.SetBuildID(2) - _hookTwo.SetNumber(2) - _hookTwo.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Hook - }{ - { - failure: false, - want: []*library.Hook{_hookOne, _hookTwo}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the hooks table - defer _database.Sqlite.Exec("delete from hooks;") - - for _, hook := range test.want { - // create the hook in the database - err := _database.CreateHook(hook) - if err != nil { - t.Errorf("unable to create test hook: %v", err) - } - } - - got, err := _database.GetHookList() - - if test.failure { - if err == nil { - t.Errorf("GetHookList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetHookList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetHookList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetRepoHookList(t *testing.T) { - // setup types - _hookOne := testHook() - _hookOne.SetID(1) - _hookOne.SetRepoID(1) - _hookOne.SetBuildID(1) - _hookOne.SetNumber(1) - _hookOne.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - _hookTwo := testHook() - _hookTwo.SetID(2) - _hookTwo.SetRepoID(1) - _hookTwo.SetBuildID(2) - _hookTwo.SetNumber(2) - _hookTwo.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Hook - }{ - { - failure: false, - want: []*library.Hook{_hookTwo, _hookOne}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the hooks table - defer _database.Sqlite.Exec("delete from hooks;") - - for _, hook := range test.want { - // create the hook in the database - err := _database.CreateHook(hook) - if err != nil { - t.Errorf("unable to create test hook: %v", err) - } - } - - got, err := _database.GetRepoHookList(_repo, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetRepoHookList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoHookList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoHookList is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/hook_test.go b/database/sqlite/hook_test.go deleted file mode 100644 index 371c787e8..000000000 --- a/database/sqlite/hook_test.go +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/library" -) - -func TestSqlite_Client_GetHook(t *testing.T) { - // setup types - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - - _hook := testHook() - _hook.SetID(1) - _hook.SetRepoID(1) - _hook.SetBuildID(1) - _hook.SetNumber(1) - _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Hook - }{ - { - failure: false, - want: _hook, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the hook in the database - err := _database.CreateHook(test.want) - if err != nil { - t.Errorf("unable to create test hook: %v", err) - } - } - - got, err := _database.GetHook(1, _repo) - - // cleanup the hooks table - _ = _database.Sqlite.Exec("DELETE FROM hooks;") - - if test.failure { - if err == nil { - t.Errorf("GetHook should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetHook returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetHook is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetLastHook(t *testing.T) { - // setup types - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - - _hook := testHook() - _hook.SetID(1) - _hook.SetRepoID(1) - _hook.SetBuildID(1) - _hook.SetNumber(1) - _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Hook - }{ - { - failure: false, - want: _hook, - }, - { - failure: false, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the hook in the database - err := _database.CreateHook(test.want) - if err != nil { - t.Errorf("unable to create test hook: %v", err) - } - } - - got, err := _database.GetLastHook(_repo) - - // cleanup the hooks table - _ = _database.Sqlite.Exec("DELETE FROM hooks;") - - if test.failure { - if err == nil { - t.Errorf("GetLastHook should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetLastHook returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetLastHook is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_CreateHook(t *testing.T) { - // setup types - _hook := testHook() - _hook.SetID(1) - _hook.SetRepoID(1) - _hook.SetBuildID(1) - _hook.SetNumber(1) - _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the hooks table - defer _database.Sqlite.Exec("delete from hooks;") - - err := _database.CreateHook(_hook) - - if test.failure { - if err == nil { - t.Errorf("CreateHook should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateHook returned err: %v", err) - } - } -} - -func TestSqlite_Client_UpdateHook(t *testing.T) { - // setup types - _hook := testHook() - _hook.SetID(1) - _hook.SetRepoID(1) - _hook.SetBuildID(1) - _hook.SetNumber(1) - _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the hooks table - defer _database.Sqlite.Exec("delete from hooks;") - - // create the hook in the database - err := _database.CreateHook(_hook) - if err != nil { - t.Errorf("unable to create test hook: %v", err) - } - - err = _database.UpdateHook(_hook) - - if test.failure { - if err == nil { - t.Errorf("UpdateHook should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateHook returned err: %v", err) - } - } -} - -func TestSqlite_Client_DeleteHook(t *testing.T) { - // setup types - _hook := testHook() - _hook.SetID(1) - _hook.SetRepoID(1) - _hook.SetBuildID(1) - _hook.SetNumber(1) - _hook.SetSourceID("c8da1302-07d6-11ea-882f-4893bca275b8") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the hooks table - defer _database.Sqlite.Exec("delete from hooks;") - - // create the hook in the database - err := _database.CreateHook(_hook) - if err != nil { - t.Errorf("unable to create test hook: %v", err) - } - - err = _database.DeleteHook(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteHook should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteHook returned err: %v", err) - } - } -} - -// testHook is a test helper function to create a -// library Hook type with all fields set to their -// zero values. -func testHook() *library.Hook { - i := 0 - i64 := int64(0) - str := "" - - return &library.Hook{ - ID: &i64, - RepoID: &i64, - BuildID: &i64, - Number: &i, - SourceID: &str, - Created: &i64, - Host: &str, - Event: &str, - Branch: &str, - Error: &str, - Status: &str, - Link: &str, - } -} diff --git a/database/sqlite/log.go b/database/sqlite/log.go deleted file mode 100644 index 605d50064..000000000 --- a/database/sqlite/log.go +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "errors" - "fmt" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetBuildLogs gets a collection of logs for a build by unique ID from the database. -func (c *client) GetBuildLogs(id int64) ([]*library.Log, error) { - c.Logger.Tracef("listing logs for build %d from the database", id) - - // variable to store query results - l := new([]database.Log) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableLog). - Raw(dml.ListBuildLogs, id). - Scan(l).Error - if err != nil { - return nil, err - } - - // variable we want to return - logs := []*library.Log{} - // iterate through all query results - for _, log := range *l { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := log - - // decompress log data for the step - // - // https://pkg.go.dev/github.com/go-vela/types/database#Log.Decompress - err = tmp.Decompress() - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch uncompressed logs - c.Logger.Errorf("unable to decompress logs for build %d: %v", id, err) - } - - // convert query result to library type - logs = append(logs, tmp.ToLibrary()) - } - - return logs, nil -} - -// GetStepLog gets a log by unique ID from the database. -// -// nolint: dupl // ignore similar code with service -func (c *client) GetStepLog(id int64) (*library.Log, error) { - c.Logger.Tracef("getting log for step %d from the database", id) - - // variable to store query results - l := new(database.Log) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableLog). - Raw(dml.SelectStepLog, id). - Scan(l) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decompress log data for the step - // - // https://pkg.go.dev/github.com/go-vela/types/database#Log.Decompress - err := l.Decompress() - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch uncompressed logs - c.Logger.Errorf("unable to decompress logs for step %d: %v", id, err) - - // return the uncompressed log - return l.ToLibrary(), result.Error - } - - // return the decompressed log - return l.ToLibrary(), result.Error -} - -// GetServiceLog gets a log by unique ID from the database. -// -// nolint: dupl // ignore similar code with step -func (c *client) GetServiceLog(id int64) (*library.Log, error) { - c.Logger.Tracef("getting log for service %d from the database", id) - - // variable to store query results - l := new(database.Log) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableLog). - Raw(dml.SelectServiceLog, id). - Scan(l) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decompress log data for the service - // - // https://pkg.go.dev/github.com/go-vela/types/database#Log.Decompress - err := l.Decompress() - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allowing us to fetch uncompressed logs - c.Logger.Errorf("unable to decompress logs for service %d: %v", id, err) - - // return the uncompressed log - return l.ToLibrary(), result.Error - } - - // return the decompressed log - return l.ToLibrary(), result.Error -} - -// CreateLog creates a new log in the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) CreateLog(l *library.Log) error { - // check if the log entry is for a step - if l.GetStepID() > 0 { - c.Logger.Tracef("creating log for step %d in the database", l.GetStepID()) - } else { - c.Logger.Tracef("creating log for service %d in the database", l.GetServiceID()) - } - - // cast to database type - log := database.LogFromLibrary(l) - - // validate the necessary fields are populated - err := log.Validate() - if err != nil { - return err - } - - // compress log data for the resource - // - // https://pkg.go.dev/github.com/go-vela/types/database#Log.Compress - err = log.Compress(c.config.CompressionLevel) - if err != nil { - return fmt.Errorf("unable to compress logs for step %d: %v", l.GetStepID(), err) - } - - // send query to the database - return c.Sqlite. - Table(constants.TableLog). - Create(log).Error -} - -// UpdateLog updates a log in the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) UpdateLog(l *library.Log) error { - // check if the log entry is for a step - if l.GetStepID() > 0 { - c.Logger.Tracef("updating log for step %d in the database", l.GetStepID()) - } else { - c.Logger.Tracef("updating log for service %d in the database", l.GetServiceID()) - } - - // cast to database type - log := database.LogFromLibrary(l) - - // validate the necessary fields are populated - err := log.Validate() - if err != nil { - return err - } - - // compress log data for the resource - // - // https://pkg.go.dev/github.com/go-vela/types/database#Log.Compress - err = log.Compress(c.config.CompressionLevel) - if err != nil { - return fmt.Errorf("unable to compress logs for step %d: %v", l.GetStepID(), err) - } - - // send query to the database - return c.Sqlite. - Table(constants.TableLog). - Save(log).Error -} - -// DeleteLog deletes a log by unique ID from the database. -func (c *client) DeleteLog(id int64) error { - c.Logger.Tracef("deleting log %d from the database", id) - - // send query to the database - return c.Sqlite. - Table(constants.TableLog). - Exec(dml.DeleteLog, id).Error -} diff --git a/database/sqlite/log_test.go b/database/sqlite/log_test.go deleted file mode 100644 index 280f06bce..000000000 --- a/database/sqlite/log_test.go +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/library" -) - -func TestSqlite_Client_GetBuildLogs(t *testing.T) { - // setup types - _logOne := testLog() - _logOne.SetID(1) - _logOne.SetStepID(1) - _logOne.SetBuildID(1) - _logOne.SetRepoID(1) - _logOne.SetData([]byte{}) - - _logTwo := testLog() - _logTwo.SetID(2) - _logTwo.SetServiceID(1) - _logTwo.SetBuildID(1) - _logTwo.SetRepoID(1) - _logTwo.SetData([]byte{}) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Log - }{ - { - failure: false, - want: []*library.Log{_logTwo, _logOne}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the logs table - defer _database.Sqlite.Exec("delete from logs;") - - for _, log := range test.want { - // create the log in the database - err := _database.CreateLog(log) - if err != nil { - t.Errorf("unable to create test log: %v", err) - } - } - - got, err := _database.GetBuildLogs(1) - - if test.failure { - if err == nil { - t.Errorf("GetBuildLogs should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildLogs returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildLogs is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetStepLog(t *testing.T) { - // setup types - _log := testLog() - _log.SetID(1) - _log.SetStepID(1) - _log.SetBuildID(1) - _log.SetRepoID(1) - _log.SetData([]byte{}) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Log - }{ - { - failure: false, - want: _log, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the log in the database - err := _database.CreateLog(test.want) - if err != nil { - t.Errorf("unable to create test log: %v", err) - } - } - - got, err := _database.GetStepLog(1) - - // cleanup the logs table - _ = _database.Sqlite.Exec("DELETE FROM logs;") - - if test.failure { - if err == nil { - t.Errorf("GetStepLog should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetStepLog returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetStepLog is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetServiceLog(t *testing.T) { - // setup types - _log := testLog() - _log.SetID(1) - _log.SetServiceID(1) - _log.SetBuildID(1) - _log.SetRepoID(1) - _log.SetData([]byte{}) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Log - }{ - { - failure: false, - want: _log, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the log in the database - err := _database.CreateLog(test.want) - if err != nil { - t.Errorf("unable to create test log: %v", err) - } - } - - got, err := _database.GetServiceLog(1) - - // cleanup the logs table - _ = _database.Sqlite.Exec("DELETE FROM logs;") - - if test.failure { - if err == nil { - t.Errorf("GetServiceLog should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetServiceLog returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetServiceLog is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_CreateLog(t *testing.T) { - // setup types - _log := testLog() - _log.SetID(1) - _log.SetStepID(1) - _log.SetBuildID(1) - _log.SetRepoID(1) - _log.SetData([]byte{}) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the logs table - defer _database.Sqlite.Exec("delete from logs;") - - err := _database.CreateLog(_log) - - if test.failure { - if err == nil { - t.Errorf("CreateLog should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateLog returned err: %v", err) - } - } -} - -func TestSqlite_Client_UpdateLog(t *testing.T) { - // setup types - _log := testLog() - _log.SetID(1) - _log.SetStepID(1) - _log.SetBuildID(1) - _log.SetRepoID(1) - _log.SetData([]byte{}) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the logs table - defer _database.Sqlite.Exec("delete from logs;") - - // create the log in the database - err := _database.CreateLog(_log) - if err != nil { - t.Errorf("unable to create test log: %v", err) - } - - err = _database.UpdateLog(_log) - - if test.failure { - if err == nil { - t.Errorf("UpdateLog should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateLog returned err: %v", err) - } - } -} - -func TestSqlite_Client_DeleteLog(t *testing.T) { - // setup types - _log := testLog() - _log.SetID(1) - _log.SetStepID(1) - _log.SetBuildID(1) - _log.SetRepoID(1) - _log.SetData([]byte{}) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the logs table - defer _database.Sqlite.Exec("delete from logs;") - - // create the log in the database - err := _database.CreateLog(_log) - if err != nil { - t.Errorf("unable to create test log: %v", err) - } - - err = _database.DeleteLog(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteLog should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteLog returned err: %v", err) - } - } -} - -// testLog is a test helper function to create a -// library Log type with all fields set to their -// zero values. -func testLog() *library.Log { - i64 := int64(0) - b := []byte{} - - return &library.Log{ - ID: &i64, - BuildID: &i64, - RepoID: &i64, - ServiceID: &i64, - StepID: &i64, - Data: &b, - } -} diff --git a/database/sqlite/opts.go b/database/sqlite/opts.go deleted file mode 100644 index 406dddcc8..000000000 --- a/database/sqlite/opts.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "fmt" - "time" -) - -// ClientOpt represents a configuration option to initialize the database client for Sqlite. -type ClientOpt func(*client) error - -// WithAddress sets the Sqlite address in the database client for Sqlite. -func WithAddress(address string) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring address in sqlite database client") - - // check if the Sqlite address provided is empty - if len(address) == 0 { - return fmt.Errorf("no Sqlite address provided") - } - - // set the address in the sqlite client - c.config.Address = address - - return nil - } -} - -// WithCompressionLevel sets the compression level in the database client for Sqlite. -func WithCompressionLevel(level int) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring compression level in sqlite database client") - - // set the compression level in the sqlite client - c.config.CompressionLevel = level - - return nil - } -} - -// WithConnectionLife sets the connection duration in the database client for Sqlite. -func WithConnectionLife(duration time.Duration) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring connection duration in sqlite database client") - - // set the connection duration in the sqlite client - c.config.ConnectionLife = duration - - return nil - } -} - -// WithConnectionIdle sets the maximum idle connections in the database client for Sqlite. -func WithConnectionIdle(idle int) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring maximum idle connections in sqlite database client") - - // set the maximum idle connections in the sqlite client - c.config.ConnectionIdle = idle - - return nil - } -} - -// WithConnectionOpen sets the maximum open connections in the database client for Sqlite. -func WithConnectionOpen(open int) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring maximum open connections in sqlite database client") - - // set the maximum open connections in the sqlite client - c.config.ConnectionOpen = open - - return nil - } -} - -// WithEncryptionKey sets the encryption key in the database client for Sqlite. -func WithEncryptionKey(key string) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring encryption key in sqlite database client") - - // check if the Sqlite encryption key provided is empty - if len(key) == 0 { - return fmt.Errorf("no Sqlite encryption key provided") - } - - // set the encryption key in the sqlite client - c.config.EncryptionKey = key - - return nil - } -} - -// WithSkipCreation sets the skip creation logic in the database client for Sqlite. -func WithSkipCreation(skipCreation bool) ClientOpt { - return func(c *client) error { - c.Logger.Trace("configuring skip creating objects in sqlite database client") - - // set to skip creating tables and indexes in the sqlite client - c.config.SkipCreation = skipCreation - - return nil - } -} diff --git a/database/sqlite/opts_test.go b/database/sqlite/opts_test.go deleted file mode 100644 index 42f70c58e..000000000 --- a/database/sqlite/opts_test.go +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - "time" - - "github.com/sirupsen/logrus" -) - -func TestSqlite_ClientOpt_WithAddress(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - failure bool - address string - want string - }{ - { - failure: false, - address: "sqlite://foo:bar@localhost:5432/vela", - want: "sqlite://foo:bar@localhost:5432/vela", - }, - { - failure: true, - address: "", - want: "", - }, - } - - // run tests - for _, test := range tests { - err := WithAddress(test.address)(c) - - if test.failure { - if err == nil { - t.Errorf("WithAddress should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("WithAddress returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.Address, test.want) { - t.Errorf("WithAddress is %v, want %v", c.config.Address, test.want) - } - } -} - -func TestSqlite_ClientOpt_WithCompressionLevel(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - level int - want int - }{ - { - level: 3, - want: 3, - }, - { - level: 0, - want: 0, - }, - } - - // run tests - for _, test := range tests { - err := WithCompressionLevel(test.level)(c) - - if err != nil { - t.Errorf("WithCompressionLevel returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.CompressionLevel, test.want) { - t.Errorf("WithCompressionLevel is %v, want %v", c.config.CompressionLevel, test.want) - } - } -} - -func TestSqlite_ClientOpt_WithConnectionLife(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - duration time.Duration - want time.Duration - }{ - { - duration: 10 * time.Second, - want: 10 * time.Second, - }, - { - duration: 0, - want: 0, - }, - } - - // run tests - for _, test := range tests { - err := WithConnectionLife(test.duration)(c) - - if err != nil { - t.Errorf("WithConnectionLife returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.ConnectionLife, test.want) { - t.Errorf("WithConnectionLife is %v, want %v", c.config.ConnectionLife, test.want) - } - } -} - -func TestSqlite_ClientOpt_WithConnectionIdle(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - idle int - want int - }{ - { - idle: 5, - want: 5, - }, - { - idle: 0, - want: 0, - }, - } - - // run tests - for _, test := range tests { - err := WithConnectionIdle(test.idle)(c) - - if err != nil { - t.Errorf("WithConnectionIdle returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.ConnectionIdle, test.want) { - t.Errorf("WithConnectionIdle is %v, want %v", c.config.ConnectionIdle, test.want) - } - } -} - -func TestSqlite_ClientOpt_WithConnectionOpen(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - open int - want int - }{ - { - open: 10, - want: 10, - }, - { - open: 0, - want: 0, - }, - } - - // run tests - for _, test := range tests { - err := WithConnectionOpen(test.open)(c) - - if err != nil { - t.Errorf("WithConnectionOpen returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.ConnectionOpen, test.want) { - t.Errorf("WithConnectionOpen is %v, want %v", c.config.ConnectionOpen, test.want) - } - } -} - -func TestSqlite_ClientOpt_WithEncryptionKey(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - failure bool - key string - want string - }{ - { - failure: false, - key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", - want: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", - }, - { - failure: true, - key: "", - want: "", - }, - } - - // run tests - for _, test := range tests { - err := WithEncryptionKey(test.key)(c) - - if test.failure { - if err == nil { - t.Errorf("WithEncryptionKey should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("WithEncryptionKey returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.EncryptionKey, test.want) { - t.Errorf("WithEncryptionKey is %v, want %v", c.config.EncryptionKey, test.want) - } - } -} - -func TestSqlite_ClientOpt_WithSkipCreation(t *testing.T) { - // setup types - c := new(client) - c.config = new(config) - logger := logrus.StandardLogger() - c.Logger = logrus.NewEntry(logger) - - // setup tests - tests := []struct { - skipCreation bool - want bool - }{ - { - skipCreation: true, - want: true, - }, - { - skipCreation: false, - want: false, - }, - } - - // run tests - for _, test := range tests { - err := WithSkipCreation(test.skipCreation)(c) - - if err != nil { - t.Errorf("WithSkipCreation returned err: %v", err) - } - - if !reflect.DeepEqual(c.config.SkipCreation, test.want) { - t.Errorf("WithSkipCreation is %v, want %v", c.config.SkipCreation, test.want) - } - } -} diff --git a/database/sqlite/ping.go b/database/sqlite/ping.go deleted file mode 100644 index a23b9f707..000000000 --- a/database/sqlite/ping.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "fmt" - "time" -) - -// Ping sends a "ping" request with backoff to the database. -func (c *client) Ping() error { - c.Logger.Trace("sending ping requests to the database") - - // create a loop to attempt ping requests 5 times - for i := 0; i < 5; i++ { - // capture database/sql database from gorm database - // - // https://pkg.go.dev/gorm.io/gorm#DB.DB - _sql, err := c.Sqlite.DB() - if err != nil { - return err - } - - // send ping request to database - // - // https://pkg.go.dev/database/sql#DB.Ping - err = _sql.Ping() - if err != nil { - c.Logger.Debugf("unable to ping database - retrying in %v", time.Duration(i)*time.Second) - - // sleep for loop iteration in seconds - time.Sleep(time.Duration(i) * time.Second) - - // continue to next iteration of the loop - continue - } - - // able to ping database so return with no error - return nil - } - - return fmt.Errorf("unable to successfully ping database") -} diff --git a/database/sqlite/ping_test.go b/database/sqlite/ping_test.go deleted file mode 100644 index aee71b003..000000000 --- a/database/sqlite/ping_test.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "testing" -) - -func TestSqlite_Client_Ping(t *testing.T) { - // setup types - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { - _sql, _ := _database.Sqlite.DB() - _sql.Close() - }() - - _bad, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - // close the bad database to simulate failures to ping - _sql, _ := _bad.Sqlite.DB() - _sql.Close() - - // setup tests - tests := []struct { - failure bool - database *client - }{ - { - failure: false, - database: _database, - }, - { - failure: true, - database: _bad, - }, - } - - // run tests - for _, test := range tests { - err = test.database.Ping() - - if test.failure { - if err == nil { - t.Errorf("Ping should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("Ping returned err: %v", err) - } - } -} diff --git a/database/sqlite/repo.go b/database/sqlite/repo.go deleted file mode 100644 index 81bb01f8d..000000000 --- a/database/sqlite/repo.go +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "errors" - "fmt" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetRepo gets a repo by org and name from the database. -func (c *client) GetRepo(org, name string) (*library.Repo, error) { - c.Logger.WithFields(logrus.Fields{ - "org": org, - "repo": name, - }).Tracef("getting repo %s/%s from the database", org, name) - - // variable to store query results - r := new(database.Repo) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableRepo). - Raw(dml.SelectRepo, org, name). - Scan(r) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt - err := r.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted repos - c.Logger.Errorf("unable to decrypt repo %s/%s: %v", org, name, err) - - // return the unencrypted repo - return r.ToLibrary(), result.Error - } - - // return the decrypted repo - return r.ToLibrary(), result.Error -} - -// CreateRepo creates a new repo in the database. -// -// nolint: dupl // ignore similar code with update -func (c *client) CreateRepo(r *library.Repo) error { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("creating repo %s in the database", r.GetFullName()) - - // cast to database type - repo := database.RepoFromLibrary(r) - - // validate the necessary fields are populated - err := repo.Validate() - if err != nil { - return err - } - - // encrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Encrypt - err = repo.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt repo %s: %v", r.GetFullName(), err) - } - - // send query to the database - return c.Sqlite. - Table(constants.TableRepo). - Create(repo).Error -} - -// UpdateRepo updates a repo in the database. -// -// nolint: dupl // ignore similar code with create -func (c *client) UpdateRepo(r *library.Repo) error { - c.Logger.WithFields(logrus.Fields{ - "org": r.GetOrg(), - "repo": r.GetName(), - }).Tracef("updating repo %s in the database", r.GetFullName()) - - // cast to database type - repo := database.RepoFromLibrary(r) - - // validate the necessary fields are populated - err := repo.Validate() - if err != nil { - return err - } - - // encrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Encrypt - err = repo.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt repo %s: %v", r.GetFullName(), err) - } - - // send query to the database - return c.Sqlite. - Table(constants.TableRepo). - Save(repo).Error -} - -// DeleteRepo deletes a repo by unique ID from the database. -func (c *client) DeleteRepo(id int64) error { - c.Logger.Tracef("deleting repo %d in the database", id) - - // send query to the database - return c.Sqlite. - Table(constants.TableRepo). - Exec(dml.DeleteRepo, id).Error -} diff --git a/database/sqlite/repo_count.go b/database/sqlite/repo_count.go deleted file mode 100644 index 8438db1ee..000000000 --- a/database/sqlite/repo_count.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetRepoCount gets a count of all repos from the database. -func (c *client) GetRepoCount() (int64, error) { - c.Logger.Trace("getting count of repos from the database") - - // variable to store query results - var r int64 - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableRepo). - Raw(dml.SelectReposCount). - Pluck("count", &r).Error - - return r, err -} - -// GetOrgRepoCount gets a count of all repos for a specific org from the database. -func (c *client) GetOrgRepoCount(org string, filters map[string]string) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "org": org, - }).Tracef("getting count of repos for org %s from the database", org) - - // variable to store query results - var r int64 - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableRepo). - Select("count(*)"). - Where("org = ?", org). - Where(filters). - Pluck("count", &r).Error - - return r, err -} - -// GetUserRepoCount gets a count of all repos for a specific user from the database. -func (c *client) GetUserRepoCount(u *library.User) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("getting count of repos for user %s in the database", u.GetName()) - - // variable to store query results - var r int64 - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableRepo). - Raw(dml.SelectUserReposCount, u.GetID()). - Pluck("count", &r).Error - - return r, err -} diff --git a/database/sqlite/repo_count_test.go b/database/sqlite/repo_count_test.go deleted file mode 100644 index ed2e77de4..000000000 --- a/database/sqlite/repo_count_test.go +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the repo table - err = _database.Sqlite.Exec(ddl.CreateRepoTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableRepo, err) - } -} - -func TestSqlite_Client_GetRepoCount(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - - _repoTwo := testRepo() - _repoTwo.SetID(2) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // create the repos in the database - err := _database.CreateRepo(_repoOne) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - err = _database.CreateRepo(_repoTwo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - got, err := _database.GetRepoCount() - - if test.failure { - if err == nil { - t.Errorf("GetRepoCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetUserRepoCount(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - - _repoTwo := testRepo() - _repoTwo.SetID(2) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("public") - - _user := new(library.User) - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // create the repos in the database - err := _database.CreateRepo(_repoOne) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - err = _database.CreateRepo(_repoTwo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - got, err := _database.GetUserRepoCount(_user) - - if test.failure { - if err == nil { - t.Errorf("GetUserRepoCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserRepoCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserRepoCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetOrgRepoCount(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - - _repoTwo := testRepo() - _repoTwo.SetID(2) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("public") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 1, - }, - } - filters := map[string]string{} - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // create the repos in the database - err := _database.CreateRepo(_repoOne) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - err = _database.CreateRepo(_repoTwo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - got, err := _database.GetOrgRepoCount("foo", filters) - - if test.failure { - if err == nil { - t.Errorf("GetRepoCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetOrgRepoCount_NonAdmin(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - - _repoTwo := testRepo() - _repoTwo.SetID(2) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("foo") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("foo/foo") - _repoTwo.SetVisibility("private") - - _repoThree := testRepo() - _repoThree.SetID(3) - _repoThree.SetUserID(1) - _repoThree.SetHash("baz") - _repoThree.SetOrg("bar") - _repoThree.SetName("foo") - _repoThree.SetFullName("bar/foo") - _repoThree.SetVisibility("private") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 1, - }, - } - filters := map[string]string{} - filters["visibility"] = "public" - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - for _, repo := range []*library.Repo{_repoOne, _repoTwo, _repoThree} { - // create the repos in the database - err := _database.CreateRepo(repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - } - - got, err := _database.GetOrgRepoCount("foo", filters) - - if test.failure { - if err == nil { - t.Errorf("GetRepoCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/repo_list.go b/database/sqlite/repo_list.go deleted file mode 100644 index 72afbe125..000000000 --- a/database/sqlite/repo_list.go +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetRepoList gets a list of all repos from the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) GetRepoList() ([]*library.Repo, error) { - c.Logger.Trace("listing repos from the database") - - // variable to store query results - r := new([]database.Repo) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableRepo). - Raw(dml.ListRepos). - Scan(r).Error - if err != nil { - return nil, err - } - - // variable we want to return - repos := []*library.Repo{} - // iterate through all query results - for _, repo := range *r { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := repo - - // decrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted repos - c.Logger.Errorf("unable to decrypt repo %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - repos = append(repos, tmp.ToLibrary()) - } - - return repos, nil -} - -// GetOrgRepoList gets a list of all repos by org from the database. -// -// nolint: lll // ignore long line length due to variable names -func (c *client) GetOrgRepoList(org string, filters map[string]string, page, perPage int) ([]*library.Repo, error) { - c.Logger.WithFields(logrus.Fields{ - "org": org, - }).Tracef("listing repos for org %s from the database", org) - - // variable to store query results - r := new([]database.Repo) - - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableRepo). - Where("org = ?", org). - Where(filters). - Limit(perPage). - Offset(offset). - Scan(r).Error - if err != nil { - return nil, err - } - - // variable we want to return - repos := []*library.Repo{} - // iterate through all query results - for _, repo := range *r { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := repo - - // decrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted repos - c.Logger.Errorf("unable to decrypt repo %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - repos = append(repos, tmp.ToLibrary()) - } - - return repos, nil -} - -// GetUserRepoList gets a list of all repos by user ID from the database. -func (c *client) GetUserRepoList(u *library.User, page, perPage int) ([]*library.Repo, error) { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("listing repos for user %s from the database", u.GetName()) - - // variable to store query results - r := new([]database.Repo) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableRepo). - Raw(dml.ListUserRepos, u.GetID(), perPage, offset). - Scan(r).Error - if err != nil { - return nil, err - } - - // variable we want to return - repos := []*library.Repo{} - // iterate through all query results - for _, repo := range *r { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := repo - - // decrypt the fields for the repo - // - // https://pkg.go.dev/github.com/go-vela/types/database#Repo.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted repos - c.Logger.Errorf("unable to decrypt repo %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - repos = append(repos, tmp.ToLibrary()) - } - - return repos, nil -} diff --git a/database/sqlite/repo_list_test.go b/database/sqlite/repo_list_test.go deleted file mode 100644 index 1d9567cdf..000000000 --- a/database/sqlite/repo_list_test.go +++ /dev/null @@ -1,335 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the repo table - err = _database.Sqlite.Exec(ddl.CreateRepoTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableRepo, err) - } -} - -func TestSqlite_Client_GetRepoList(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - _repoOne.SetPipelineType("yaml") - _repoOne.SetPreviousName("") - - _repoTwo := testRepo() - _repoTwo.SetID(2) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("public") - _repoTwo.SetPipelineType("yaml") - _repoTwo.SetPreviousName("oldName") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Repo - }{ - { - failure: false, - want: []*library.Repo{_repoOne, _repoTwo}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - for _, repo := range test.want { - // create the repo in the database - err := _database.CreateRepo(repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - } - - got, err := _database.GetRepoList() - - if test.failure { - if err == nil { - t.Errorf("GetRepoList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepoList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepoList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetOrgRepoList(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - _repoOne.SetPipelineType("yaml") - _repoOne.SetPreviousName("oldName") - - _repoTwo := testRepo() - _repoTwo.SetID(2) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("foo") - _repoTwo.SetName("baz") - _repoTwo.SetFullName("foo/baz") - _repoTwo.SetVisibility("public") - _repoTwo.SetPipelineType("yaml") - _repoTwo.SetPreviousName("") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Repo - }{ - { - failure: false, - want: []*library.Repo{_repoOne, _repoTwo}, - }, - } - filters := map[string]string{} - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - for _, repo := range test.want { - // create the repo in the database - err := _database.CreateRepo(repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - } - - got, err := _database.GetOrgRepoList("foo", filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetOrgRepoList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgRepoList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgRepoList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetOrgRepoList_NonAdmin(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - _repoOne.SetPipelineType("yaml") - _repoOne.SetPreviousName("") - - _repoTwo := testRepo() - _repoTwo.SetID(2) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("foo") - _repoTwo.SetName("baz") - _repoTwo.SetFullName("foo/baz") - _repoTwo.SetVisibility("private") - _repoTwo.SetPipelineType("yaml") - _repoTwo.SetPreviousName("") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Repo - }{ - { - failure: false, - want: []*library.Repo{_repoOne}, - }, - } - filters := map[string]string{} - filters["visibility"] = "public" - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - for _, repo := range test.want { - // create the repo in the database - err := _database.CreateRepo(repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - } - - got, err := _database.GetOrgRepoList("foo", filters, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetOrgRepoList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetOrgRepoList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetOrgRepoList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetUserRepoList(t *testing.T) { - // setup types - _repoOne := testRepo() - _repoOne.SetID(1) - _repoOne.SetUserID(1) - _repoOne.SetHash("baz") - _repoOne.SetOrg("foo") - _repoOne.SetName("bar") - _repoOne.SetFullName("foo/bar") - _repoOne.SetVisibility("public") - _repoOne.SetPipelineType("yaml") - _repoOne.SetPreviousName("") - - _repoTwo := testRepo() - _repoTwo.SetID(2) - _repoTwo.SetUserID(1) - _repoTwo.SetHash("baz") - _repoTwo.SetOrg("bar") - _repoTwo.SetName("foo") - _repoTwo.SetFullName("bar/foo") - _repoTwo.SetVisibility("public") - _repoTwo.SetPipelineType("yaml") - _repoTwo.SetPreviousName("") - - _user := new(library.User) - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Repo - }{ - { - failure: false, - want: []*library.Repo{_repoTwo, _repoOne}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - for _, repo := range test.want { - // create the repo in the database - err := _database.CreateRepo(repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - } - - got, err := _database.GetUserRepoList(_user, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetUserRepoList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserRepoList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserRepoList is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/repo_test.go b/database/sqlite/repo_test.go deleted file mode 100644 index a5fc9a925..000000000 --- a/database/sqlite/repo_test.go +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/library" -) - -func TestSqlite_Client_GetRepo(t *testing.T) { - // setup types - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - _repo.SetPipelineType("yaml") - _repo.SetPreviousName("") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Repo - }{ - { - failure: false, - want: _repo, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the repo in the database - err := _database.CreateRepo(test.want) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - } - - got, err := _database.GetRepo("foo", "bar") - - // cleanup the repos table - _ = _database.Sqlite.Exec("DELETE FROM repos;") - - if test.failure { - if err == nil { - t.Errorf("GetRepo should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetRepo returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetRepo is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_CreateRepo(t *testing.T) { - // setup types - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - _repo.SetPreviousName("") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - err := _database.CreateRepo(_repo) - - if test.failure { - if err == nil { - t.Errorf("CreateRepo should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateRepo returned err: %v", err) - } - } -} - -func TestSqlite_Client_UpdateRepo(t *testing.T) { - // setup types - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - _repo.SetPreviousName("") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // create the repo in the database - err := _database.CreateRepo(_repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - err = _database.UpdateRepo(_repo) - - if test.failure { - if err == nil { - t.Errorf("UpdateRepo should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateRepo returned err: %v", err) - } - } -} - -func TestSqlite_Client_DeleteRepo(t *testing.T) { - // setup types - _repo := testRepo() - _repo.SetID(1) - _repo.SetUserID(1) - _repo.SetHash("baz") - _repo.SetOrg("foo") - _repo.SetName("bar") - _repo.SetFullName("foo/bar") - _repo.SetVisibility("public") - _repo.SetPreviousName("") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the repos table - defer _database.Sqlite.Exec("delete from repos;") - - // create the repo in the database - err = _database.CreateRepo(_repo) - if err != nil { - t.Errorf("unable to create test repo: %v", err) - } - - err := _database.DeleteRepo(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteRepo should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteRepo returned err: %v", err) - } - } -} - -// testRepo is a test helper function to create a -// library Repo type with all fields set to their -// zero values. -func testRepo() *library.Repo { - i64 := int64(0) - i := 0 - str := "" - b := false - - return &library.Repo{ - ID: &i64, - UserID: &i64, - Hash: &str, - Org: &str, - Name: &str, - FullName: &str, - Link: &str, - Clone: &str, - Branch: &str, - BuildLimit: &i64, - Timeout: &i64, - Counter: &i, - Visibility: &str, - Private: &b, - Trusted: &b, - Active: &b, - AllowPull: &b, - AllowPush: &b, - AllowDeploy: &b, - AllowTag: &b, - AllowComment: &b, - PreviousName: &str, - } -} diff --git a/database/sqlite/secret.go b/database/sqlite/secret.go deleted file mode 100644 index 98076763a..000000000 --- a/database/sqlite/secret.go +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "errors" - "fmt" - "strings" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetSecret gets a secret by type, org, name (repo or team) and secret name from the database. -func (c *client) GetSecret(t, o, n, secretName string) (*library.Secret, error) { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": o, - "repo": n, - "secret": secretName, - "type": t, - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ - "org": o, - "team": n, - "secret": secretName, - "type": t, - } - } - - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("getting %s secret %s for %s/%s from the database", t, secretName, o, n) - - var err error - - // variable to store query results - s := new(database.Secret) - - // send query to the database and store result in variable - switch t { - case constants.SecretOrg: - result := c.Sqlite. - Table(constants.TableSecret). - Raw(dml.SelectOrgSecret, o, secretName). - Scan(s) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - err = result.Error - case constants.SecretRepo: - result := c.Sqlite. - Table(constants.TableSecret). - Raw(dml.SelectRepoSecret, o, n, secretName). - Scan(s) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - err = result.Error - case constants.SecretShared: - result := c.Sqlite. - Table(constants.TableSecret). - Raw(dml.SelectSharedSecret, o, n, secretName). - Scan(s) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - err = result.Error - } - if err != nil { - return nil, err - } - - // decrypt the value for the secret - // - // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt - err = s.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted secrets - c.Logger.Errorf("unable to decrypt %s secret %s for %s/%s: %v", t, secretName, o, n, err) - - // return the unencrypted secret - return s.ToLibrary(), nil - } - - // return the decrypted secret - return s.ToLibrary(), nil -} - -// CreateSecret creates a new secret in the database. -// -// nolint: dupl // ignore similar code with update -func (c *client) CreateSecret(s *library.Secret) error { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": s.GetOrg(), - "repo": s.GetRepo(), - "secret": s.GetName(), - "type": s.GetType(), - } - - // check if secret is a shared secret - if strings.EqualFold(s.GetType(), constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ - "org": s.GetOrg(), - "team": s.GetTeam(), - "secret": s.GetName(), - "type": s.GetType(), - } - } - - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("creating %s secret %s in the database", s.GetType(), s.GetName()) - - // cast to database type - secret := database.SecretFromLibrary(s) - - // validate the necessary fields are populated - err := secret.Validate() - if err != nil { - return err - } - - // encrypt the value for the secret - // - // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Encrypt - err = secret.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt secret %s: %v", s.GetName(), err) - } - - // send query to the database - return c.Sqlite. - Table(constants.TableSecret). - Create(secret.Nullify()).Error -} - -// UpdateSecret updates a secret in the database. -// -// nolint: dupl // ignore similar code with create -func (c *client) UpdateSecret(s *library.Secret) error { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": s.GetOrg(), - "repo": s.GetRepo(), - "secret": s.GetName(), - "type": s.GetType(), - } - - // check if secret is a shared secret - if strings.EqualFold(s.GetType(), constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ - "org": s.GetOrg(), - "team": s.GetTeam(), - "secret": s.GetName(), - "type": s.GetType(), - } - } - - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("updating %s secret %s in the database", s.GetType(), s.GetName()) - - // cast to database type - secret := database.SecretFromLibrary(s) - - // validate the necessary fields are populated - err := secret.Validate() - if err != nil { - return err - } - - // encrypt the value for the secret - // - // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Encrypt - err = secret.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt secret %s: %v", s.GetName(), err) - } - - // send query to the database - return c.Sqlite. - Table(constants.TableSecret). - Save(secret.Nullify()).Error -} - -// DeleteSecret deletes a secret by unique ID from the database. -func (c *client) DeleteSecret(id int64) error { - c.Logger.Tracef("Deleting secret %d from the database", id) - - // send query to the database - return c.Sqlite. - Table(constants.TableSecret). - Exec(dml.DeleteSecret, id).Error -} diff --git a/database/sqlite/secret_count.go b/database/sqlite/secret_count.go deleted file mode 100644 index 0cc6c4815..000000000 --- a/database/sqlite/secret_count.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "strings" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" -) - -// GetTypeSecretCount gets a count of secrets by type, -// owner, and name (repo or team) from the database. -func (c *client) GetTypeSecretCount(t, o, n string, teams []string) (int64, error) { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": o, - "repo": n, - "type": t, - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ - "org": o, - "team": n, - "type": t, - } - } - - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("getting count of %s secrets for %s/%s from the database", t, o, n) - - var err error - - // variable to store query results - var s int64 - - // send query to the database and store result in variable - switch t { - case constants.SecretOrg: - err = c.Sqlite. - Table(constants.TableSecret). - Raw(dml.SelectOrgSecretsCount, o). - Pluck("count", &s).Error - case constants.SecretRepo: - err = c.Sqlite. - Table(constants.TableSecret). - Raw(dml.SelectRepoSecretsCount, o, n). - Pluck("count", &s).Error - case constants.SecretShared: - if n == "*" { - // GitHub teams are not case-sensitive, the DB is lowercase everything for matching - var lowerTeams []string - for _, t := range teams { - lowerTeams = append(lowerTeams, strings.ToLower(t)) - } - err = c.Sqlite. - Table(constants.TableSecret). - Select("count(*)"). - Where("type = 'shared' AND org = ?", o). - Where("LOWER(team) IN (?)", lowerTeams). - Pluck("count", &s).Error - } else { - err = c.Sqlite. - Table(constants.TableSecret). - Raw(dml.SelectSharedSecretsCount, o, n). - Pluck("count", &s).Error - } - } - - return s, err -} diff --git a/database/sqlite/secret_count_test.go b/database/sqlite/secret_count_test.go deleted file mode 100644 index 4e5f20ee2..000000000 --- a/database/sqlite/secret_count_test.go +++ /dev/null @@ -1,348 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the secret table - err = _database.Sqlite.Exec(ddl.CreateSecretTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableSecret, err) - } -} - -func TestSqlite_Client_GetTypeSecretCount_Org(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetRepo("*") - _secretOne.SetName("baz") - _secretOne.SetValue("bar") - _secretOne.SetType("org") - _secretOne.SetCreatedAt(1) - _secretOne.SetCreatedBy("user") - _secretOne.SetUpdatedAt(1) - _secretOne.SetUpdatedBy("user2") - - _secretTwo := testSecret() - _secretTwo.SetID(2) - _secretTwo.SetOrg("foo") - _secretTwo.SetRepo("*") - _secretTwo.SetName("bar") - _secretTwo.SetValue("baz") - _secretTwo.SetType("org") - _secretTwo.SetCreatedAt(1) - _secretTwo.SetCreatedBy("user") - _secretTwo.SetUpdatedAt(1) - _secretTwo.SetUpdatedBy("user2") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - // create the secrets in the database - err := _database.CreateSecret(_secretOne) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - - err = _database.CreateSecret(_secretTwo) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - - got, err := _database.GetTypeSecretCount("org", "foo", "*", []string{}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetTypeSecretCount_Repo(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetRepo("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("repo") - _secretOne.SetCreatedAt(1) - _secretOne.SetCreatedBy("user") - _secretOne.SetUpdatedAt(1) - _secretOne.SetUpdatedBy("user2") - - _secretTwo := testSecret() - _secretTwo.SetID(2) - _secretTwo.SetOrg("foo") - _secretTwo.SetRepo("bar") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("repo") - _secretTwo.SetCreatedAt(1) - _secretTwo.SetCreatedBy("user") - _secretTwo.SetUpdatedAt(1) - _secretTwo.SetUpdatedBy("user2") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - // create the secrets in the database - err := _database.CreateSecret(_secretOne) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - - err = _database.CreateSecret(_secretTwo) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - - got, err := _database.GetTypeSecretCount("repo", "foo", "bar", []string{}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetTypeSecretCount_Shared(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetTeam("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("shared") - _secretOne.SetCreatedAt(1) - _secretOne.SetCreatedBy("user") - _secretOne.SetUpdatedAt(1) - _secretOne.SetUpdatedBy("user2") - - _secretTwo := testSecret() - _secretTwo.SetID(2) - _secretTwo.SetOrg("foo") - _secretTwo.SetTeam("bar") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("shared") - _secretTwo.SetCreatedAt(1) - _secretTwo.SetCreatedBy("user") - _secretTwo.SetUpdatedAt(1) - _secretTwo.SetUpdatedBy("user2") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - // create the secrets in the database - err := _database.CreateSecret(_secretOne) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - - err = _database.CreateSecret(_secretTwo) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - - got, err := _database.GetTypeSecretCount("shared", "foo", "bar", []string{}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetTypeSecretCount_Shared_Wildcard(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetTeam("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("shared") - _secretOne.SetCreatedAt(1) - _secretOne.SetCreatedBy("user") - _secretOne.SetUpdatedAt(1) - _secretOne.SetUpdatedBy("user2") - - _secretTwo := testSecret() - _secretTwo.SetID(2) - _secretTwo.SetOrg("foo") - _secretTwo.SetTeam("bared") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("shared") - _secretTwo.SetCreatedAt(1) - _secretTwo.SetCreatedBy("user") - _secretTwo.SetUpdatedAt(1) - _secretTwo.SetUpdatedBy("user2") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - // create the secrets in the database - err := _database.CreateSecret(_secretOne) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - - err = _database.CreateSecret(_secretTwo) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - - got, err := _database.GetTypeSecretCount("shared", "foo", "*", []string{"bar", "bared"}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/secret_list.go b/database/sqlite/secret_list.go deleted file mode 100644 index 41650b985..000000000 --- a/database/sqlite/secret_list.go +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "strings" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" -) - -// GetSecretList gets a list of all secrets from the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) GetSecretList() ([]*library.Secret, error) { - c.Logger.Tracef("listing secrets from the database") - - // variable to store query results - s := new([]database.Secret) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableSecret). - Raw(dml.ListSecrets). - Scan(s).Error - if err != nil { - return nil, err - } - - // variable we want to return - secrets := []*library.Secret{} - // iterate through all query results - for _, secret := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := secret - - // decrypt the value for the secret - // - // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted secrets - c.Logger.Errorf("unable to decrypt secret %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - secrets = append(secrets, tmp.ToLibrary()) - } - - return secrets, nil -} - -// GetTypeSecretList gets a list of secrets by type, -// owner, and name (repo or team) from the database. -// -// nolint: lll // ignore long line length -func (c *client) GetTypeSecretList(t, o, n string, page, perPage int, teams []string) ([]*library.Secret, error) { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": o, - "repo": n, - "type": t, - } - - // check if secret is a shared secret - if strings.EqualFold(t, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ - "org": o, - "team": n, - "type": t, - } - } - - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("listing %s secrets for %s/%s from the database", t, o, n) - - var err error - - // variable to store query results - s := new([]database.Secret) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - switch t { - case constants.SecretOrg: - err = c.Sqlite. - Table(constants.TableSecret). - Raw(dml.ListOrgSecrets, o, perPage, offset). - Scan(s).Error - case constants.SecretRepo: - err = c.Sqlite. - Table(constants.TableSecret). - Raw(dml.ListRepoSecrets, o, n, perPage, offset). - Scan(s).Error - case constants.SecretShared: - if n == "*" { - // GitHub teams are not case-sensitive, the DB is lowercase everything for matching - var lowerTeams []string - for _, t := range teams { - lowerTeams = append(lowerTeams, strings.ToLower(t)) - } - err = c.Sqlite. - Table(constants.TableSecret). - Where("type = 'shared' AND org = ?", o). - Where("LOWER(team) IN (?)", lowerTeams). - Order("id DESC"). - Limit(perPage). - Offset(offset). - Scan(s).Error - } else { - err = c.Sqlite. - Table(constants.TableSecret). - Raw(dml.ListSharedSecrets, o, n, perPage, offset). - Scan(s).Error - } - } - if err != nil { - return nil, err - } - - // variable we want to return - secrets := []*library.Secret{} - // iterate through all query results - for _, secret := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := secret - - // decrypt the value for the secret - // - // https://pkg.go.dev/github.com/go-vela/types/database#Secret.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted secrets - c.Logger.Errorf("unable to decrypt secret %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - secrets = append(secrets, tmp.ToLibrary()) - } - - return secrets, nil -} diff --git a/database/sqlite/secret_list_test.go b/database/sqlite/secret_list_test.go deleted file mode 100644 index f9a48a183..000000000 --- a/database/sqlite/secret_list_test.go +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the secret table - err = _database.Sqlite.Exec(ddl.CreateSecretTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableSecret, err) - } -} - -func TestSqlite_Client_GetSecretList(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetRepo("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("repo") - - _secretTwo := testSecret() - _secretTwo.SetID(2) - _secretTwo.SetOrg("foo") - _secretTwo.SetRepo("bar") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("repo") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Secret - }{ - { - failure: false, - want: []*library.Secret{_secretOne, _secretTwo}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - for _, secret := range test.want { - // create the secret in the database - err := _database.CreateSecret(secret) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - } - - got, err := _database.GetSecretList() - - if test.failure { - if err == nil { - t.Errorf("GetSecretList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetSecretList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetSecretList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetTypeSecretList_Org(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetRepo("*") - _secretOne.SetName("baz") - _secretOne.SetValue("bar") - _secretOne.SetType("org") - - _secretTwo := testSecret() - _secretTwo.SetID(2) - _secretTwo.SetOrg("foo") - _secretTwo.SetRepo("*") - _secretTwo.SetName("bar") - _secretTwo.SetValue("baz") - _secretTwo.SetType("org") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Secret - }{ - { - failure: false, - want: []*library.Secret{_secretTwo, _secretOne}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - for _, secret := range test.want { - // create the secret in the database - err := _database.CreateSecret(secret) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - } - - got, err := _database.GetTypeSecretList("org", "foo", "*", 1, 10, []string{}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetTypeSecretList_Repo(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetRepo("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("repo") - - _secretTwo := testSecret() - _secretTwo.SetID(2) - _secretTwo.SetOrg("foo") - _secretTwo.SetRepo("bar") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("repo") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Secret - }{ - { - failure: false, - want: []*library.Secret{_secretTwo, _secretOne}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - for _, secret := range test.want { - // create the secret in the database - err := _database.CreateSecret(secret) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - } - - got, err := _database.GetTypeSecretList("repo", "foo", "bar", 1, 10, []string{}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetTypeSecretList_Shared(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetTeam("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("shared") - - _secretTwo := testSecret() - _secretTwo.SetID(2) - _secretTwo.SetOrg("foo") - _secretTwo.SetTeam("bar") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("shared") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Secret - }{ - { - failure: false, - want: []*library.Secret{_secretTwo, _secretOne}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - for _, secret := range test.want { - // create the secret in the database - err := _database.CreateSecret(secret) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - } - - got, err := _database.GetTypeSecretList("shared", "foo", "bar", 1, 10, []string{}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetTypeSecretList_Shared_wildcard(t *testing.T) { - // setup types - _secretOne := testSecret() - _secretOne.SetID(1) - _secretOne.SetOrg("foo") - _secretOne.SetTeam("bar") - _secretOne.SetName("baz") - _secretOne.SetValue("foob") - _secretOne.SetType("shared") - - _secretTwo := testSecret() - _secretTwo.SetID(2) - _secretTwo.SetOrg("foo") - _secretTwo.SetTeam("bared") - _secretTwo.SetName("foob") - _secretTwo.SetValue("baz") - _secretTwo.SetType("shared") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Secret - }{ - { - failure: false, - want: []*library.Secret{_secretTwo, _secretOne}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - for _, secret := range test.want { - // create the secret in the database - err := _database.CreateSecret(secret) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - } - - got, err := _database.GetTypeSecretList("shared", "foo", "*", 1, 10, []string{"bar", "bared"}) - - if test.failure { - if err == nil { - t.Errorf("GetTypeSecretList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetTypeSecretList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetTypeSecretList is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/secret_test.go b/database/sqlite/secret_test.go deleted file mode 100644 index 2d92fa710..000000000 --- a/database/sqlite/secret_test.go +++ /dev/null @@ -1,410 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/library" -) - -func TestSqlite_Client_GetSecret_Org(t *testing.T) { - // setup types - _secret := testSecret() - _secret.SetID(1) - _secret.SetOrg("foo") - _secret.SetRepo("*") - _secret.SetName("bar") - _secret.SetValue("baz") - _secret.SetType("org") - _secret.SetCreatedAt(1) - _secret.SetCreatedBy("user") - _secret.SetUpdatedAt(1) - _secret.SetUpdatedBy("user2") - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Secret - }{ - { - failure: false, - want: _secret, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the secret in the database - err := _database.CreateSecret(test.want) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - } - - got, err := _database.GetSecret("org", "foo", "*", "bar") - - // cleanup the secrets table - _ = _database.Sqlite.Exec("DELETE FROM secrets;") - - if test.failure { - if err == nil { - t.Errorf("GetSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetSecret returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetSecret is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetSecret_Repo(t *testing.T) { - // setup types - _secret := testSecret() - _secret.SetID(1) - _secret.SetOrg("foo") - _secret.SetRepo("bar") - _secret.SetName("baz") - _secret.SetValue("foob") - _secret.SetType("repo") - _secret.SetCreatedAt(1) - _secret.SetCreatedBy("user") - _secret.SetUpdatedAt(1) - _secret.SetUpdatedBy("user2") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Secret - }{ - { - failure: false, - want: _secret, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the secret in the database - err := _database.CreateSecret(test.want) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - } - - got, err := _database.GetSecret("repo", "foo", "bar", "baz") - - // cleanup the secrets table - _ = _database.Sqlite.Exec("DELETE FROM secrets;") - - if test.failure { - if err == nil { - t.Errorf("GetSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetSecret returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetSecret is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetSecret_Shared(t *testing.T) { - // setup types - _secret := testSecret() - _secret.SetID(1) - _secret.SetOrg("foo") - _secret.SetTeam("bar") - _secret.SetName("baz") - _secret.SetValue("foob") - _secret.SetType("shared") - _secret.SetCreatedAt(1) - _secret.SetCreatedBy("user") - _secret.SetUpdatedAt(1) - _secret.SetUpdatedBy("user2") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Secret - }{ - { - failure: false, - want: _secret, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the secret in the database - err := _database.CreateSecret(test.want) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - } - - got, err := _database.GetSecret("shared", "foo", "bar", "baz") - - // cleanup the secrets table - _ = _database.Sqlite.Exec("DELETE FROM secrets;") - - if test.failure { - if err == nil { - t.Errorf("GetSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetSecret returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetSecret is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_CreateSecret(t *testing.T) { - // setup types - _secret := testSecret() - _secret.SetID(1) - _secret.SetOrg("foo") - _secret.SetRepo("bar") - _secret.SetName("baz") - _secret.SetValue("foob") - _secret.SetType("repo") - _secret.SetCreatedAt(1) - _secret.SetCreatedBy("user") - _secret.SetUpdatedAt(1) - _secret.SetUpdatedBy("user2") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - err := _database.CreateSecret(_secret) - - if test.failure { - if err == nil { - t.Errorf("CreateSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateSecret returned err: %v", err) - } - } -} - -func TestSqlite_Client_UpdateSecret(t *testing.T) { - // setup types - _secret := testSecret() - _secret.SetID(1) - _secret.SetOrg("foo") - _secret.SetRepo("bar") - _secret.SetName("baz") - _secret.SetValue("foob") - _secret.SetType("repo") - _secret.SetCreatedAt(1) - _secret.SetCreatedBy("user") - _secret.SetUpdatedAt(1) - _secret.SetUpdatedBy("user2") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - // create the secret in the database - err := _database.CreateSecret(_secret) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - - err = _database.UpdateSecret(_secret) - - if test.failure { - if err == nil { - t.Errorf("UpdateSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateSecret returned err: %v", err) - } - } -} - -func TestSqlite_Client_DeleteSecret(t *testing.T) { - // setup types - _secret := testSecret() - _secret.SetID(1) - _secret.SetOrg("foo") - _secret.SetRepo("bar") - _secret.SetName("baz") - _secret.SetValue("foob") - _secret.SetType("repo") - _secret.SetCreatedAt(1) - _secret.SetCreatedBy("user") - _secret.SetUpdatedAt(1) - _secret.SetUpdatedBy("user2") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the secrets table - defer _database.Sqlite.Exec("delete from secrets;") - - // create the secret in the database - err := _database.CreateSecret(_secret) - if err != nil { - t.Errorf("unable to create test secret: %v", err) - } - - err = _database.DeleteSecret(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteSecret should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteSecret returned err: %v", err) - } - } -} - -// testSecret is a test helper function to create a -// library Secret type with all fields set to their -// zero values. -func testSecret() *library.Secret { - i64 := int64(0) - str := "" - booL := false - var arr []string - - return &library.Secret{ - ID: &i64, - Org: &str, - Repo: &str, - Team: &str, - Name: &str, - Value: &str, - Type: &str, - Images: &arr, - Events: &arr, - AllowCommand: &booL, - CreatedAt: &i64, - CreatedBy: &str, - UpdatedAt: &i64, - UpdatedBy: &str, - } -} diff --git a/database/sqlite/service.go b/database/sqlite/service.go deleted file mode 100644 index ecc9dad6b..000000000 --- a/database/sqlite/service.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "errors" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetService gets a service by number and build ID from the database. -func (c *client) GetService(number int, b *library.Build) (*library.Service, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "service": number, - }).Tracef("getting service %d for build %d from the database", number, b.GetNumber()) - - // variable to store query results - s := new(database.Service) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableService). - Raw(dml.SelectBuildService, b.GetID(), number). - Scan(s) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return s.ToLibrary(), result.Error -} - -// CreateService creates a new service in the database. -func (c *client) CreateService(s *library.Service) error { - c.Logger.WithFields(logrus.Fields{ - "service": s.GetNumber(), - }).Tracef("creating service %s in the database", s.GetName()) - - // cast to database type - service := database.ServiceFromLibrary(s) - - // validate the necessary fields are populated - err := service.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Sqlite. - Table(constants.TableService). - Create(service).Error -} - -// UpdateService updates a service in the database. -func (c *client) UpdateService(s *library.Service) error { - c.Logger.WithFields(logrus.Fields{ - "service": s.GetNumber(), - }).Tracef("updating service %s in the database", s.GetName()) - - // cast to database type - service := database.ServiceFromLibrary(s) - - // validate the necessary fields are populated - err := service.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Sqlite. - Table(constants.TableService). - Save(service).Error -} - -// DeleteService deletes a service by unique ID from the database. -func (c *client) DeleteService(id int64) error { - c.Logger.Tracef("deleting service %d from the database", id) - - // send query to the database - return c.Sqlite. - Table(constants.TableService). - Exec(dml.DeleteService, id).Error -} diff --git a/database/sqlite/service_count.go b/database/sqlite/service_count.go deleted file mode 100644 index 4979e9527..000000000 --- a/database/sqlite/service_count.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetBuildServiceCount gets a count of all services by build ID from the database. -func (c *client) GetBuildServiceCount(b *library.Build) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("getting count of services for build %d from the database", b.GetNumber()) - - // variable to store query results - var s int64 - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableService). - Raw(dml.SelectBuildServicesCount, b.GetID()). - Pluck("count", &s).Error - - return s, err -} - -// GetServiceImageCount gets a count of all service images -// and the count of their occurrence in the database. -func (c *client) GetServiceImageCount() (map[string]float64, error) { - c.Logger.Tracef("getting count of all images for services from the database") - - type imageCount struct { - Image string - Count int - } - - // variable to store query results - images := new([]imageCount) - counts := make(map[string]float64) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableService). - Raw(dml.SelectServiceImagesCount). - Scan(images).Error - - for _, image := range *images { - counts[image.Image] = float64(image.Count) - } - - return counts, err -} - -// GetServiceStatusCount gets a list of all service statuses -// and the count of their occurrence in the database. -func (c *client) GetServiceStatusCount() (map[string]float64, error) { - c.Logger.Trace("getting count of all statuses for services from the database") - - type statusCount struct { - Status string - Count int - } - - // variable to store query results - s := new([]statusCount) - counts := map[string]float64{ - "pending": 0, - "failure": 0, - "killed": 0, - "running": 0, - "success": 0, - } - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableService). - Raw(dml.SelectServiceStatusesCount). - Scan(s).Error - - for _, status := range *s { - counts[status.Status] = float64(status.Count) - } - - return counts, err -} diff --git a/database/sqlite/service_count_test.go b/database/sqlite/service_count_test.go deleted file mode 100644 index 7b0d5eaba..000000000 --- a/database/sqlite/service_count_test.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the service table - err = _database.Sqlite.Exec(ddl.CreateServiceTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableService, err) - } -} - -func TestSqlite_Client_GetBuildServiceCount(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _serviceOne := testService() - _serviceOne.SetID(1) - _serviceOne.SetRepoID(1) - _serviceOne.SetBuildID(1) - _serviceOne.SetNumber(1) - _serviceOne.SetName("foo") - _serviceOne.SetImage("bar") - - _serviceTwo := testService() - _serviceTwo.SetID(2) - _serviceTwo.SetRepoID(1) - _serviceTwo.SetBuildID(1) - _serviceTwo.SetNumber(2) - _serviceTwo.SetName("bar") - _serviceTwo.SetImage("foo") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the services table - defer _database.Sqlite.Exec("delete from services;") - - // create the services in the database - err := _database.CreateService(_serviceOne) - if err != nil { - t.Errorf("unable to create test service: %v", err) - } - - err = _database.CreateService(_serviceTwo) - if err != nil { - t.Errorf("unable to create test service: %v", err) - } - - got, err := _database.GetBuildServiceCount(_build) - - if test.failure { - if err == nil { - t.Errorf("GetBuildServiceCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildServiceCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildServiceCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetServiceImageCount(t *testing.T) { - // setup types - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want map[string]float64 - }{ - { - failure: false, - want: map[string]float64{}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetServiceImageCount() - - if test.failure { - if err == nil { - t.Errorf("GetServiceImageCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetServiceImageCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetServiceImageCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetServiceStatusCount(t *testing.T) { - // setup types - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want map[string]float64 - }{ - { - failure: false, - want: map[string]float64{ - "pending": 0, - "failure": 0, - "killed": 0, - "running": 0, - "success": 0, - }, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetServiceStatusCount() - - if test.failure { - if err == nil { - t.Errorf("GetServiceStatusCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetServiceStatusCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetServiceStatusCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/service_list.go b/database/sqlite/service_list.go deleted file mode 100644 index 390fbdf76..000000000 --- a/database/sqlite/service_list.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetServiceList gets a list of all services from the database. -func (c *client) GetServiceList() ([]*library.Service, error) { - c.Logger.Trace("listing services from the database") - - // variable to store query results - s := new([]database.Service) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableService). - Raw(dml.ListServices). - Scan(s).Error - - // variable we want to return - services := []*library.Service{} - // iterate through all query results - for _, service := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := service - - // convert query result to library type - services = append(services, tmp.ToLibrary()) - } - - return services, err -} - -// GetBuildServiceList gets a list of services by build ID from the database. -// -// nolint: lll // ignore long line length due to parameters -func (c *client) GetBuildServiceList(b *library.Build, page, perPage int) ([]*library.Service, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("listing services for build %d from the database", b.GetNumber()) - - // variable to store query results - s := new([]database.Service) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableService). - Raw(dml.ListBuildServices, b.GetID(), perPage, offset). - Scan(s).Error - - // variable we want to return - services := []*library.Service{} - // iterate through all query results - for _, service := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := service - - // convert query result to library type - services = append(services, tmp.ToLibrary()) - } - - return services, err -} diff --git a/database/sqlite/service_list_test.go b/database/sqlite/service_list_test.go deleted file mode 100644 index 200316325..000000000 --- a/database/sqlite/service_list_test.go +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the service table - err = _database.Sqlite.Exec(ddl.CreateServiceTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableService, err) - } -} - -func TestSqlite_Client_GetServiceList(t *testing.T) { - // setup types - _serviceOne := testService() - _serviceOne.SetID(1) - _serviceOne.SetRepoID(1) - _serviceOne.SetBuildID(1) - _serviceOne.SetNumber(1) - _serviceOne.SetName("foo") - _serviceOne.SetImage("bar") - - _serviceTwo := testService() - _serviceTwo.SetID(2) - _serviceTwo.SetRepoID(1) - _serviceTwo.SetBuildID(1) - _serviceTwo.SetNumber(2) - _serviceTwo.SetName("bar") - _serviceTwo.SetImage("foo") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Service - }{ - { - failure: false, - want: []*library.Service{_serviceOne, _serviceTwo}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the services table - defer _database.Sqlite.Exec("delete from services;") - - for _, service := range test.want { - // create the service in the database - err := _database.CreateService(service) - if err != nil { - t.Errorf("unable to create test service: %v", err) - } - } - - got, err := _database.GetServiceList() - - if test.failure { - if err == nil { - t.Errorf("GetServiceList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetServiceList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetServiceList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetBuildServiceList(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _serviceOne := testService() - _serviceOne.SetID(1) - _serviceOne.SetRepoID(1) - _serviceOne.SetBuildID(1) - _serviceOne.SetNumber(1) - _serviceOne.SetName("foo") - _serviceOne.SetImage("bar") - - _serviceTwo := testService() - _serviceTwo.SetID(2) - _serviceTwo.SetRepoID(1) - _serviceTwo.SetBuildID(1) - _serviceTwo.SetNumber(2) - _serviceTwo.SetName("bar") - _serviceTwo.SetImage("foo") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Service - }{ - { - failure: false, - want: []*library.Service{_serviceTwo, _serviceOne}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the services table - defer _database.Sqlite.Exec("delete from services;") - - for _, service := range test.want { - // create the service in the database - err := _database.CreateService(service) - if err != nil { - t.Errorf("unable to create test service: %v", err) - } - } - - got, err := _database.GetBuildServiceList(_build, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetBuildServiceList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildServiceList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildServiceList is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/service_test.go b/database/sqlite/service_test.go deleted file mode 100644 index 006a3ece6..000000000 --- a/database/sqlite/service_test.go +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/library" -) - -func TestSqlite_Client_GetService(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _service := testService() - _service.SetID(1) - _service.SetRepoID(1) - _service.SetBuildID(1) - _service.SetNumber(1) - _service.SetName("foo") - _service.SetImage("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Service - }{ - { - failure: false, - want: _service, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the service in the database - err := _database.CreateService(test.want) - if err != nil { - t.Errorf("unable to create test service: %v", err) - } - } - - got, err := _database.GetService(1, _build) - - // cleanup the services table - _ = _database.Sqlite.Exec("DELETE FROM services;") - - if test.failure { - if err == nil { - t.Errorf("GetService should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetService returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetService is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_CreateService(t *testing.T) { - // setup types - _service := testService() - _service.SetID(1) - _service.SetRepoID(1) - _service.SetBuildID(1) - _service.SetNumber(1) - _service.SetName("foo") - _service.SetImage("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the services table - defer _database.Sqlite.Exec("delete from services;") - - err := _database.CreateService(_service) - - if test.failure { - if err == nil { - t.Errorf("CreateService should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateService returned err: %v", err) - } - } -} - -func TestSqlite_Client_UpdateService(t *testing.T) { - // setup types - _service := testService() - _service.SetID(1) - _service.SetRepoID(1) - _service.SetBuildID(1) - _service.SetNumber(1) - _service.SetName("foo") - _service.SetImage("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the services table - defer _database.Sqlite.Exec("delete from services;") - - // create the service in the database - err := _database.CreateService(_service) - if err != nil { - t.Errorf("unable to create test service: %v", err) - } - - err = _database.UpdateService(_service) - - if test.failure { - if err == nil { - t.Errorf("UpdateService should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateService returned err: %v", err) - } - } -} - -func TestSqlite_Client_DeleteService(t *testing.T) { - // setup types - _service := testService() - _service.SetID(1) - _service.SetRepoID(1) - _service.SetBuildID(1) - _service.SetNumber(1) - _service.SetName("foo") - _service.SetImage("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the services table - defer _database.Sqlite.Exec("delete from services;") - - // create the service in the database - err := _database.CreateService(_service) - if err != nil { - t.Errorf("unable to create test service: %v", err) - } - - err = _database.DeleteService(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteService should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteService returned err: %v", err) - } - } -} - -// testService is a test helper function to create a -// library Service type with all fields set to their -// zero values. -func testService() *library.Service { - i64 := int64(0) - i := 0 - str := "" - - return &library.Service{ - ID: &i64, - BuildID: &i64, - RepoID: &i64, - Number: &i, - Name: &str, - Image: &str, - Status: &str, - Error: &str, - ExitCode: &i, - Created: &i64, - Started: &i64, - Finished: &i64, - Host: &str, - Runtime: &str, - Distribution: &str, - } -} diff --git a/database/sqlite/sqlite.go b/database/sqlite/sqlite.go deleted file mode 100644 index 6750540bd..000000000 --- a/database/sqlite/sqlite.go +++ /dev/null @@ -1,336 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "fmt" - "time" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" - "github.com/sirupsen/logrus" - - "gorm.io/driver/sqlite" - "gorm.io/gorm" -) - -type ( - config struct { - // specifies the address to use for the Sqlite client - Address string - // specifies the level of compression to use for the Sqlite client - CompressionLevel int - // specifies the connection duration to use for the Sqlite client - ConnectionLife time.Duration - // specifies the maximum idle connections for the Sqlite client - ConnectionIdle int - // specifies the maximum open connections for the Sqlite client - ConnectionOpen int - // specifies the encryption key to use for the Sqlite client - EncryptionKey string - // specifies to skip creating tables and indexes for the Sqlite client - SkipCreation bool - } - - client struct { - config *config - Sqlite *gorm.DB - // https://pkg.go.dev/github.com/sirupsen/logrus#Entry - Logger *logrus.Entry - } -) - -// New returns a Database implementation that integrates with a Sqlite instance. -// -// nolint: revive // ignore returning unexported client -func New(opts ...ClientOpt) (*client, error) { - // create new Sqlite client - c := new(client) - - // create new fields - c.config = new(config) - c.Sqlite = new(gorm.DB) - - // create new logger for the client - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#StandardLogger - logger := logrus.StandardLogger() - - // create new logger for the client - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#NewEntry - c.Logger = logrus.NewEntry(logger).WithField("database", c.Driver()) - - // apply all provided configuration options - for _, opt := range opts { - err := opt(c) - if err != nil { - return nil, err - } - } - - // create the new Sqlite database client - // - // https://pkg.go.dev/gorm.io/gorm#Open - _sqlite, err := gorm.Open(sqlite.Open(c.config.Address), &gorm.Config{}) - if err != nil { - return nil, err - } - - // set the Sqlite database client in the Sqlite client - c.Sqlite = _sqlite - - // setup database with proper configuration - err = setupDatabase(c) - if err != nil { - return nil, err - } - - return c, nil -} - -// NewTest returns a Database implementation that integrates with a fake Sqlite instance. -// -// This function is intended for running tests only. -// -// nolint: revive // ignore returning unexported client -func NewTest() (*client, error) { - // create new Sqlite client - c := new(client) - - // create new fields - c.config = &config{ - CompressionLevel: 3, - ConnectionLife: 30 * time.Minute, - ConnectionIdle: 2, - ConnectionOpen: 0, - EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", - SkipCreation: false, - } - c.Sqlite = new(gorm.DB) - - // create new logger for the client - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#StandardLogger - logger := logrus.StandardLogger() - - // create new logger for the client - // - // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#NewEntry - c.Logger = logrus.NewEntry(logger) - - // create the new Sqlite database client - // - // https://pkg.go.dev/gorm.io/gorm#Open - _sqlite, err := gorm.Open( - sqlite.Open("file::memory:?cache=shared"), - &gorm.Config{SkipDefaultTransaction: true}, - ) - if err != nil { - return nil, err - } - - c.Sqlite = _sqlite - - // create the tables in the database - err = createTables(c) - if err != nil { - return nil, err - } - - return c, nil -} - -// setupDatabase is a helper function to setup -// the database with the proper configuration. -func setupDatabase(c *client) error { - // capture database/sql database from gorm database - // - // https://pkg.go.dev/gorm.io/gorm#DB.DB - _sql, err := c.Sqlite.DB() - if err != nil { - return err - } - - // set the maximum amount of time a connection may be reused - // - // https://golang.org/pkg/database/sql/#DB.SetConnMaxLifetime - _sql.SetConnMaxLifetime(c.config.ConnectionLife) - - // set the maximum number of connections in the idle connection pool - // - // https://golang.org/pkg/database/sql/#DB.SetMaxIdleConns - _sql.SetMaxIdleConns(c.config.ConnectionIdle) - - // set the maximum number of open connections to the database - // - // https://golang.org/pkg/database/sql/#DB.SetMaxOpenConns - _sql.SetMaxOpenConns(c.config.ConnectionOpen) - - // verify connection to the database - err = c.Ping() - if err != nil { - return err - } - - // check if we should skip creating database objects - if c.config.SkipCreation { - c.Logger.Warning("skipping creation of data tables and indexes in the sqlite database") - - return nil - } - - // create the tables in the database - err = createTables(c) - if err != nil { - return err - } - - // create the indexes in the database - err = createIndexes(c) - if err != nil { - return err - } - - return nil -} - -// createTables is a helper function to setup -// the database with the necessary tables. -func createTables(c *client) error { - c.Logger.Trace("creating data tables in the sqlite database") - - // create the builds table - err := c.Sqlite.Exec(ddl.CreateBuildTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableBuild, err) - } - - // create the hooks table - err = c.Sqlite.Exec(ddl.CreateHookTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableHook, err) - } - - // create the logs table - err = c.Sqlite.Exec(ddl.CreateLogTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableLog, err) - } - - // create the repos table - err = c.Sqlite.Exec(ddl.CreateRepoTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableRepo, err) - } - - // create the secrets table - err = c.Sqlite.Exec(ddl.CreateSecretTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableSecret, err) - } - - // create the services table - err = c.Sqlite.Exec(ddl.CreateServiceTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableService, err) - } - - // create the steps table - err = c.Sqlite.Exec(ddl.CreateStepTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableStep, err) - } - - // create the users table - err = c.Sqlite.Exec(ddl.CreateUserTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableUser, err) - } - - // create the workers table - err = c.Sqlite.Exec(ddl.CreateWorkerTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %v", constants.TableWorker, err) - } - - return nil -} - -// createIndexes is a helper function to setup -// the database with the necessary indexes. -// -// nolint: lll // ignore long line length due to error messages -func createIndexes(c *client) error { - c.Logger.Trace("creating data indexes in the sqlite database") - - // create the builds_repo_id index for the builds table - err := c.Sqlite.Exec(ddl.CreateBuildRepoIDIndex).Error - if err != nil { - return fmt.Errorf("unable to create builds_repo_id index for the %s table: %v", constants.TableBuild, err) - } - - // create the builds_status index for the builds table - err = c.Sqlite.Exec(ddl.CreateBuildStatusIndex).Error - if err != nil { - return fmt.Errorf("unable to create builds_status index for the %s table: %v", constants.TableBuild, err) - } - - // create the builds_created index for the builds table - err = c.Sqlite.Exec(ddl.CreateBuildCreatedIndex).Error - if err != nil { - return fmt.Errorf("unable to create builds_created index for the %s table: %v", constants.TableBuild, err) - } - - // create the hooks_repo_id index for the hooks table - err = c.Sqlite.Exec(ddl.CreateHookRepoIDIndex).Error - if err != nil { - return fmt.Errorf("unable to create hooks_repo_id index for the %s table: %v", constants.TableHook, err) - } - - // create the logs_build_id index for the logs table - err = c.Sqlite.Exec(ddl.CreateLogBuildIDIndex).Error - if err != nil { - return fmt.Errorf("unable to create logs_build_id index for the %s table: %v", constants.TableLog, err) - } - - // create the repos_org_name index for the repos table - err = c.Sqlite.Exec(ddl.CreateRepoOrgNameIndex).Error - if err != nil { - return fmt.Errorf("unable to create repos_org_name index for the %s table: %v", constants.TableRepo, err) - } - - // create the secrets_type_org_repo index for the secrets table - err = c.Sqlite.Exec(ddl.CreateSecretTypeOrgRepo).Error - if err != nil { - return fmt.Errorf("unable to create secrets_type_org_repo index for the %s table: %v", constants.TableSecret, err) - } - - // create the secrets_type_org_team index for the secrets table - err = c.Sqlite.Exec(ddl.CreateSecretTypeOrgTeam).Error - if err != nil { - return fmt.Errorf("unable to create secrets_type_org_team index for the %s table: %v", constants.TableSecret, err) - } - - // create the secrets_type_org index for the secrets table - err = c.Sqlite.Exec(ddl.CreateSecretTypeOrg).Error - if err != nil { - return fmt.Errorf("unable to create secrets_type_org index for the %s table: %v", constants.TableSecret, err) - } - - // create the users_refresh index for the users table - err = c.Sqlite.Exec(ddl.CreateUserRefreshIndex).Error - if err != nil { - return fmt.Errorf("unable to create users_refresh index for the %s table: %v", constants.TableUser, err) - } - - // create the workers_hostname_address index for the workers table - err = c.Sqlite.Exec(ddl.CreateWorkerHostnameAddressIndex).Error - if err != nil { - return fmt.Errorf("unable to create workers_hostname_address index for the %s table: %v", constants.TableWorker, err) - } - - return nil -} diff --git a/database/sqlite/sqlite_test.go b/database/sqlite/sqlite_test.go deleted file mode 100644 index 99831a70b..000000000 --- a/database/sqlite/sqlite_test.go +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "testing" - "time" -) - -func TestSqlite_New(t *testing.T) { - // setup tests - tests := []struct { - failure bool - address string - want string - }{ - { - failure: false, - address: ":memory:", - want: ":memory:", - }, - { - failure: true, - address: "", - want: "", - }, - } - - // run tests - for _, test := range tests { - _, err := New( - WithAddress(test.address), - WithCompressionLevel(3), - WithConnectionLife(10*time.Second), - WithConnectionIdle(5), - WithConnectionOpen(20), - WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), - WithSkipCreation(false), - ) - - if test.failure { - if err == nil { - t.Errorf("New should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("New returned err: %v", err) - } - } -} - -func TestSqlite_setupDatabase(t *testing.T) { - // setup types - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup the skip test database client - _skipDatabase, err := NewTest() - if err != nil { - t.Errorf("unable to create new skip sqlite test database: %v", err) - } - defer func() { _sql, _ := _skipDatabase.Sqlite.DB(); _sql.Close() }() - - err = WithSkipCreation(true)(_skipDatabase) - if err != nil { - t.Errorf("unable to set SkipCreation for sqlite test database: %v", err) - } - - tests := []struct { - failure bool - database *client - }{ - { - failure: false, - database: _database, - }, - { - failure: false, - database: _skipDatabase, - }, - } - - // run tests - for _, test := range tests { - err := setupDatabase(test.database) - - if test.failure { - if err == nil { - t.Errorf("setupDatabase should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("setupDatabase returned err: %v", err) - } - } -} - -func TestSqlite_createTables(t *testing.T) { - // setup types - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := createTables(_database) - - if test.failure { - if err == nil { - t.Errorf("createTables should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("createTables returned err: %v", err) - } - } -} - -func TestPostgres_createIndexes(t *testing.T) { - // setup types - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := createIndexes(_database) - - if test.failure { - if err == nil { - t.Errorf("createIndexes should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("createIndexes returned err: %v", err) - } - } -} diff --git a/database/sqlite/step.go b/database/sqlite/step.go deleted file mode 100644 index f055b2ebe..000000000 --- a/database/sqlite/step.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "errors" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetStep gets a step by number and build ID from the database. -func (c *client) GetStep(number int, b *library.Build) (*library.Step, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - "step": number, - }).Tracef("getting step %d for build %d from the database", number, b.GetNumber()) - - // variable to store query results - s := new(database.Step) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableStep). - Raw(dml.SelectBuildStep, b.GetID(), number). - Scan(s) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return s.ToLibrary(), result.Error -} - -// CreateStep creates a new step in the database. -func (c *client) CreateStep(s *library.Step) error { - c.Logger.WithFields(logrus.Fields{ - "step": s.GetNumber(), - }).Tracef("creating step %s in the database", s.GetName()) - - // cast to database type - step := database.StepFromLibrary(s) - - // validate the necessary fields are populated - err := step.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Sqlite. - Table(constants.TableStep). - Create(step).Error -} - -// UpdateStep updates a step in the database. -func (c *client) UpdateStep(s *library.Step) error { - c.Logger.WithFields(logrus.Fields{ - "step": s.GetNumber(), - }).Tracef("updating step %s in the database", s.GetName()) - - // cast to database type - step := database.StepFromLibrary(s) - - // validate the necessary fields are populated - err := step.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Sqlite. - Table(constants.TableStep). - Save(step).Error -} - -// DeleteStep deletes a step by unique ID from the database. -func (c *client) DeleteStep(id int64) error { - c.Logger.Tracef("deleting step %d from the database", id) - - // send query to the database - return c.Sqlite. - Table(constants.TableStep). - Exec(dml.DeleteStep, id).Error -} diff --git a/database/sqlite/step_count.go b/database/sqlite/step_count.go deleted file mode 100644 index 1d7e45c87..000000000 --- a/database/sqlite/step_count.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetBuildStepCount gets a count of all steps by build ID from the database. -func (c *client) GetBuildStepCount(b *library.Build) (int64, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("getting count of steps for build %d from the database", b.GetNumber()) - - // variable to store query results - var s int64 - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableStep). - Raw(dml.SelectBuildStepsCount, b.GetID()). - Pluck("count", &s).Error - - return s, err -} - -// GetStepImageCount gets a count of all step images -// and the count of their occurrence in the database. -func (c *client) GetStepImageCount() (map[string]float64, error) { - c.Logger.Tracef("getting count of all images for steps from the database") - - type imageCount struct { - Image string - Count int - } - - // variable to store query results - images := new([]imageCount) - counts := make(map[string]float64) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableStep). - Raw(dml.SelectStepImagesCount). - Scan(images).Error - - for _, image := range *images { - counts[image.Image] = float64(image.Count) - } - - return counts, err -} - -// GetStepStatusCount gets a list of all step statuses -// and the count of their occurrence in the database. -func (c *client) GetStepStatusCount() (map[string]float64, error) { - c.Logger.Trace("getting count of all statuses for steps from the database") - - type statusCount struct { - Status string - Count int - } - - // variable to store query results - s := new([]statusCount) - counts := map[string]float64{ - "pending": 0, - "failure": 0, - "killed": 0, - "running": 0, - "success": 0, - } - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableStep). - Raw(dml.SelectStepStatusesCount). - Scan(s).Error - - for _, status := range *s { - counts[status.Status] = float64(status.Count) - } - - return counts, err -} diff --git a/database/sqlite/step_count_test.go b/database/sqlite/step_count_test.go deleted file mode 100644 index 1ac7e5f39..000000000 --- a/database/sqlite/step_count_test.go +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the step table - err = _database.Sqlite.Exec(ddl.CreateStepTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableStep, err) - } -} - -func TestSqlite_Client_GetBuildStepCount(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _stepOne := testStep() - _stepOne.SetID(1) - _stepOne.SetRepoID(1) - _stepOne.SetBuildID(1) - _stepOne.SetNumber(1) - _stepOne.SetName("foo") - _stepOne.SetImage("bar") - - _stepTwo := testStep() - _stepTwo.SetID(2) - _stepTwo.SetRepoID(1) - _stepTwo.SetBuildID(1) - _stepTwo.SetNumber(2) - _stepTwo.SetName("bar") - _stepTwo.SetImage("foo") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the steps table - defer _database.Sqlite.Exec("delete from steps;") - - // create the steps in the database - err := _database.CreateStep(_stepOne) - if err != nil { - t.Errorf("unable to create test step: %v", err) - } - - err = _database.CreateStep(_stepTwo) - if err != nil { - t.Errorf("unable to create test step: %v", err) - } - - got, err := _database.GetBuildStepCount(_build) - - if test.failure { - if err == nil { - t.Errorf("GetBuildStepCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildStepCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildStepCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetStepImageCount(t *testing.T) { - // setup types - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want map[string]float64 - }{ - { - failure: false, - want: map[string]float64{}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetStepImageCount() - - if test.failure { - if err == nil { - t.Errorf("GetStepImageCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetStepImageCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetStepImageCount is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetStepStatusCount(t *testing.T) { - // setup types - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want map[string]float64 - }{ - { - failure: false, - want: map[string]float64{ - "pending": 0, - "failure": 0, - "killed": 0, - "running": 0, - "success": 0, - }, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetStepStatusCount() - - if test.failure { - if err == nil { - t.Errorf("GetStepStatusCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetStepStatusCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetStepStatusCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/step_list.go b/database/sqlite/step_list.go deleted file mode 100644 index 4c82fa858..000000000 --- a/database/sqlite/step_list.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" -) - -// GetStepList gets a list of all steps from the database. -func (c *client) GetStepList() ([]*library.Step, error) { - c.Logger.Trace("listing steps from the database") - - // variable to store query results - s := new([]database.Step) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableStep). - Raw(dml.ListSteps). - Scan(s).Error - - // variable we want to return - steps := []*library.Step{} - // iterate through all query results - for _, step := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := step - - // convert query result to library type - steps = append(steps, tmp.ToLibrary()) - } - - return steps, err -} - -// GetBuildStepList gets a list of steps by build ID from the database. -func (c *client) GetBuildStepList(b *library.Build, page, perPage int) ([]*library.Step, error) { - c.Logger.WithFields(logrus.Fields{ - "build": b.GetNumber(), - }).Tracef("listing steps for build %d from the database", b.GetNumber()) - - // variable to store query results - s := new([]database.Step) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableStep). - Raw(dml.ListBuildSteps, b.GetID(), perPage, offset). - Scan(s).Error - - // variable we want to return - steps := []*library.Step{} - // iterate through all query results - for _, step := range *s { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := step - - // convert query result to library type - steps = append(steps, tmp.ToLibrary()) - } - - return steps, err -} diff --git a/database/sqlite/step_list_test.go b/database/sqlite/step_list_test.go deleted file mode 100644 index 23303adf5..000000000 --- a/database/sqlite/step_list_test.go +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the step table - err = _database.Sqlite.Exec(ddl.CreateStepTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableStep, err) - } -} - -func TestSqlite_Client_GetStepList(t *testing.T) { - // setup types - _stepOne := testStep() - _stepOne.SetID(1) - _stepOne.SetRepoID(1) - _stepOne.SetBuildID(1) - _stepOne.SetNumber(1) - _stepOne.SetName("foo") - _stepOne.SetImage("bar") - - _stepTwo := testStep() - _stepTwo.SetID(2) - _stepTwo.SetRepoID(1) - _stepTwo.SetBuildID(1) - _stepTwo.SetNumber(2) - _stepTwo.SetName("bar") - _stepTwo.SetImage("foo") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Step - }{ - { - failure: false, - want: []*library.Step{_stepOne, _stepTwo}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the steps table - defer _database.Sqlite.Exec("delete from steps;") - - for _, step := range test.want { - // create the step in the database - err := _database.CreateStep(step) - if err != nil { - t.Errorf("unable to create test step: %v", err) - } - } - - got, err := _database.GetStepList() - - if test.failure { - if err == nil { - t.Errorf("GetStepList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetStepList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetStepList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetBuildStepList(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _stepOne := testStep() - _stepOne.SetID(1) - _stepOne.SetRepoID(1) - _stepOne.SetBuildID(1) - _stepOne.SetNumber(1) - _stepOne.SetName("foo") - _stepOne.SetImage("bar") - - _stepTwo := testStep() - _stepTwo.SetID(2) - _stepTwo.SetRepoID(1) - _stepTwo.SetBuildID(1) - _stepTwo.SetNumber(2) - _stepTwo.SetName("bar") - _stepTwo.SetImage("foo") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Step - }{ - { - failure: false, - want: []*library.Step{_stepTwo, _stepOne}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the steps table - defer _database.Sqlite.Exec("delete from steps;") - - for _, step := range test.want { - // create the step in the database - err := _database.CreateStep(step) - if err != nil { - t.Errorf("unable to create test step: %v", err) - } - } - - got, err := _database.GetBuildStepList(_build, 1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetBuildStepList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetBuildStepList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetBuildStepList is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/step_test.go b/database/sqlite/step_test.go deleted file mode 100644 index 52d73e47a..000000000 --- a/database/sqlite/step_test.go +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/library" -) - -func TestSqlite_Client_GetStep(t *testing.T) { - // setup types - _build := testBuild() - _build.SetID(1) - _build.SetRepoID(1) - _build.SetNumber(1) - - _step := testStep() - _step.SetID(1) - _step.SetRepoID(1) - _step.SetBuildID(1) - _step.SetNumber(1) - _step.SetName("foo") - _step.SetImage("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Step - }{ - { - failure: false, - want: _step, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the step in the database - err := _database.CreateStep(test.want) - if err != nil { - t.Errorf("unable to create test step: %v", err) - } - } - - got, err := _database.GetStep(1, _build) - - // cleanup the steps table - _ = _database.Sqlite.Exec("DELETE FROM steps;") - - if test.failure { - if err == nil { - t.Errorf("GetStep should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetStep returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetStep is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_CreateStep(t *testing.T) { - // setup types - _step := testStep() - _step.SetID(1) - _step.SetRepoID(1) - _step.SetBuildID(1) - _step.SetNumber(1) - _step.SetName("foo") - _step.SetImage("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the steps table - defer _database.Sqlite.Exec("delete from steps;") - - err := _database.CreateStep(_step) - - if test.failure { - if err == nil { - t.Errorf("CreateStep should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateStep returned err: %v", err) - } - } -} - -func TestSqlite_Client_UpdateStep(t *testing.T) { - // setup types - _step := testStep() - _step.SetID(1) - _step.SetRepoID(1) - _step.SetBuildID(1) - _step.SetNumber(1) - _step.SetName("foo") - _step.SetImage("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the steps table - defer _database.Sqlite.Exec("delete from steps;") - - // create the step in the database - err := _database.CreateStep(_step) - if err != nil { - t.Errorf("unable to create test step: %v", err) - } - - err = _database.UpdateStep(_step) - - if test.failure { - if err == nil { - t.Errorf("UpdateStep should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateStep returned err: %v", err) - } - } -} - -func TestSqlite_Client_DeleteStep(t *testing.T) { - // setup types - _step := testStep() - _step.SetID(1) - _step.SetRepoID(1) - _step.SetBuildID(1) - _step.SetNumber(1) - _step.SetName("foo") - _step.SetImage("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the steps table - defer _database.Sqlite.Exec("delete from steps;") - - // create the step in the database - err := _database.CreateStep(_step) - if err != nil { - t.Errorf("unable to create test step: %v", err) - } - - err = _database.DeleteStep(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteStep should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteStep returned err: %v", err) - } - } -} - -// testStep is a test helper function to create a -// library Step type with all fields set to their -// zero values. -func testStep() *library.Step { - i64 := int64(0) - i := 0 - str := "" - - return &library.Step{ - ID: &i64, - BuildID: &i64, - RepoID: &i64, - Number: &i, - Name: &str, - Image: &str, - Stage: &str, - Status: &str, - Error: &str, - ExitCode: &i, - Created: &i64, - Started: &i64, - Finished: &i64, - Host: &str, - Runtime: &str, - Distribution: &str, - } -} diff --git a/database/sqlite/user.go b/database/sqlite/user.go deleted file mode 100644 index dae174edd..000000000 --- a/database/sqlite/user.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "errors" - "fmt" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetUser gets a user by unique ID from the database. -func (c *client) GetUser(id int64) (*library.User, error) { - c.Logger.Tracef("getting user %d from the database", id) - - // variable to store query results - u := new(database.User) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableUser). - Raw(dml.SelectUser, id). - Scan(u) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err := u.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %d: %v", id, err) - - // return the unencrypted user - return u.ToLibrary(), result.Error - } - - // return the decrypted user - return u.ToLibrary(), result.Error -} - -// GetUserName gets a user by name from the database. -func (c *client) GetUserName(name string) (*library.User, error) { - c.Logger.WithFields(logrus.Fields{ - "user": name, - }).Tracef("getting user %s from the database", name) - - // variable to store query results - u := new(database.User) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableUser). - Raw(dml.SelectUserName, name). - Scan(u) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err := u.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %s: %v", name, err) - - // return the unencrypted user - return u.ToLibrary(), result.Error - } - - // return the decrypted user - return u.ToLibrary(), result.Error -} - -// CreateUser creates a new user in the database. -// -// nolint: dupl // ignore similar code with update -func (c *client) CreateUser(u *library.User) error { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("creating user %s in the database", u.GetName()) - - // cast to database type - // - // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary - user := database.UserFromLibrary(u) - - // validate the necessary fields are populated - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate - err := user.Validate() - if err != nil { - return err - } - - // encrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt - err = user.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt user %s: %v", u.GetName(), err) - } - - // send query to the database - return c.Sqlite. - Table(constants.TableUser). - Create(user).Error -} - -// UpdateUser updates a user in the database. -// -// nolint: dupl // ignore similar code with create -func (c *client) UpdateUser(u *library.User) error { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("updating user %s in the database", u.GetName()) - - // cast to database type - // - // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary - user := database.UserFromLibrary(u) - - // validate the necessary fields are populated - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate - err := user.Validate() - if err != nil { - return err - } - - // encrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt - err = user.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt user %s: %v", u.GetName(), err) - } - - // send query to the database - return c.Sqlite. - Table(constants.TableUser). - Save(user).Error -} - -// DeleteUser deletes a user by unique ID from the database. -func (c *client) DeleteUser(id int64) error { - c.Logger.Tracef("deleting user %d from the database", id) - - // send query to the database - return c.Sqlite. - Table(constants.TableUser). - Exec(dml.DeleteUser, id).Error -} diff --git a/database/sqlite/user_count_test.go b/database/sqlite/user_count_test.go deleted file mode 100644 index f866f0aa7..000000000 --- a/database/sqlite/user_count_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the user table - err = _database.Sqlite.Exec(ddl.CreateUserTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableUser, err) - } -} - -func TestSqlite_Client_GetUserCount(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - _userOne.SetToken("bar") - _userOne.SetHash("baz") - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - _userTwo.SetToken("foo") - _userTwo.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - // create the users in the database - err := _database.CreateUser(_userOne) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - - err = _database.CreateUser(_userTwo) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - - got, err := _database.GetUserCount() - - if test.failure { - if err == nil { - t.Errorf("GetUserCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/user_list.go b/database/sqlite/user_list.go deleted file mode 100644 index d90863d6b..000000000 --- a/database/sqlite/user_list.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" -) - -// GetUserList gets a list of all users from the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) GetUserList() ([]*library.User, error) { - c.Logger.Trace("listing users from the database") - - // variable to store query results - u := new([]database.User) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableUser). - Raw(dml.ListUsers). - Scan(u).Error - if err != nil { - return nil, err - } - - // variable we want to return - users := []*library.User{} - // iterate through all query results - for _, user := range *u { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := user - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary - users = append(users, tmp.ToLibrary()) - } - - return users, nil -} - -// GetUserLiteList gets a lite list of all users from the database. -func (c *client) GetUserLiteList(page, perPage int) ([]*library.User, error) { - c.Logger.Trace("listing lite users from the database") - - // variable to store query results - u := new([]database.User) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableUser). - Raw(dml.ListLiteUsers, perPage, offset). - Scan(u).Error - - // variable we want to return - users := []*library.User{} - // iterate through all query results - for _, user := range *u { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := user - - // convert query result to library type - users = append(users, tmp.ToLibrary()) - } - - return users, err -} diff --git a/database/sqlite/user_list_test.go b/database/sqlite/user_list_test.go deleted file mode 100644 index 0ee623f72..000000000 --- a/database/sqlite/user_list_test.go +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the user table - err = _database.Sqlite.Exec(ddl.CreateUserTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableUser, err) - } -} - -func TestSqlite_Client_GetUserList(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - _userOne.SetToken("bar") - _userOne.SetHash("baz") - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - _userTwo.SetToken("foo") - _userTwo.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.User - }{ - { - failure: false, - want: []*library.User{_userOne, _userTwo}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - for _, user := range test.want { - // create the user in the database - err := _database.CreateUser(user) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - } - - got, err := _database.GetUserList() - - if test.failure { - if err == nil { - t.Errorf("GetUserList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetUserLiteList(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.User - }{ - { - failure: false, - want: []*library.User{_userTwo, _userOne}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - for _, user := range test.want { - // set the required fields for the user - user.SetToken("baz") - user.SetHash("foob") - - // create the user in the database - err := _database.CreateUser(user) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - - // clear the required fields for the user - // so we get back the expected data - user.SetToken("") - user.SetHash("") - } - - got, err := _database.GetUserLiteList(1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetUserLiteList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserLiteList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserLiteList is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/user_test.go b/database/sqlite/user_test.go deleted file mode 100644 index ef97c6eda..000000000 --- a/database/sqlite/user_test.go +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/library" -) - -func TestSqlite_Client_GetUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.User - }{ - { - failure: false, - want: _user, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the user in the database - err := _database.CreateUser(test.want) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - } - - got, err := _database.GetUser(1) - - // cleanup the users table - _ = _database.Sqlite.Exec("DELETE FROM users;") - - if test.failure { - if err == nil { - t.Errorf("GetUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUser returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUser is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_CreateUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - err := _database.CreateUser(_user) - - if test.failure { - if err == nil { - t.Errorf("CreateUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateUser returned err: %v", err) - } - } -} - -func TestSqlite_Client_UpdateUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - // create the user in the database - err := _database.CreateUser(_user) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - - err = _database.UpdateUser(_user) - - if test.failure { - if err == nil { - t.Errorf("UpdateUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateUser returned err: %v", err) - } - } -} - -func TestSqlite_Client_DeleteUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - // create the user in the database - err := _database.CreateUser(_user) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - - err = _database.DeleteUser(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteUser returned err: %v", err) - } - } -} - -// testUser is a test helper function to create a -// library User type with all fields set to their -// zero values. -func testUser() *library.User { - i64 := int64(0) - str := "" - b := false - var arr []string - - return &library.User{ - ID: &i64, - Name: &str, - RefreshToken: &str, - Token: &str, - Hash: &str, - Favorites: &arr, - Active: &b, - Admin: &b, - } -} diff --git a/database/sqlite/worker.go b/database/sqlite/worker.go deleted file mode 100644 index 25ad1392b..000000000 --- a/database/sqlite/worker.go +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "errors" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetWorker gets a worker by hostname from the database. -func (c *client) GetWorker(hostname string) (*library.Worker, error) { - c.Logger.WithFields(logrus.Fields{ - "worker": hostname, - }).Tracef("getting worker %s from the database", hostname) - - // variable to store query results - w := new(database.Worker) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableWorker). - Raw(dml.SelectWorker, hostname). - Scan(w) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return w.ToLibrary(), result.Error -} - -// GetWorker gets a worker by address from the database. -func (c *client) GetWorkerByAddress(address string) (*library.Worker, error) { - c.Logger.Tracef("getting worker by address %s from the database", address) - - // variable to store query results - w := new(database.Worker) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableWorker). - Raw(dml.SelectWorkerByAddress, address). - Scan(w) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - return w.ToLibrary(), result.Error -} - -// CreateWorker creates a new worker in the database. -func (c *client) CreateWorker(w *library.Worker) error { - c.Logger.WithFields(logrus.Fields{ - "worker": w.GetHostname(), - }).Tracef("creating worker %s in the database", w.GetHostname()) - - // cast to database type - worker := database.WorkerFromLibrary(w) - - // validate the necessary fields are populated - err := worker.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Sqlite. - Table(constants.TableWorker). - Create(worker).Error -} - -// UpdateWorker updates a worker in the database. -func (c *client) UpdateWorker(w *library.Worker) error { - c.Logger.WithFields(logrus.Fields{ - "worker": w.GetHostname(), - }).Tracef("updating worker %s in the database", w.GetHostname()) - - // cast to database type - worker := database.WorkerFromLibrary(w) - - // validate the necessary fields are populated - err := worker.Validate() - if err != nil { - return err - } - - // send query to the database - return c.Sqlite. - Table(constants.TableWorker). - Save(worker).Error -} - -// DeleteWorker deletes a worker by unique ID from the database. -func (c *client) DeleteWorker(id int64) error { - c.Logger.Tracef("deleting worker %d in the database", id) - - // send query to the database - return c.Sqlite. - Table(constants.TableWorker). - Exec(dml.DeleteWorker, id).Error -} diff --git a/database/sqlite/worker_count_test.go b/database/sqlite/worker_count_test.go deleted file mode 100644 index fdd652852..000000000 --- a/database/sqlite/worker_count_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the worker table - err = _database.Sqlite.Exec(ddl.CreateWorkerTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableWorker, err) - } -} - -func TestSqlite_Client_GetWorkerCount(t *testing.T) { - // setup types - _workerOne := testWorker() - _workerOne.SetID(1) - _workerOne.SetHostname("worker_0") - _workerOne.SetAddress("localhost") - _workerOne.SetActive(true) - - _workerTwo := testWorker() - _workerTwo.SetID(2) - _workerTwo.SetHostname("worker_1") - _workerTwo.SetAddress("localhost") - _workerTwo.SetActive(true) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the workers table - defer _database.Sqlite.Exec("delete from workers;") - - // create the workers in the database - err := _database.CreateWorker(_workerOne) - if err != nil { - t.Errorf("unable to create test worker: %v", err) - } - - err = _database.CreateWorker(_workerTwo) - if err != nil { - t.Errorf("unable to create test worker: %v", err) - } - - got, err := _database.GetWorkerCount() - - if test.failure { - if err == nil { - t.Errorf("GetWorkerCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetWorkerCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetWorkerCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/worker_list.go b/database/sqlite/worker_list.go deleted file mode 100644 index e16d99071..000000000 --- a/database/sqlite/worker_list.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" -) - -// GetWorkerList gets a list of all workers from the database. -func (c *client) GetWorkerList() ([]*library.Worker, error) { - c.Logger.Trace("listing workers from the database") - - // variable to store query results - w := new([]database.Worker) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableWorker). - Raw(dml.ListWorkers). - Scan(w).Error - - // variable we want to return - workers := []*library.Worker{} - // iterate through all query results - for _, worker := range *w { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := worker - - // convert query result to library type - workers = append(workers, tmp.ToLibrary()) - } - - return workers, err -} diff --git a/database/sqlite/worker_list_test.go b/database/sqlite/worker_list_test.go deleted file mode 100644 index 8a6c0eba1..000000000 --- a/database/sqlite/worker_list_test.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the worker table - err = _database.Sqlite.Exec(ddl.CreateWorkerTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableWorker, err) - } -} - -func TestSqlite_Client_GetWorkerList(t *testing.T) { - // setup types - _workerOne := testWorker() - _workerOne.SetID(1) - _workerOne.SetHostname("worker_0") - _workerOne.SetAddress("localhost") - _workerOne.SetActive(true) - - _workerTwo := testWorker() - _workerTwo.SetID(2) - _workerTwo.SetHostname("worker_1") - _workerTwo.SetAddress("localhost") - _workerTwo.SetActive(true) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.Worker - }{ - { - failure: false, - want: []*library.Worker{_workerOne, _workerTwo}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the workers table - defer _database.Sqlite.Exec("delete from workers;") - - for _, worker := range test.want { - // create the worker in the database - err := _database.CreateWorker(worker) - if err != nil { - t.Errorf("unable to create test worker: %v", err) - } - } - - got, err := _database.GetWorkerList() - - if test.failure { - if err == nil { - t.Errorf("GetWorkerList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetWorkerList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetWorkerList is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/worker_test.go b/database/sqlite/worker_test.go deleted file mode 100644 index 219351e6d..000000000 --- a/database/sqlite/worker_test.go +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/library" -) - -func TestSqlite_Client_GetWorker(t *testing.T) { - // setup types - _worker := testWorker() - _worker.SetID(1) - _worker.SetHostname("worker_0") - _worker.SetAddress("localhost") - _worker.SetActive(true) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Worker - }{ - { - failure: false, - want: _worker, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the worker in the database - err := _database.CreateWorker(test.want) - if err != nil { - t.Errorf("unable to create test worker: %v", err) - } - } - - got, err := _database.GetWorker("worker_0") - - // cleanup the workers table - _ = _database.Sqlite.Exec("DELETE FROM workers;") - - if test.failure { - if err == nil { - t.Errorf("GetWorker should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetWorker returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetWorker is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetWorkerByAddress(t *testing.T) { - // setup types - _worker := testWorker() - _worker.SetID(1) - _worker.SetHostname("worker_0") - _worker.SetAddress("localhost") - _worker.SetActive(true) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.Worker - }{ - { - failure: false, - want: _worker, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the worker in the database - err := _database.CreateWorker(test.want) - if err != nil { - t.Errorf("unable to create test worker: %v", err) - } - } - - got, err := _database.GetWorkerByAddress("localhost") - - // cleanup the workers table - _ = _database.Sqlite.Exec("DELETE FROM workers;") - - if test.failure { - if err == nil { - t.Errorf("GetWorkerByAddress should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetWorkerByAddress returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetWorkerByAddress is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_CreateWorker(t *testing.T) { - // setup types - _worker := testWorker() - _worker.SetID(1) - _worker.SetHostname("worker_0") - _worker.SetAddress("localhost") - _worker.SetActive(true) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the workers table - defer _database.Sqlite.Exec("delete from workers;") - - err := _database.CreateWorker(_worker) - - if test.failure { - if err == nil { - t.Errorf("CreateWorker should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateWorker returned err: %v", err) - } - } -} - -func TestSqlite_Client_UpdateWorker(t *testing.T) { - // setup types - _worker := testWorker() - _worker.SetID(1) - _worker.SetHostname("worker_0") - _worker.SetAddress("localhost") - _worker.SetActive(true) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the workers table - defer _database.Sqlite.Exec("delete from workers;") - - // create the worker in the database - err := _database.CreateWorker(_worker) - if err != nil { - t.Errorf("unable to create test worker: %v", err) - } - - err = _database.UpdateWorker(_worker) - - if test.failure { - if err == nil { - t.Errorf("UpdateWorker should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateWorker returned err: %v", err) - } - } -} - -func TestSqlite_Client_DeleteWorker(t *testing.T) { - // setup types - _worker := testWorker() - _worker.SetID(1) - _worker.SetHostname("worker_0") - _worker.SetAddress("localhost") - _worker.SetActive(true) - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the workers table - defer _database.Sqlite.Exec("delete from workers;") - - // create the worker in the database - err := _database.CreateWorker(_worker) - if err != nil { - t.Errorf("unable to create test worker: %v", err) - } - - err = _database.DeleteWorker(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteWorker should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteWorker returned err: %v", err) - } - } -} - -// testWorker is a test helper function to create a -// library Worker type with all fields set to their -// zero values. -func testWorker() *library.Worker { - i64 := int64(0) - str := "" - b := false - var arr []string - - return &library.Worker{ - ID: &i64, - Hostname: &str, - Address: &str, - Routes: &arr, - Active: &b, - LastCheckedIn: &i64, - BuildLimit: &i64, - } -} diff --git a/database/step/clean.go b/database/step/clean.go new file mode 100644 index 000000000..54e686e86 --- /dev/null +++ b/database/step/clean.go @@ -0,0 +1,35 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "time" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CleanSteps updates steps to an error with a created timestamp prior to a defined moment. +func (e *engine) CleanSteps(msg string, before int64) (int64, error) { + logrus.Tracef("cleaning pending or running steps in the database created prior to %d", before) + + s := new(library.Step) + s.SetStatus(constants.StatusError) + s.SetError(msg) + s.SetFinished(time.Now().UTC().Unix()) + + step := database.StepFromLibrary(s) + + // send query to the database + result := e.client. + Table(constants.TableStep). + Where("created < ?", before). + Where("status = 'running' OR status = 'pending'"). + Updates(step) + + return result.RowsAffected, result.Error +} diff --git a/database/step/clean_test.go b/database/step/clean_test.go new file mode 100644 index 000000000..e772751ac --- /dev/null +++ b/database/step/clean_test.go @@ -0,0 +1,130 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestStep_Engine_CleanStep(t *testing.T) { + // setup types + _stepOne := testStep() + _stepOne.SetID(1) + _stepOne.SetRepoID(1) + _stepOne.SetBuildID(1) + _stepOne.SetNumber(1) + _stepOne.SetName("foo") + _stepOne.SetImage("bar") + _stepOne.SetCreated(1) + _stepOne.SetStatus("running") + + _stepTwo := testStep() + _stepTwo.SetID(2) + _stepTwo.SetRepoID(1) + _stepTwo.SetBuildID(1) + _stepTwo.SetNumber(2) + _stepTwo.SetName("foo") + _stepTwo.SetImage("bar") + _stepTwo.SetCreated(1) + _stepTwo.SetStatus("pending") + + _stepThree := testStep() + _stepThree.SetID(3) + _stepThree.SetRepoID(1) + _stepThree.SetBuildID(1) + _stepThree.SetNumber(3) + _stepThree.SetName("foo") + _stepThree.SetImage("bar") + _stepThree.SetCreated(1) + _stepThree.SetStatus("success") + + _stepFour := testStep() + _stepFour.SetID(4) + _stepFour.SetRepoID(1) + _stepFour.SetBuildID(1) + _stepFour.SetNumber(4) + _stepFour.SetName("foo") + _stepFour.SetImage("bar") + _stepFour.SetCreated(5) + _stepFour.SetStatus("pending") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the name query + _mock.ExpectExec(`UPDATE "steps" SET "status"=$1,"error"=$2,"finished"=$3 WHERE created < $4 AND (status = 'running' OR status = 'pending')`). + WithArgs("error", "msg", NowTimestamp{}, 3). + WillReturnResult(sqlmock.NewResult(1, 2)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateStep(_stepOne) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + _, err = _sqlite.CreateStep(_stepTwo) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + _, err = _sqlite.CreateStep(_stepThree) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + _, err = _sqlite.CreateStep(_stepFour) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CleanSteps("msg", 3) + + if test.failure { + if err == nil { + t.Errorf("CleanSteps for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CleanSteps for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CleanSteps for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/step/count.go b/database/step/count.go new file mode 100644 index 000000000..6f8a665b4 --- /dev/null +++ b/database/step/count.go @@ -0,0 +1,25 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/go-vela/types/constants" +) + +// CountSteps gets the count of all steps from the database. +func (e *engine) CountSteps() (int64, error) { + e.logger.Tracef("getting count of all steps from the database") + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableStep). + Count(&s). + Error + + return s, err +} diff --git a/database/step/count_build.go b/database/step/count_build.go new file mode 100644 index 000000000..448e04d7d --- /dev/null +++ b/database/step/count_build.go @@ -0,0 +1,31 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CountStepsForBuild gets the count of steps by build ID from the database. +func (e *engine) CountStepsForBuild(b *library.Build, filters map[string]interface{}) (int64, error) { + e.logger.WithFields(logrus.Fields{ + "build": b.GetNumber(), + }).Tracef("getting count of steps for build %d from the database", b.GetNumber()) + + // variable to store query results + var s int64 + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableStep). + Where("build_id = ?", b.GetID()). + Where(filters). + Count(&s). + Error + + return s, err +} diff --git a/database/step/count_build_test.go b/database/step/count_build_test.go new file mode 100644 index 000000000..df5a830f4 --- /dev/null +++ b/database/step/count_build_test.go @@ -0,0 +1,104 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestStep_Engine_CountStepsForBuild(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + + _stepOne := testStep() + _stepOne.SetID(1) + _stepOne.SetRepoID(1) + _stepOne.SetBuildID(1) + _stepOne.SetNumber(1) + _stepOne.SetName("foo") + _stepOne.SetImage("bar") + + _stepTwo := testStep() + _stepTwo.SetID(2) + _stepTwo.SetRepoID(1) + _stepTwo.SetBuildID(2) + _stepTwo.SetNumber(1) + _stepTwo.SetName("foo") + _stepTwo.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "steps" WHERE build_id = $1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateStep(_stepOne) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + _, err = _sqlite.CreateStep(_stepTwo) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 1, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 1, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountStepsForBuild(_build, filters) + + if test.failure { + if err == nil { + t.Errorf("CountStepsForBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountStepsForBuild for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountStepsForBuild for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/step/count_test.go b/database/step/count_test.go new file mode 100644 index 000000000..dc473bd96 --- /dev/null +++ b/database/step/count_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestStep_Engine_CountSteps(t *testing.T) { + // setup types + _stepOne := testStep() + _stepOne.SetID(1) + _stepOne.SetRepoID(1) + _stepOne.SetBuildID(1) + _stepOne.SetNumber(1) + _stepOne.SetName("foo") + _stepOne.SetImage("bar") + + _stepTwo := testStep() + _stepTwo.SetID(2) + _stepTwo.SetRepoID(1) + _stepTwo.SetBuildID(2) + _stepTwo.SetNumber(1) + _stepTwo.SetName("foo") + _stepTwo.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "steps"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateStep(_stepOne) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + _, err = _sqlite.CreateStep(_stepTwo) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountSteps() + + if test.failure { + if err == nil { + t.Errorf("CountSteps for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountSteps for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountSteps for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/step/create.go b/database/step/create.go new file mode 100644 index 000000000..2d18bc9ce --- /dev/null +++ b/database/step/create.go @@ -0,0 +1,37 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreateStep creates a new step in the database. +func (e *engine) CreateStep(s *library.Step) (*library.Step, error) { + e.logger.WithFields(logrus.Fields{ + "step": s.GetNumber(), + }).Tracef("creating step %s in the database", s.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#StepFromLibrary + step := database.StepFromLibrary(s) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Step.Validate + err := step.Validate() + if err != nil { + return nil, err + } + + // send query to the database + result := e.client.Table(constants.TableStep).Create(step) + + return step.ToLibrary(), result.Error +} diff --git a/database/step/create_test.go b/database/step/create_test.go new file mode 100644 index 000000000..0bf3ffca6 --- /dev/null +++ b/database/step/create_test.go @@ -0,0 +1,80 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestStep_Engine_CreateStep(t *testing.T) { + // setup types + _step := testStep() + _step.SetID(1) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetNumber(1) + _step.SetName("foo") + _step.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`INSERT INTO "steps" +("build_id","repo_id","number","name","image","stage","status","error","exit_code","created","started","finished","host","runtime","distribution","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16) RETURNING "id"`). + WithArgs(1, 1, 1, "foo", "bar", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CreateStep(_step) + + if test.failure { + if err == nil { + t.Errorf("CreateStep for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateStep for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _step) { + t.Errorf("CreateStep for %s returned %s, want %s", test.name, got, _step) + } + }) + } +} diff --git a/database/step/delete.go b/database/step/delete.go new file mode 100644 index 000000000..d5107dea8 --- /dev/null +++ b/database/step/delete.go @@ -0,0 +1,30 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// DeleteStep deletes an existing step from the database. +func (e *engine) DeleteStep(s *library.Step) error { + e.logger.WithFields(logrus.Fields{ + "step": s.GetNumber(), + }).Tracef("deleting step %s from the database", s.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#StepFromLibrary + step := database.StepFromLibrary(s) + + // send query to the database + return e.client. + Table(constants.TableStep). + Delete(step). + Error +} diff --git a/database/step/delete_test.go b/database/step/delete_test.go new file mode 100644 index 000000000..d77f81a25 --- /dev/null +++ b/database/step/delete_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestStep_Engine_DeleteStep(t *testing.T) { + // setup types + _step := testStep() + _step.SetID(1) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetNumber(1) + _step.SetName("foo") + _step.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`DELETE FROM "steps" WHERE "steps"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateStep(_step) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeleteStep(_step) + + if test.failure { + if err == nil { + t.Errorf("DeleteStep for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeleteStep for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/step/get.go b/database/step/get.go new file mode 100644 index 000000000..1b07d1f6a --- /dev/null +++ b/database/step/get.go @@ -0,0 +1,34 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetStep gets a step by ID from the database. +func (e *engine) GetStep(id int64) (*library.Step, error) { + e.logger.Tracef("getting step %d from the database", id) + + // variable to store query results + s := new(database.Step) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableStep). + Where("id = ?", id). + Take(s). + Error + if err != nil { + return nil, err + } + + // return the step + // + // https://pkg.go.dev/github.com/go-vela/types/database#Step.ToLibrary + return s.ToLibrary(), nil +} diff --git a/database/step/get_build.go b/database/step/get_build.go new file mode 100644 index 000000000..49aed7551 --- /dev/null +++ b/database/step/get_build.go @@ -0,0 +1,39 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetStepForBuild gets a step by number and build ID from the database. +func (e *engine) GetStepForBuild(b *library.Build, number int) (*library.Step, error) { + e.logger.WithFields(logrus.Fields{ + "build": b.GetNumber(), + "step": number, + }).Tracef("getting step %d from the database", number) + + // variable to store query results + s := new(database.Step) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableStep). + Where("build_id = ?", b.GetID()). + Where("number = ?", number). + Take(s). + Error + if err != nil { + return nil, err + } + + // return the step + // + // https://pkg.go.dev/github.com/go-vela/types/database#Step.ToLibrary + return s.ToLibrary(), nil +} diff --git a/database/step/get_build_test.go b/database/step/get_build_test.go new file mode 100644 index 000000000..8428598f5 --- /dev/null +++ b/database/step/get_build_test.go @@ -0,0 +1,92 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestStep_Engine_GetStepForBuild(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + + _step := testStep() + _step.SetID(1) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetNumber(1) + _step.SetName("foo") + _step.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "name", "image", "stage", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}). + AddRow(1, 1, 1, 1, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", "") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "steps" WHERE build_id = $1 AND number = $2 LIMIT 1`).WithArgs(1, 1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateStep(_step) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Step + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _step, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _step, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetStepForBuild(_build, 1) + + if test.failure { + if err == nil { + t.Errorf("GetStepForBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetStepForBuild for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetStepForBuild for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/step/get_test.go b/database/step/get_test.go new file mode 100644 index 000000000..b30ced1b0 --- /dev/null +++ b/database/step/get_test.go @@ -0,0 +1,87 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestStep_Engine_GetStep(t *testing.T) { + // setup types + _step := testStep() + _step.SetID(1) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetNumber(1) + _step.SetName("foo") + _step.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "name", "image", "stage", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}). + AddRow(1, 1, 1, 1, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", "") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "steps" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateStep(_step) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Step + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _step, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _step, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetStep(1) + + if test.failure { + if err == nil { + t.Errorf("GetStep for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetStep for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetStep for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/step/interface.go b/database/step/interface.go new file mode 100644 index 000000000..e7a377d26 --- /dev/null +++ b/database/step/interface.go @@ -0,0 +1,51 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/go-vela/types/library" +) + +// StepInterface represents the Vela interface for step +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type StepInterface interface { + // Step Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreateStepTable defines a function that creates the steps table. + CreateStepTable(string) error + + // Step Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CleanSteps defines a function that sets running or pending steps to error status before a given created time. + CleanSteps(string, int64) (int64, error) + // CountSteps defines a function that gets the count of all steps. + CountSteps() (int64, error) + // CountStepsForBuild defines a function that gets the count of steps by build ID. + CountStepsForBuild(*library.Build, map[string]interface{}) (int64, error) + // CreateStep defines a function that creates a new step. + CreateStep(*library.Step) (*library.Step, error) + // DeleteStep defines a function that deletes an existing step. + DeleteStep(*library.Step) error + // GetStep defines a function that gets a step by ID. + GetStep(int64) (*library.Step, error) + // GetStepForBuild defines a function that gets a step by number and build ID. + GetStepForBuild(*library.Build, int) (*library.Step, error) + // ListSteps defines a function that gets a list of all steps. + ListSteps() ([]*library.Step, error) + // ListStepsForBuild defines a function that gets a list of steps by build ID. + ListStepsForBuild(*library.Build, map[string]interface{}, int, int) ([]*library.Step, int64, error) + // ListStepImageCount defines a function that gets a list of all step images and the count of their occurrence. + ListStepImageCount() (map[string]float64, error) + // ListStepStatusCount defines a function that gets a list of all step statuses and the count of their occurrence. + ListStepStatusCount() (map[string]float64, error) + // UpdateStep defines a function that updates an existing step. + UpdateStep(*library.Step) (*library.Step, error) +} diff --git a/database/step/list.go b/database/step/list.go new file mode 100644 index 000000000..3c7fedbc9 --- /dev/null +++ b/database/step/list.go @@ -0,0 +1,54 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListSteps gets a list of all steps from the database. +func (e *engine) ListSteps() ([]*library.Step, error) { + e.logger.Trace("listing all steps from the database") + + // variables to store query results and return value + count := int64(0) + w := new([]database.Step) + steps := []*library.Step{} + + // count the results + count, err := e.CountSteps() + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return steps, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableStep). + Find(&w). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, step := range *w { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := step + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Step.ToLibrary + steps = append(steps, tmp.ToLibrary()) + } + + return steps, nil +} diff --git a/database/step/list_build.go b/database/step/list_build.go new file mode 100644 index 000000000..f338b1374 --- /dev/null +++ b/database/step/list_build.go @@ -0,0 +1,65 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// ListStepsForBuild gets a list of all steps from the database. +func (e *engine) ListStepsForBuild(b *library.Build, filters map[string]interface{}, page int, perPage int) ([]*library.Step, int64, error) { + e.logger.WithFields(logrus.Fields{ + "build": b.GetNumber(), + }).Tracef("listing steps for build %d from the database", b.GetNumber()) + + // variables to store query results and return value + count := int64(0) + s := new([]database.Step) + steps := []*library.Step{} + + // count the results + count, err := e.CountStepsForBuild(b, filters) + if err != nil { + return steps, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return steps, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableStep). + Where("build_id = ?", b.GetID()). + Where(filters). + Order("id DESC"). + Limit(perPage). + Offset(offset). + Find(&s). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, step := range *s { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := step + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Step.ToLibrary + steps = append(steps, tmp.ToLibrary()) + } + + return steps, count, nil +} diff --git a/database/step/list_build_test.go b/database/step/list_build_test.go new file mode 100644 index 000000000..4ebe4c0be --- /dev/null +++ b/database/step/list_build_test.go @@ -0,0 +1,114 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestStep_Engine_ListStepsForBuild(t *testing.T) { + // setup types + _build := testBuild() + _build.SetID(1) + _build.SetRepoID(1) + _build.SetNumber(1) + + _stepOne := testStep() + _stepOne.SetID(1) + _stepOne.SetRepoID(1) + _stepOne.SetBuildID(1) + _stepOne.SetNumber(1) + _stepOne.SetName("foo") + _stepOne.SetImage("bar") + + _stepTwo := testStep() + _stepTwo.SetID(2) + _stepTwo.SetRepoID(1) + _stepTwo.SetBuildID(1) + _stepTwo.SetNumber(2) + _stepTwo.SetName("foo") + _stepTwo.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "steps" WHERE build_id = $1`).WithArgs(1).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "name", "image", "stage", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}). + AddRow(2, 1, 1, 2, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", ""). + AddRow(1, 1, 1, 1, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", "") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "steps" WHERE build_id = $1 ORDER BY id DESC LIMIT 10`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateStep(_stepOne) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + _, err = _sqlite.CreateStep(_stepTwo) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Step + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Step{_stepTwo, _stepOne}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Step{_stepTwo, _stepOne}, + }, + } + + filters := map[string]interface{}{} + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListStepsForBuild(_build, filters, 1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListStepsForBuild for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListStepsForBuild for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListStepsForBuild for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/step/list_image.go b/database/step/list_image.go new file mode 100644 index 000000000..cd02af894 --- /dev/null +++ b/database/step/list_image.go @@ -0,0 +1,44 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "database/sql" + + "github.com/go-vela/types/constants" +) + +// ListStepImageCount gets a list of all step images and the count of their occurrence from the database. +func (e *engine) ListStepImageCount() (map[string]float64, error) { + e.logger.Tracef("getting count of all images for steps from the database") + + // variables to store query results and return value + s := []struct { + Image sql.NullString + Count sql.NullInt32 + }{} + images := make(map[string]float64) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableStep). + Select("image", " count(image) as count"). + Group("image"). + Find(&s). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, value := range s { + // check if the image returned is not empty + if value.Image.Valid { + images[value.Image.String] = float64(value.Count.Int32) + } + } + + return images, nil +} diff --git a/database/step/list_image_test.go b/database/step/list_image_test.go new file mode 100644 index 000000000..b7170634f --- /dev/null +++ b/database/step/list_image_test.go @@ -0,0 +1,97 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestStep_Engine_ListStepImageCount(t *testing.T) { + // setup types + _stepOne := testStep() + _stepOne.SetID(1) + _stepOne.SetRepoID(1) + _stepOne.SetBuildID(1) + _stepOne.SetNumber(1) + _stepOne.SetName("foo") + _stepOne.SetImage("bar") + + _stepTwo := testStep() + _stepTwo.SetID(2) + _stepTwo.SetRepoID(1) + _stepTwo.SetBuildID(1) + _stepTwo.SetNumber(2) + _stepTwo.SetName("foo") + _stepTwo.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"image", "count"}).AddRow("bar", 2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT "image", count(image) as count FROM "steps" GROUP BY "image"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateStep(_stepOne) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + _, err = _sqlite.CreateStep(_stepTwo) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want map[string]float64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: map[string]float64{"bar": 2}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: map[string]float64{"bar": 2}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListStepImageCount() + + if test.failure { + if err == nil { + t.Errorf("ListStepImageCount for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListStepImageCount for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListStepImageCount for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/step/list_status.go b/database/step/list_status.go new file mode 100644 index 000000000..9b54e3b7f --- /dev/null +++ b/database/step/list_status.go @@ -0,0 +1,50 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "database/sql" + + "github.com/go-vela/types/constants" +) + +// ListStepStatusCount gets a list of all step statuses and the count of their occurrence from the database. +func (e *engine) ListStepStatusCount() (map[string]float64, error) { + e.logger.Tracef("getting count of all statuses for steps from the database") + + // variables to store query results and return value + s := []struct { + Status sql.NullString + Count sql.NullInt32 + }{} + statuses := map[string]float64{ + "pending": 0, + "failure": 0, + "killed": 0, + "running": 0, + "success": 0, + } + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableStep). + Select("status", " count(status) as count"). + Group("status"). + Find(&s). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, value := range s { + // check if the status returned is not empty + if value.Status.Valid { + statuses[value.Status.String] = float64(value.Count.Int32) + } + } + + return statuses, nil +} diff --git a/database/step/list_status_test.go b/database/step/list_status_test.go new file mode 100644 index 000000000..7716b02e9 --- /dev/null +++ b/database/step/list_status_test.go @@ -0,0 +1,114 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestStep_Engine_ListStepStatusCount(t *testing.T) { + // setup types + _stepOne := testStep() + _stepOne.SetID(1) + _stepOne.SetRepoID(1) + _stepOne.SetBuildID(1) + _stepOne.SetNumber(1) + _stepOne.SetName("foo") + _stepOne.SetImage("bar") + + _stepTwo := testStep() + _stepTwo.SetID(2) + _stepTwo.SetRepoID(1) + _stepTwo.SetBuildID(1) + _stepTwo.SetNumber(2) + _stepTwo.SetName("foo") + _stepTwo.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"status", "count"}). + AddRow("pending", 0). + AddRow("failure", 0). + AddRow("killed", 0). + AddRow("running", 0). + AddRow("success", 0) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT "status", count(status) as count FROM "steps" GROUP BY "status"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateStep(_stepOne) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + _, err = _sqlite.CreateStep(_stepTwo) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want map[string]float64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: map[string]float64{ + "pending": 0, + "failure": 0, + "killed": 0, + "running": 0, + "success": 0, + }, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: map[string]float64{ + "pending": 0, + "failure": 0, + "killed": 0, + "running": 0, + "success": 0, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListStepStatusCount() + + if test.failure { + if err == nil { + t.Errorf("ListStepStatusCount for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListStepStatusCount for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListStepStatusCount for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/step/list_test.go b/database/step/list_test.go new file mode 100644 index 000000000..c98852b1b --- /dev/null +++ b/database/step/list_test.go @@ -0,0 +1,107 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestStep_Engine_ListSteps(t *testing.T) { + // setup types + _stepOne := testStep() + _stepOne.SetID(1) + _stepOne.SetRepoID(1) + _stepOne.SetBuildID(1) + _stepOne.SetNumber(1) + _stepOne.SetName("foo") + _stepOne.SetImage("bar") + + _stepTwo := testStep() + _stepTwo.SetID(2) + _stepTwo.SetRepoID(1) + _stepTwo.SetBuildID(2) + _stepTwo.SetNumber(1) + _stepTwo.SetName("bar") + _stepTwo.SetImage("foo") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "steps"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "repo_id", "build_id", "number", "name", "image", "stage", "status", "error", "exit_code", "created", "started", "finished", "host", "runtime", "distribution"}). + AddRow(1, 1, 1, 1, "foo", "bar", "", "", "", 0, 0, 0, 0, "", "", ""). + AddRow(2, 1, 2, 1, "bar", "foo", "", "", "", 0, 0, 0, 0, "", "", "") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "steps"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateStep(_stepOne) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + _, err = _sqlite.CreateStep(_stepTwo) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Step + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Step{_stepOne, _stepTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Step{_stepOne, _stepTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListSteps() + + if test.failure { + if err == nil { + t.Errorf("ListSteps for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListSteps for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListSteps for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/step/opts.go b/database/step/opts.go new file mode 100644 index 000000000..80db4689c --- /dev/null +++ b/database/step/opts.go @@ -0,0 +1,44 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Steps. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Steps. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the step engine + e.client = client + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Steps. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the step engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Steps. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the step engine + e.config.SkipCreation = skipCreation + + return nil + } +} diff --git a/database/step/opts_test.go b/database/step/opts_test.go new file mode 100644 index 000000000..4ea1d64c0 --- /dev/null +++ b/database/step/opts_test.go @@ -0,0 +1,161 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestStep_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestStep_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestStep_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} diff --git a/database/step/step.go b/database/step/step.go new file mode 100644 index 000000000..43e905797 --- /dev/null +++ b/database/step/step.go @@ -0,0 +1,74 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the StepInterface interface. + config struct { + // specifies to skip creating tables and indexes for the Step engine + SkipCreation bool + } + + // engine represents the step functionality that implements the StepInterface interface. + engine struct { + // engine configuration settings used in step functions + config *config + + // gorm.io/gorm database client used in step functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in step functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with steps in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new Step engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating step database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of steps table in the database") + + return e, nil + } + + // create the steps table + err := e.CreateStepTable(e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableStep, err) + } + + return e, nil +} diff --git a/database/step/step_test.go b/database/step/step_test.go new file mode 100644 index 000000000..136d5ca40 --- /dev/null +++ b/database/step/step_test.go @@ -0,0 +1,247 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "database/sql/driver" + "reflect" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestStep_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithClient(test.client), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres step engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite step engine: %v", err) + } + + return _engine +} + +// testBuild is a test helper function to create a library +// Build type with all fields set to their zero values. +func testBuild() *library.Build { + return &library.Build{ + ID: new(int64), + RepoID: new(int64), + PipelineID: new(int64), + Number: new(int), + Parent: new(int), + Event: new(string), + EventAction: new(string), + Status: new(string), + Error: new(string), + Enqueued: new(int64), + Created: new(int64), + Started: new(int64), + Finished: new(int64), + Deploy: new(string), + Clone: new(string), + Source: new(string), + Title: new(string), + Message: new(string), + Commit: new(string), + Sender: new(string), + Author: new(string), + Email: new(string), + Link: new(string), + Branch: new(string), + Ref: new(string), + BaseRef: new(string), + HeadRef: new(string), + Host: new(string), + Runtime: new(string), + Distribution: new(string), + } +} + +// testStep is a test helper function to create a library +// Step type with all fields set to their zero values. +func testStep() *library.Step { + return &library.Step{ + ID: new(int64), + BuildID: new(int64), + RepoID: new(int64), + Number: new(int), + Name: new(string), + Image: new(string), + Stage: new(string), + Status: new(string), + Error: new(string), + ExitCode: new(int), + Created: new(int64), + Started: new(int64), + Finished: new(int64), + Host: new(string), + Runtime: new(string), + Distribution: new(string), + } +} + +// This will be used with the github.com/DATA-DOG/go-sqlmock library to compare values +// that are otherwise not easily compared. These typically would be values generated +// before adding or updating them in the database. +// +// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime + +// NowTimestamp is used to test whether timestamps get updated correctly to the current time with lenience. +type NowTimestamp struct{} + +// Match satisfies sqlmock.Argument interface. +func (t NowTimestamp) Match(v driver.Value) bool { + ts, ok := v.(int64) + if !ok { + return false + } + now := time.Now().Unix() + + return now-ts < 10 +} diff --git a/database/step/table.go b/database/step/table.go new file mode 100644 index 000000000..ffd44c6c6 --- /dev/null +++ b/database/step/table.go @@ -0,0 +1,78 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres steps table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +steps ( + id SERIAL PRIMARY KEY, + repo_id INTEGER, + build_id INTEGER, + number INTEGER, + name VARCHAR(250), + image VARCHAR(500), + stage VARCHAR(250), + status VARCHAR(250), + error VARCHAR(500), + exit_code INTEGER, + created INTEGER, + started INTEGER, + finished INTEGER, + host VARCHAR(250), + runtime VARCHAR(250), + distribution VARCHAR(250), + UNIQUE(build_id, number) +); +` + + // CreateSqliteTable represents a query to create the Sqlite steps table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +steps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_id INTEGER, + build_id INTEGER, + number INTEGER, + name TEXT, + image TEXT, + stage TEXT, + status TEXT, + error TEXT, + exit_code INTEGER, + created INTEGER, + started INTEGER, + finished INTEGER, + host TEXT, + runtime TEXT, + distribution TEXT, + UNIQUE(build_id, number) +); +` +) + +// CreateStepTable creates the steps table in the database. +func (e *engine) CreateStepTable(driver string) error { + e.logger.Tracef("creating steps table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the steps table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the steps table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/step/table_test.go b/database/step/table_test.go new file mode 100644 index 000000000..08900c753 --- /dev/null +++ b/database/step/table_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestStep_Engine_CreateStepTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateStepTable(test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateStepTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateStepTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/step/update.go b/database/step/update.go new file mode 100644 index 000000000..c9e5f73d0 --- /dev/null +++ b/database/step/update.go @@ -0,0 +1,37 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// UpdateStep updates an existing step in the database. +func (e *engine) UpdateStep(s *library.Step) (*library.Step, error) { + e.logger.WithFields(logrus.Fields{ + "step": s.GetNumber(), + }).Tracef("updating step %s in the database", s.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#StepFromLibrary + step := database.StepFromLibrary(s) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Step.Validate + err := step.Validate() + if err != nil { + return nil, err + } + + // send query to the database + result := e.client.Table(constants.TableStep).Save(step) + + return step.ToLibrary(), result.Error +} diff --git a/database/step/update_test.go b/database/step/update_test.go new file mode 100644 index 000000000..38d8c1bdb --- /dev/null +++ b/database/step/update_test.go @@ -0,0 +1,82 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package step + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestStep_Engine_UpdateStep(t *testing.T) { + // setup types + _step := testStep() + _step.SetID(1) + _step.SetRepoID(1) + _step.SetBuildID(1) + _step.SetNumber(1) + _step.SetName("foo") + _step.SetImage("bar") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`UPDATE "steps" +SET "build_id"=$1,"repo_id"=$2,"number"=$3,"name"=$4,"image"=$5,"stage"=$6,"status"=$7,"error"=$8,"exit_code"=$9,"created"=$10,"started"=$11,"finished"=$12,"host"=$13,"runtime"=$14,"distribution"=$15 +WHERE "id" = $16`). + WithArgs(1, 1, 1, "foo", "bar", nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + _, err := _sqlite.CreateStep(_step) + if err != nil { + t.Errorf("unable to create test step for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.UpdateStep(_step) + + if test.failure { + if err == nil { + t.Errorf("UpdateStep for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateStep for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, _step) { + t.Errorf("UpdateStep for %s returned %s, want %s", test.name, got, _step) + } + }) + } +} diff --git a/database/sqlite/user_count.go b/database/user/count.go similarity index 53% rename from database/sqlite/user_count.go rename to database/user/count.go index 72b70b003..074a5ef66 100644 --- a/database/sqlite/user_count.go +++ b/database/user/count.go @@ -2,25 +2,24 @@ // // Use of this source code is governed by the LICENSE file in this repository. -package sqlite +package user import ( - "github.com/go-vela/server/database/sqlite/dml" "github.com/go-vela/types/constants" ) -// GetUserCount gets a count of all users from the database. -func (c *client) GetUserCount() (int64, error) { - c.Logger.Trace("getting count of users from the database") +// CountUsers gets the count of all users from the database. +func (e *engine) CountUsers() (int64, error) { + e.logger.Tracef("getting count of all users from the database") // variable to store query results var u int64 // send query to the database and store result in variable - err := c.Sqlite. + err := e.client. Table(constants.TableUser). - Raw(dml.SelectUsersCount). - Pluck("count", &u).Error + Count(&u). + Error return u, err } diff --git a/database/user/count_test.go b/database/user/count_test.go new file mode 100644 index 000000000..be7cb6437 --- /dev/null +++ b/database/user/count_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_CountUsers(t *testing.T) { + // setup types + _userOne := testUser() + _userOne.SetID(1) + _userOne.SetName("foo") + _userOne.SetToken("bar") + _userOne.SetHash("baz") + + _userTwo := testUser() + _userTwo.SetID(2) + _userTwo.SetName("baz") + _userTwo.SetToken("bar") + _userTwo.SetHash("foo") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "users"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_userOne) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + err = _sqlite.CreateUser(_userTwo) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountUsers() + + if test.failure { + if err == nil { + t.Errorf("CountUsers for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountUsers for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountUsers for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/user/create.go b/database/user/create.go new file mode 100644 index 000000000..f527979b9 --- /dev/null +++ b/database/user/create.go @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code in update.go +package user + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreateUser creates a new user in the database. +func (e *engine) CreateUser(u *library.User) error { + e.logger.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Tracef("creating user %s in the database", u.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary + user := database.UserFromLibrary(u) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate + err := user.Validate() + if err != nil { + return err + } + + // encrypt the fields for the user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt + err = user.Encrypt(e.config.EncryptionKey) + if err != nil { + return fmt.Errorf("unable to encrypt user %s: %w", u.GetName(), err) + } + + // send query to the database + return e.client. + Table(constants.TableUser). + Create(user). + Error +} diff --git a/database/user/create_test.go b/database/user/create_test.go new file mode 100644 index 000000000..12de80f96 --- /dev/null +++ b/database/user/create_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_CreateUser(t *testing.T) { + // setup types + _user := testUser() + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + _user.SetHash("baz") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`INSERT INTO "users" +("name","refresh_token","token","hash","favorites","active","admin","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id"`). + WithArgs("foo", AnyArgument{}, AnyArgument{}, AnyArgument{}, nil, false, false, 1). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateUser(_user) + + if test.failure { + if err == nil { + t.Errorf("CreateUser for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateUser for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/user/delete.go b/database/user/delete.go new file mode 100644 index 000000000..95b77ff64 --- /dev/null +++ b/database/user/delete.go @@ -0,0 +1,30 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// DeleteUser deletes an existing user from the database. +func (e *engine) DeleteUser(u *library.User) error { + e.logger.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Tracef("deleting user %s from the database", u.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary + user := database.UserFromLibrary(u) + + // send query to the database + return e.client. + Table(constants.TableUser). + Delete(user). + Error +} diff --git a/database/user/delete_test.go b/database/user/delete_test.go new file mode 100644 index 000000000..0dec0a6b2 --- /dev/null +++ b/database/user/delete_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_DeleteUser(t *testing.T) { + // setup types + _user := testUser() + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + _user.SetHash("baz") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`DELETE FROM "users" WHERE "users"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_user) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeleteUser(_user) + + if test.failure { + if err == nil { + t.Errorf("DeleteUser for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeleteUser for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/user/get.go b/database/user/get.go new file mode 100644 index 000000000..d37275c80 --- /dev/null +++ b/database/user/get.go @@ -0,0 +1,47 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetUser gets a user by ID from the database. +func (e *engine) GetUser(id int64) (*library.User, error) { + e.logger.Tracef("getting user %d from the database", id) + + // variable to store query results + u := new(database.User) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableUser). + Where("id = ?", id). + Take(u). + Error + if err != nil { + return nil, err + } + + // decrypt the fields for the user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt + err = u.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted users + e.logger.Errorf("unable to decrypt user %d: %v", u.ID.Int64, err) + } + + // return the decrypted user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary + return u.ToLibrary(), nil +} diff --git a/database/user/get_name.go b/database/user/get_name.go new file mode 100644 index 000000000..4e8da5550 --- /dev/null +++ b/database/user/get_name.go @@ -0,0 +1,50 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetUserForName gets a user by name from the database. +func (e *engine) GetUserForName(name string) (*library.User, error) { + e.logger.WithFields(logrus.Fields{ + "user": name, + }).Tracef("getting user %s from the database", name) + + // variable to store query results + u := new(database.User) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableUser). + Where("name = ?", name). + Take(u). + Error + if err != nil { + return nil, err + } + + // decrypt the fields for the user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt + err = u.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted users + e.logger.Errorf("unable to decrypt user %d: %v", u.ID.Int64, err) + } + + // return the decrypted user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary + return u.ToLibrary(), nil +} diff --git a/database/user/get_name_test.go b/database/user/get_name_test.go new file mode 100644 index 000000000..4b0a6446b --- /dev/null +++ b/database/user/get_name_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestUser_Engine_GetUserForName(t *testing.T) { + // setup types + _user := testUser() + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + _user.SetHash("baz") + _user.SetFavorites([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "name", "refresh_token", "token", "hash", "favorites", "active", "admin"}). + AddRow(1, "foo", "", "bar", "baz", "{}", false, false) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "users" WHERE name = $1 LIMIT 1`).WithArgs("foo").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_user) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.User + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _user, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _user, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetUserForName("foo") + + if test.failure { + if err == nil { + t.Errorf("GetUserForName for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetUserForName for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetUserForName for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/user/get_test.go b/database/user/get_test.go new file mode 100644 index 000000000..4593ce48f --- /dev/null +++ b/database/user/get_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestUser_Engine_GetUser(t *testing.T) { + // setup types + _user := testUser() + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + _user.SetHash("baz") + _user.SetFavorites([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "name", "refresh_token", "token", "hash", "favorites", "active", "admin"}). + AddRow(1, "foo", "", "bar", "baz", "{}", false, false) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "users" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_user) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.User + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _user, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _user, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetUser(1) + + if test.failure { + if err == nil { + t.Errorf("GetUser for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetUser for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetUser for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/user/index.go b/database/user/index.go new file mode 100644 index 000000000..5445e963e --- /dev/null +++ b/database/user/index.go @@ -0,0 +1,24 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +const ( + // CreateUserRefreshIndex represents a query to create an + // index on the users table for the refresh_token column. + CreateUserRefreshIndex = ` +CREATE INDEX +IF NOT EXISTS +users_refresh +ON users (refresh_token); +` +) + +// CreateUserIndexes creates the indexes for the users table in the database. +func (e *engine) CreateUserIndexes() error { + e.logger.Tracef("creating indexes for users table in the database") + + // create the refresh_token column index for the users table + return e.client.Exec(CreateUserRefreshIndex).Error +} diff --git a/database/user/index_test.go b/database/user/index_test.go new file mode 100644 index 000000000..55728b77a --- /dev/null +++ b/database/user/index_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_CreateUserIndexes(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateUserIndexes() + + if test.failure { + if err == nil { + t.Errorf("CreateUserIndexes for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateUserIndexes for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/user/interface.go b/database/user/interface.go new file mode 100644 index 000000000..ad5986fad --- /dev/null +++ b/database/user/interface.go @@ -0,0 +1,45 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/library" +) + +// UserInterface represents the Vela interface for user +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type UserInterface interface { + // User Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreateUserIndexes defines a function that creates the indexes for the users table. + CreateUserIndexes() error + // CreateUserTable defines a function that creates the users table. + CreateUserTable(string) error + + // User Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CountUsers defines a function that gets the count of all users. + CountUsers() (int64, error) + // CreateUser defines a function that creates a new user. + CreateUser(*library.User) error + // DeleteUser defines a function that deletes an existing user. + DeleteUser(*library.User) error + // GetUser defines a function that gets a user by ID. + GetUser(int64) (*library.User, error) + // GetUserForName defines a function that gets a user by name. + GetUserForName(string) (*library.User, error) + // ListUsers defines a function that gets a list of all users. + ListUsers() ([]*library.User, error) + // ListLiteUsers defines a function that gets a lite list of users. + ListLiteUsers(int, int) ([]*library.User, int64, error) + // UpdateUser defines a function that updates an existing user. + UpdateUser(*library.User) error +} diff --git a/database/user/list.go b/database/user/list.go new file mode 100644 index 000000000..4bc730f27 --- /dev/null +++ b/database/user/list.go @@ -0,0 +1,67 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListUsers gets a list of all users from the database. +func (e *engine) ListUsers() ([]*library.User, error) { + e.logger.Trace("listing all users from the database") + + // variables to store query results and return value + count := int64(0) + u := new([]database.User) + users := []*library.User{} + + // count the results + count, err := e.CountUsers() + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return users, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableUser). + Find(&u). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, user := range *u { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := user + + // decrypt the fields for the user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt + err = tmp.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted users + e.logger.Errorf("unable to decrypt user %d: %v", tmp.ID.Int64, err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary + users = append(users, tmp.ToLibrary()) + } + + return users, nil +} diff --git a/database/user/list_lite.go b/database/user/list_lite.go new file mode 100644 index 000000000..ee90bca3b --- /dev/null +++ b/database/user/list_lite.go @@ -0,0 +1,61 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListLiteUsers gets a lite (only: id, name) list of users from the database. +// +//nolint:lll // ignore long line length due to variable names +func (e *engine) ListLiteUsers(page, perPage int) ([]*library.User, int64, error) { + e.logger.Trace("listing lite users from the database") + + // variables to store query results and return values + count := int64(0) + u := new([]database.User) + users := []*library.User{} + + // count the results + count, err := e.CountUsers() + if err != nil { + return users, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return users, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + err = e.client. + Table(constants.TableUser). + Select("id", "name"). + Limit(perPage). + Offset(offset). + Find(&u). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, user := range *u { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := user + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary + users = append(users, tmp.ToLibrary()) + } + + return users, count, nil +} diff --git a/database/user/list_lite_test.go b/database/user/list_lite_test.go new file mode 100644 index 000000000..4c44bc20b --- /dev/null +++ b/database/user/list_lite_test.go @@ -0,0 +1,116 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestUser_Engine_ListLiteUsers(t *testing.T) { + // setup types + _userOne := testUser() + _userOne.SetID(1) + _userOne.SetName("foo") + _userOne.SetToken("bar") + _userOne.SetHash("baz") + _userOne.SetFavorites([]string{}) + + _userTwo := testUser() + _userTwo.SetID(2) + _userTwo.SetName("baz") + _userTwo.SetToken("bar") + _userTwo.SetHash("foo") + _userTwo.SetFavorites([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "users"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "name"}). + AddRow(1, "foo"). + AddRow(2, "baz") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT "id","name" FROM "users" LIMIT 10`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_userOne) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + err = _sqlite.CreateUser(_userTwo) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // empty fields not returned by query + _userOne.RefreshToken = new(string) + _userOne.Token = new(string) + _userOne.Hash = new(string) + _userOne.Favorites = new([]string) + + _userTwo.RefreshToken = new(string) + _userTwo.Token = new(string) + _userTwo.Hash = new(string) + _userTwo.Favorites = new([]string) + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.User + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.User{_userOne, _userTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.User{_userTwo, _userOne}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListLiteUsers(1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListLiteUsers for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListLiteUsers for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListLiteUsers for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/user/list_test.go b/database/user/list_test.go new file mode 100644 index 000000000..61293d44c --- /dev/null +++ b/database/user/list_test.go @@ -0,0 +1,105 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestUser_Engine_ListUsers(t *testing.T) { + // setup types + _userOne := testUser() + _userOne.SetID(1) + _userOne.SetName("foo") + _userOne.SetToken("bar") + _userOne.SetHash("baz") + _userOne.SetFavorites([]string{}) + + _userTwo := testUser() + _userTwo.SetID(2) + _userTwo.SetName("baz") + _userTwo.SetToken("bar") + _userTwo.SetHash("foo") + _userTwo.SetFavorites([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "users"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "name", "refresh_token", "token", "hash", "favorites", "active", "admin"}). + AddRow(1, "foo", "", "bar", "baz", "{}", false, false). + AddRow(2, "baz", "", "bar", "foo", "{}", false, false) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "users"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_userOne) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + err = _sqlite.CreateUser(_userTwo) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.User + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.User{_userOne, _userTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.User{_userOne, _userTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListUsers() + + if test.failure { + if err == nil { + t.Errorf("ListUsers for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListUsers for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListUsers for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/user/opts.go b/database/user/opts.go new file mode 100644 index 000000000..58780c317 --- /dev/null +++ b/database/user/opts.go @@ -0,0 +1,54 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Users. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Users. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the user engine + e.client = client + + return nil + } +} + +// WithEncryptionKey sets the encryption key in the database engine for Users. +func WithEncryptionKey(key string) EngineOpt { + return func(e *engine) error { + // set the encryption key in the user engine + e.config.EncryptionKey = key + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Users. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the user engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Users. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the user engine + e.config.SkipCreation = skipCreation + + return nil + } +} diff --git a/database/user/opts_test.go b/database/user/opts_test.go new file mode 100644 index 000000000..77fb9ae23 --- /dev/null +++ b/database/user/opts_test.go @@ -0,0 +1,210 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestUser_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestUser_EngineOpt_WithEncryptionKey(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + key string + want string + }{ + { + failure: false, + name: "encryption key set", + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + want: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + }, + { + failure: false, + name: "encryption key not set", + key: "", + want: "", + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithEncryptionKey(test.key)(e) + + if test.failure { + if err == nil { + t.Errorf("WithEncryptionKey for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithEncryptionKey returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.EncryptionKey, test.want) { + t.Errorf("WithEncryptionKey is %v, want %v", e.config.EncryptionKey, test.want) + } + }) + } +} + +func TestUser_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestUser_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} diff --git a/database/user/table.go b/database/user/table.go new file mode 100644 index 000000000..456853770 --- /dev/null +++ b/database/user/table.go @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres users table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +users ( + id SERIAL PRIMARY KEY, + name VARCHAR(250), + refresh_token VARCHAR(500), + token VARCHAR(500), + hash VARCHAR(500), + favorites VARCHAR(5000), + active BOOLEAN, + admin BOOLEAN, + UNIQUE(name) +); +` + + // CreateSqliteTable represents a query to create the Sqlite users table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + refresh_token TEXT, + token TEXT, + hash TEXT, + favorites TEXT, + active BOOLEAN, + admin BOOLEAN, + UNIQUE(name) +); +` +) + +// CreateUserTable creates the users table in the database. +func (e *engine) CreateUserTable(driver string) error { + e.logger.Tracef("creating users table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the users table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the users table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/user/table_test.go b/database/user/table_test.go new file mode 100644 index 000000000..95a2d4c00 --- /dev/null +++ b/database/user/table_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_CreateUserTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateUserTable(test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateUserTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateUserTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/user/update.go b/database/user/update.go new file mode 100644 index 000000000..c7efc5e7f --- /dev/null +++ b/database/user/update.go @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +//nolint:dupl // ignore similar code in create.go +package user + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// UpdateUser updates an existing user in the database. +func (e *engine) UpdateUser(u *library.User) error { + e.logger.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Tracef("updating user %s in the database", u.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary + user := database.UserFromLibrary(u) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate + err := user.Validate() + if err != nil { + return err + } + + // encrypt the fields for the user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt + err = user.Encrypt(e.config.EncryptionKey) + if err != nil { + return fmt.Errorf("unable to encrypt user %s: %w", u.GetName(), err) + } + + // send query to the database + return e.client. + Table(constants.TableUser). + Save(user). + Error +} diff --git a/database/user/update_test.go b/database/user/update_test.go new file mode 100644 index 000000000..4253ac435 --- /dev/null +++ b/database/user/update_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_UpdateUser(t *testing.T) { + // setup types + _user := testUser() + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + _user.SetHash("baz") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`UPDATE "users" +SET "name"=$1,"refresh_token"=$2,"token"=$3,"hash"=$4,"favorites"=$5,"active"=$6,"admin"=$7 +WHERE "id" = $8`). + WithArgs("foo", AnyArgument{}, AnyArgument{}, AnyArgument{}, nil, false, false, 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_user) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.UpdateUser(_user) + + if test.failure { + if err == nil { + t.Errorf("UpdateUser for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateUser for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/user/user.go b/database/user/user.go new file mode 100644 index 000000000..99e1f9701 --- /dev/null +++ b/database/user/user.go @@ -0,0 +1,82 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the UserInterface interface. + config struct { + // specifies the encryption key to use for the User engine + EncryptionKey string + // specifies to skip creating tables and indexes for the User engine + SkipCreation bool + } + + // engine represents the user functionality that implements the UserInterface interface. + engine struct { + // engine configuration settings used in user functions + config *config + + // gorm.io/gorm database client used in user functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in user functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with users in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new User engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating user database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of users table and indexes in the database") + + return e, nil + } + + // create the users table + err := e.CreateUserTable(e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableUser, err) + } + + // create the indexes for the users table + err = e.CreateUserIndexes() + if err != nil { + return nil, fmt.Errorf("unable to create indexes for %s table: %w", constants.TableUser, err) + } + + return e, nil +} diff --git a/database/user/user_test.go b/database/user/user_test.go new file mode 100644 index 000000000..1586103a7 --- /dev/null +++ b/database/user/user_test.go @@ -0,0 +1,200 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "database/sql/driver" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestUser_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", SkipCreation: false}, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", SkipCreation: false}, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithClient(test.client), + WithEncryptionKey(test.key), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres user engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite user engine: %v", err) + } + + return _engine +} + +// testUser is a test helper function to create a library +// User type with all fields set to their zero values. +func testUser() *library.User { + return &library.User{ + ID: new(int64), + Name: new(string), + RefreshToken: new(string), + Token: new(string), + Hash: new(string), + Favorites: new([]string), + Active: new(bool), + Admin: new(bool), + } +} + +// This will be used with the github.com/DATA-DOG/go-sqlmock library to compare values +// that are otherwise not easily compared. These typically would be values generated +// before adding or updating them in the database. +// +// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime +type AnyArgument struct{} + +// Match satisfies sqlmock.Argument interface. +func (a AnyArgument) Match(v driver.Value) bool { + return true +} diff --git a/database/validate.go b/database/validate.go new file mode 100644 index 000000000..18f07cb84 --- /dev/null +++ b/database/validate.go @@ -0,0 +1,75 @@ +// Copyright (c) 2023 Target Brands, Ine. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package database + +import ( + "fmt" + "strings" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" +) + +// Validate verifies the required fields from the provided configuration are populated correctly. +func (c *config) Validate() error { + logrus.Trace("validating database configuration for engine") + + // verify a database driver was provided + if len(c.Driver) == 0 { + return fmt.Errorf("no database driver provided") + } + + // verify a database address was provided + if len(c.Address) == 0 { + return fmt.Errorf("no database address provided") + } + + // check if the database address has a trailing slash + if strings.HasSuffix(c.Address, "/") { + return fmt.Errorf("invalid database address provided: address must not have trailing slash") + } + + // verify a database encryption key was provided + if len(c.EncryptionKey) == 0 { + return fmt.Errorf("no database encryption key provided") + } + + // check the database encryption key length - enforce AES-256 by forcing 32 characters in the key + if len(c.EncryptionKey) != 32 { + return fmt.Errorf("invalid database encryption key provided: key length (%d) must be 32 characters", len(c.EncryptionKey)) + } + + // verify the database compression level is valid + switch c.CompressionLevel { + case constants.CompressionNegOne: + fallthrough + case constants.CompressionZero: + fallthrough + case constants.CompressionOne: + fallthrough + case constants.CompressionTwo: + fallthrough + case constants.CompressionThree: + fallthrough + case constants.CompressionFour: + fallthrough + case constants.CompressionFive: + fallthrough + case constants.CompressionSix: + fallthrough + case constants.CompressionSeven: + fallthrough + case constants.CompressionEight: + fallthrough + case constants.CompressionNine: + break + default: + return fmt.Errorf("invalid database compression level provided: level (%d) must be between %d and %d", + c.CompressionLevel, constants.CompressionNegOne, constants.CompressionNine, + ) + } + + return nil +} diff --git a/database/setup_test.go b/database/validate_test.go similarity index 59% rename from database/setup_test.go rename to database/validate_test.go index 4ec2c958c..60761e215 100644 --- a/database/setup_test.go +++ b/database/validate_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Ine. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -9,107 +9,17 @@ import ( "time" ) -func TestDatabase_Setup_Postgres(t *testing.T) { - // setup types - _setup := &Setup{ - Driver: "postgres", - Address: "postgres://foo:bar@localhost:5432/vela", - CompressionLevel: 3, - ConnectionLife: 10 * time.Second, - ConnectionIdle: 5, - ConnectionOpen: 20, - EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", - SkipCreation: false, - } - +func TestDatabase_Config_Validate(t *testing.T) { // setup tests tests := []struct { failure bool - setup *Setup - }{ - { - failure: true, - setup: _setup, - }, - { - failure: true, - setup: &Setup{Driver: "postgres"}, - }, - } - - // run tests - for _, test := range tests { - _, err := test.setup.Postgres() - - if test.failure { - if err == nil { - t.Errorf("Postgres should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("Postgres returned err: %v", err) - } - } -} - -func TestDatabase_Setup_Sqlite(t *testing.T) { - // setup types - _setup := &Setup{ - Driver: "sqlite3", - Address: "file::memory:?cache=shared", - CompressionLevel: 3, - ConnectionLife: 10 * time.Second, - ConnectionIdle: 5, - ConnectionOpen: 20, - EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", - SkipCreation: false, - } - - // setup tests - tests := []struct { - failure bool - setup *Setup + name string + config *config }{ { + name: "success with postgres", failure: false, - setup: _setup, - }, - { - failure: true, - setup: &Setup{Driver: "sqlite3"}, - }, - } - - // run tests - for _, test := range tests { - _, err := test.setup.Sqlite() - - if test.failure { - if err == nil { - t.Errorf("Sqlite should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("Sqlite returned err: %v", err) - } - } -} - -func TestDatabase_Setup_Validate(t *testing.T) { - // setup tests - tests := []struct { - failure bool - setup *Setup - }{ - { - failure: false, - setup: &Setup{ + config: &config{ Driver: "postgres", Address: "postgres://foo:bar@localhost:5432/vela", CompressionLevel: 3, @@ -121,8 +31,9 @@ func TestDatabase_Setup_Validate(t *testing.T) { }, }, { + name: "success with sqlite3", failure: false, - setup: &Setup{ + config: &config{ Driver: "sqlite3", Address: "file::memory:?cache=shared", CompressionLevel: 3, @@ -134,8 +45,9 @@ func TestDatabase_Setup_Validate(t *testing.T) { }, }, { + name: "success with negative compression level", failure: false, - setup: &Setup{ + config: &config{ Driver: "postgres", Address: "postgres://foo:bar@localhost:5432/vela", CompressionLevel: -1, @@ -147,10 +59,11 @@ func TestDatabase_Setup_Validate(t *testing.T) { }, }, { + name: "failure with empty driver", failure: true, - setup: &Setup{ - Driver: "postgres", - Address: "postgres://foo:bar@localhost:5432/vela/", + config: &config{ + Driver: "", + Address: "postgres://foo:bar@localhost:5432/vela", CompressionLevel: 3, ConnectionLife: 10 * time.Second, ConnectionIdle: 5, @@ -160,10 +73,11 @@ func TestDatabase_Setup_Validate(t *testing.T) { }, }, { + name: "failure with empty address", failure: true, - setup: &Setup{ - Driver: "", - Address: "postgres://foo:bar@localhost:5432/vela", + config: &config{ + Driver: "postgres", + Address: "", CompressionLevel: 3, ConnectionLife: 10 * time.Second, ConnectionIdle: 5, @@ -173,10 +87,11 @@ func TestDatabase_Setup_Validate(t *testing.T) { }, }, { + name: "failure with invalid address", failure: true, - setup: &Setup{ + config: &config{ Driver: "postgres", - Address: "", + Address: "postgres://foo:bar@localhost:5432/vela/", CompressionLevel: 3, ConnectionLife: 10 * time.Second, ConnectionIdle: 5, @@ -186,41 +101,44 @@ func TestDatabase_Setup_Validate(t *testing.T) { }, }, { + name: "failure with invalid compression level", failure: true, - setup: &Setup{ + config: &config{ Driver: "postgres", Address: "postgres://foo:bar@localhost:5432/vela", - CompressionLevel: 3, + CompressionLevel: 10, ConnectionLife: 10 * time.Second, ConnectionIdle: 5, ConnectionOpen: 20, - EncryptionKey: "", + EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", SkipCreation: false, }, }, { + name: "failure with empty encryption key", failure: true, - setup: &Setup{ + config: &config{ Driver: "postgres", Address: "postgres://foo:bar@localhost:5432/vela", CompressionLevel: 3, ConnectionLife: 10 * time.Second, ConnectionIdle: 5, ConnectionOpen: 20, - EncryptionKey: "A1B2C3D4E5G6H7I8J9K0", + EncryptionKey: "", SkipCreation: false, }, }, { + name: "failure with invalid encryption key", failure: true, - setup: &Setup{ + config: &config{ Driver: "postgres", Address: "postgres://foo:bar@localhost:5432/vela", - CompressionLevel: 10, + CompressionLevel: 3, ConnectionLife: 10 * time.Second, ConnectionIdle: 5, ConnectionOpen: 20, - EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + EncryptionKey: "A1B2C3D4E5G6H7I8J9K0", SkipCreation: false, }, }, @@ -228,18 +146,20 @@ func TestDatabase_Setup_Validate(t *testing.T) { // run tests for _, test := range tests { - err := test.setup.Validate() + t.Run(test.name, func(t *testing.T) { + err := test.config.Validate() - if test.failure { - if err == nil { - t.Errorf("Validate should have returned err") - } + if test.failure { + if err == nil { + t.Errorf("Validate for %s should have returned err", test.name) + } - continue - } + return + } - if err != nil { - t.Errorf("Validate returned err: %v", err) - } + if err != nil { + t.Errorf("Validate for %s returned err: %v", test.name, err) + } + }) } } diff --git a/database/sqlite/worker_count.go b/database/worker/count.go similarity index 53% rename from database/sqlite/worker_count.go rename to database/worker/count.go index 38b2cf7b9..8ac0f3eb5 100644 --- a/database/sqlite/worker_count.go +++ b/database/worker/count.go @@ -2,25 +2,24 @@ // // Use of this source code is governed by the LICENSE file in this repository. -package sqlite +package worker import ( - "github.com/go-vela/server/database/sqlite/dml" "github.com/go-vela/types/constants" ) -// GetWorkerCount gets a count of all workers from the database. -func (c *client) GetWorkerCount() (int64, error) { - c.Logger.Trace("getting count of workers from the database") +// CountWorkers gets the count of all workers from the database. +func (e *engine) CountWorkers() (int64, error) { + e.logger.Tracef("getting count of all workers from the database") // variable to store query results var w int64 // send query to the database and store result in variable - err := c.Sqlite. + err := e.client. Table(constants.TableWorker). - Raw(dml.SelectWorkersCount). - Pluck("count", &w).Error + Count(&w). + Error return w, err } diff --git a/database/worker/count_test.go b/database/worker/count_test.go new file mode 100644 index 000000000..bd9d4c4ac --- /dev/null +++ b/database/worker/count_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestWorker_Engine_CountWorkers(t *testing.T) { + // setup types + _workerOne := testWorker() + _workerOne.SetID(1) + _workerOne.SetHostname("worker_0") + _workerOne.SetAddress("localhost") + _workerOne.SetActive(true) + + _workerTwo := testWorker() + _workerTwo.SetID(2) + _workerTwo.SetHostname("worker_1") + _workerTwo.SetAddress("localhost") + _workerTwo.SetActive(true) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "workers"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateWorker(_workerOne) + if err != nil { + t.Errorf("unable to create test worker for sqlite: %v", err) + } + + err = _sqlite.CreateWorker(_workerTwo) + if err != nil { + t.Errorf("unable to create test worker for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountWorkers() + + if test.failure { + if err == nil { + t.Errorf("CountWorkers for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountWorkers for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountWorkers for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/worker/create.go b/database/worker/create.go new file mode 100644 index 000000000..6c62b30b6 --- /dev/null +++ b/database/worker/create.go @@ -0,0 +1,38 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreateWorker creates a new worker in the database. +func (e *engine) CreateWorker(w *library.Worker) error { + e.logger.WithFields(logrus.Fields{ + "worker": w.GetHostname(), + }).Tracef("creating worker %s in the database", w.GetHostname()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#WorkerFromLibrary + worker := database.WorkerFromLibrary(w) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Worker.Validate + err := worker.Validate() + if err != nil { + return err + } + + // send query to the database + return e.client. + Table(constants.TableWorker). + Create(worker). + Error +} diff --git a/database/worker/create_test.go b/database/worker/create_test.go new file mode 100644 index 000000000..e4c4dc9cb --- /dev/null +++ b/database/worker/create_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestWorker_Engine_CreateWorker(t *testing.T) { + // setup types + _worker := testWorker() + _worker.SetID(1) + _worker.SetHostname("worker_0") + _worker.SetAddress("localhost") + _worker.SetActive(true) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`INSERT INTO "workers" +("hostname","address","routes","active","status","last_status_update_at","running_build_ids","last_build_started_at","last_build_finished_at","last_checked_in","build_limit","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) RETURNING "id"`). + WithArgs("worker_0", "localhost", nil, true, nil, nil, nil, nil, nil, nil, nil, 1). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateWorker(_worker) + + if test.failure { + if err == nil { + t.Errorf("CreateWorker for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateWorker for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/worker/delete.go b/database/worker/delete.go new file mode 100644 index 000000000..a04ebb13e --- /dev/null +++ b/database/worker/delete.go @@ -0,0 +1,30 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// DeleteWorker deletes an existing worker from the database. +func (e *engine) DeleteWorker(w *library.Worker) error { + e.logger.WithFields(logrus.Fields{ + "worker": w.GetHostname(), + }).Tracef("deleting worker %s from the database", w.GetHostname()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#WorkerFromLibrary + worker := database.WorkerFromLibrary(w) + + // send query to the database + return e.client. + Table(constants.TableWorker). + Delete(worker). + Error +} diff --git a/database/worker/delete_test.go b/database/worker/delete_test.go new file mode 100644 index 000000000..c8a9bd1be --- /dev/null +++ b/database/worker/delete_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestWorker_Engine_DeleteWorker(t *testing.T) { + // setup types + _worker := testWorker() + _worker.SetID(1) + _worker.SetHostname("worker_0") + _worker.SetAddress("localhost") + _worker.SetActive(true) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`DELETE FROM "workers" WHERE "workers"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateWorker(_worker) + if err != nil { + t.Errorf("unable to create test worker for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeleteWorker(_worker) + + if test.failure { + if err == nil { + t.Errorf("DeleteWorker for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeleteWorker for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/worker/get.go b/database/worker/get.go new file mode 100644 index 000000000..dd2b07ecc --- /dev/null +++ b/database/worker/get.go @@ -0,0 +1,34 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetWorker gets a worker by ID from the database. +func (e *engine) GetWorker(id int64) (*library.Worker, error) { + e.logger.Tracef("getting worker %d from the database", id) + + // variable to store query results + w := new(database.Worker) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableWorker). + Where("id = ?", id). + Take(w). + Error + if err != nil { + return nil, err + } + + // return the worker + // + // https://pkg.go.dev/github.com/go-vela/types/database#Worker.ToLibrary + return w.ToLibrary(), nil +} diff --git a/database/worker/get_hostname.go b/database/worker/get_hostname.go new file mode 100644 index 000000000..6bcf42a2b --- /dev/null +++ b/database/worker/get_hostname.go @@ -0,0 +1,37 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetWorkerForHostname gets a worker by hostname from the database. +func (e *engine) GetWorkerForHostname(hostname string) (*library.Worker, error) { + e.logger.WithFields(logrus.Fields{ + "worker": hostname, + }).Tracef("getting worker %s from the database", hostname) + + // variable to store query results + w := new(database.Worker) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableWorker). + Where("hostname = ?", hostname). + Take(w). + Error + if err != nil { + return nil, err + } + + // return the worker + // + // https://pkg.go.dev/github.com/go-vela/types/database#Worker.ToLibrary + return w.ToLibrary(), nil +} diff --git a/database/worker/get_hostname_test.go b/database/worker/get_hostname_test.go new file mode 100644 index 000000000..3dd1d4fe6 --- /dev/null +++ b/database/worker/get_hostname_test.go @@ -0,0 +1,85 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestWorker_Engine_GetWorkerForName(t *testing.T) { + // setup types + _worker := testWorker() + _worker.SetID(1) + _worker.SetHostname("worker_0") + _worker.SetAddress("localhost") + _worker.SetActive(true) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "hostname", "address", "routes", "active", "status", "last_status_update_at", "running_build_ids", "last_build_started_at", "last_build_finished_at", "last_checked_in", "build_limit"}). + AddRow(1, "worker_0", "localhost", nil, true, nil, 0, nil, 0, 0, 0, 0) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "workers" WHERE hostname = $1 LIMIT 1`).WithArgs("worker_0").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateWorker(_worker) + if err != nil { + t.Errorf("unable to create test worker for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Worker + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _worker, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _worker, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetWorkerForHostname("worker_0") + + if test.failure { + if err == nil { + t.Errorf("GetWorkerForHostname for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetWorkerForHostname for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetWorkerForHostname for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/worker/get_test.go b/database/worker/get_test.go new file mode 100644 index 000000000..17fd03739 --- /dev/null +++ b/database/worker/get_test.go @@ -0,0 +1,85 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestWorker_Engine_GetWorker(t *testing.T) { + // setup types + _worker := testWorker() + _worker.SetID(1) + _worker.SetHostname("worker_0") + _worker.SetAddress("localhost") + _worker.SetActive(true) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "hostname", "address", "routes", "active", "last_checked_in", "build_limit"}). + AddRow(1, "worker_0", "localhost", nil, true, 0, 0) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "workers" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateWorker(_worker) + if err != nil { + t.Errorf("unable to create test worker for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.Worker + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _worker, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _worker, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetWorker(1) + + if test.failure { + if err == nil { + t.Errorf("GetWorker for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetWorker for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetWorker for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/worker/index.go b/database/worker/index.go new file mode 100644 index 000000000..f8f01a4b6 --- /dev/null +++ b/database/worker/index.go @@ -0,0 +1,24 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +const ( + // CreateHostnameAddressIndex represents a query to create an + // index on the workers table for the hostname and address columns. + CreateHostnameAddressIndex = ` +CREATE INDEX +IF NOT EXISTS +workers_hostname_address +ON workers (hostname, address); +` +) + +// CreateWorkerIndexes creates the indexes for the workers table in the database. +func (e *engine) CreateWorkerIndexes() error { + e.logger.Tracef("creating indexes for workers table in the database") + + // create the hostname and address columns index for the workers table + return e.client.Exec(CreateHostnameAddressIndex).Error +} diff --git a/database/worker/index_test.go b/database/worker/index_test.go new file mode 100644 index 000000000..ead204e5c --- /dev/null +++ b/database/worker/index_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestWorker_Engine_CreateWorkerIndexes(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreateHostnameAddressIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateWorkerIndexes() + + if test.failure { + if err == nil { + t.Errorf("CreateWorkerIndexes for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateWorkerIndexes for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/worker/interface.go b/database/worker/interface.go new file mode 100644 index 000000000..9e7fe1169 --- /dev/null +++ b/database/worker/interface.go @@ -0,0 +1,43 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "github.com/go-vela/types/library" +) + +// WorkerInterface represents the Vela interface for worker +// functions with the supported Database backends. +// +//nolint:revive // ignore name stutter +type WorkerInterface interface { + // Worker Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreateWorkerIndexes defines a function that creates the indexes for the workers table. + CreateWorkerIndexes() error + // CreateWorkerTable defines a function that creates the workers table. + CreateWorkerTable(string) error + + // Worker Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CountWorkers defines a function that gets the count of all workers. + CountWorkers() (int64, error) + // CreateWorker defines a function that creates a new worker. + CreateWorker(*library.Worker) error + // DeleteWorker defines a function that deletes an existing worker. + DeleteWorker(*library.Worker) error + // GetWorker defines a function that gets a worker by ID. + GetWorker(int64) (*library.Worker, error) + // GetWorkerForHostname defines a function that gets a worker by hostname. + GetWorkerForHostname(string) (*library.Worker, error) + // ListWorkers defines a function that gets a list of all workers. + ListWorkers() ([]*library.Worker, error) + // UpdateWorker defines a function that updates an existing worker. + UpdateWorker(*library.Worker) error +} diff --git a/database/postgres/worker_list.go b/database/worker/list.go similarity index 51% rename from database/postgres/worker_list.go rename to database/worker/list.go index 87bc66e7b..4ec11ef3d 100644 --- a/database/postgres/worker_list.go +++ b/database/worker/list.go @@ -2,38 +2,53 @@ // // Use of this source code is governed by the LICENSE file in this repository. -package postgres +package worker import ( - "github.com/go-vela/server/database/postgres/dml" "github.com/go-vela/types/constants" "github.com/go-vela/types/database" "github.com/go-vela/types/library" ) -// GetWorkerList gets a list of all workers from the database. -func (c *client) GetWorkerList() ([]*library.Worker, error) { - c.Logger.Trace("listing workers from the database") +// ListWorkers gets a list of all workers from the database. +func (e *engine) ListWorkers() ([]*library.Worker, error) { + e.logger.Trace("listing all workers from the database") - // variable to store query results + // variables to store query results and return value + count := int64(0) w := new([]database.Worker) + workers := []*library.Worker{} + + // count the results + count, err := e.CountWorkers() + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return workers, nil + } // send query to the database and store result in variable - err := c.Postgres. + err = e.client. Table(constants.TableWorker). - Raw(dml.ListWorkers). - Scan(w).Error + Find(&w). + Error + if err != nil { + return nil, err + } - // variable we want to return - workers := []*library.Worker{} // iterate through all query results for _, worker := range *w { // https://golang.org/doc/faq#closures_and_goroutines tmp := worker // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#Worker.ToLibrary workers = append(workers, tmp.ToLibrary()) } - return workers, err + return workers, nil } diff --git a/database/worker/list_test.go b/database/worker/list_test.go new file mode 100644 index 000000000..5eed3f94f --- /dev/null +++ b/database/worker/list_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestWorker_Engine_ListWorkers(t *testing.T) { + // setup types + _workerOne := testWorker() + _workerOne.SetID(1) + _workerOne.SetHostname("worker_0") + _workerOne.SetAddress("localhost") + _workerOne.SetActive(true) + + _workerTwo := testWorker() + _workerTwo.SetID(2) + _workerTwo.SetHostname("worker_1") + _workerTwo.SetAddress("localhost") + _workerTwo.SetActive(true) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "workers"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "hostname", "address", "routes", "active", "status", "last_status_update_at", "running_build_ids", "last_build_started_at", "last_build_finished_at", "last_checked_in", "build_limit"}). + AddRow(1, "worker_0", "localhost", nil, true, nil, 0, nil, 0, 0, 0, 0). + AddRow(2, "worker_1", "localhost", nil, true, nil, 0, nil, 0, 0, 0, 0) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "workers"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateWorker(_workerOne) + if err != nil { + t.Errorf("unable to create test worker for sqlite: %v", err) + } + + err = _sqlite.CreateWorker(_workerTwo) + if err != nil { + t.Errorf("unable to create test worker for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.Worker + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.Worker{_workerOne, _workerTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.Worker{_workerOne, _workerTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListWorkers() + + if test.failure { + if err == nil { + t.Errorf("ListWorkers for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListWorkers for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListWorkers for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/worker/opts.go b/database/worker/opts.go new file mode 100644 index 000000000..c9891ba94 --- /dev/null +++ b/database/worker/opts.go @@ -0,0 +1,44 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Workers. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Workers. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the worker engine + e.client = client + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Workers. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the worker engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Workers. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the worker engine + e.config.SkipCreation = skipCreation + + return nil + } +} diff --git a/database/worker/opts_test.go b/database/worker/opts_test.go new file mode 100644 index 000000000..a0ebf6aa5 --- /dev/null +++ b/database/worker/opts_test.go @@ -0,0 +1,161 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestWorker_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestWorker_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestWorker_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} diff --git a/database/worker/table.go b/database/worker/table.go new file mode 100644 index 000000000..1d704674a --- /dev/null +++ b/database/worker/table.go @@ -0,0 +1,69 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres workers table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +workers ( + id SERIAL PRIMARY KEY, + hostname VARCHAR(250), + address VARCHAR(250), + routes VARCHAR(1000), + active BOOLEAN, + status VARCHAR(50), + last_status_update_at INTEGER, + running_build_ids VARCHAR(500), + last_build_started_at INTEGER, + last_build_finished_at INTEGER, + last_checked_in INTEGER, + build_limit INTEGER, + UNIQUE(hostname) +); +` + // CreateSqliteTable represents a query to create the Sqlite workers table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +workers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hostname TEXT, + address TEXT, + routes TEXT, + active BOOLEAN, + status VARCHAR(50), + last_status_update_at INTEGER, + running_build_ids VARCHAR(500), + last_build_started_at INTEGER, + last_build_finished_at INTEGER, + last_checked_in INTEGER, + build_limit INTEGER, + UNIQUE(hostname) +); +` +) + +// CreateWorkerTable creates the workers table in the database. +func (e *engine) CreateWorkerTable(driver string) error { + e.logger.Tracef("creating workers table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the workers table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the workers table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/worker/table_test.go b/database/worker/table_test.go new file mode 100644 index 000000000..681a267f2 --- /dev/null +++ b/database/worker/table_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestWorker_Engine_CreateWorkerTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateWorkerTable(test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateWorkerTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateWorkerTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/worker/update.go b/database/worker/update.go new file mode 100644 index 000000000..b0e475273 --- /dev/null +++ b/database/worker/update.go @@ -0,0 +1,38 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// UpdateWorker updates an existing worker in the database. +func (e *engine) UpdateWorker(w *library.Worker) error { + e.logger.WithFields(logrus.Fields{ + "worker": w.GetHostname(), + }).Tracef("updating worker %s in the database", w.GetHostname()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#WorkerFromLibrary + worker := database.WorkerFromLibrary(w) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#Worker.Validate + err := worker.Validate() + if err != nil { + return err + } + + // send query to the database + return e.client. + Table(constants.TableWorker). + Save(worker). + Error +} diff --git a/database/worker/update_test.go b/database/worker/update_test.go new file mode 100644 index 000000000..0beeafa47 --- /dev/null +++ b/database/worker/update_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestWorker_Engine_UpdateWorker(t *testing.T) { + // setup types + _worker := testWorker() + _worker.SetID(1) + _worker.SetHostname("worker_0") + _worker.SetAddress("localhost") + _worker.SetActive(true) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`UPDATE "workers" +SET "hostname"=$1,"address"=$2,"routes"=$3,"active"=$4,"status"=$5,"last_status_update_at"=$6,"running_build_ids"=$7,"last_build_started_at"=$8,"last_build_finished_at"=$9,"last_checked_in"=$10,"build_limit"=$11 +WHERE "id" = $12`). + WithArgs("worker_0", "localhost", nil, true, nil, nil, nil, nil, nil, nil, nil, 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateWorker(_worker) + if err != nil { + t.Errorf("unable to create test worker for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.UpdateWorker(_worker) + + if test.failure { + if err == nil { + t.Errorf("UpdateWorker for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateWorker for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/worker/worker.go b/database/worker/worker.go new file mode 100644 index 000000000..d18aa6408 --- /dev/null +++ b/database/worker/worker.go @@ -0,0 +1,80 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the WorkerInterface interface. + config struct { + // specifies to skip creating tables and indexes for the Worker engine + SkipCreation bool + } + + // engine represents the worker functionality that implements the WorkerInterface interface. + engine struct { + // engine configuration settings used in worker functions + config *config + + // gorm.io/gorm database client used in worker functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in worker functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with workers in the database. +// +//nolint:revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new Worker engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating worker database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of workers table and indexes in the database") + + return e, nil + } + + // create the workers table + err := e.CreateWorkerTable(e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableWorker, err) + } + + // create the indexes for the workers table + err = e.CreateWorkerIndexes() + if err != nil { + return nil, fmt.Errorf("unable to create indexes for %s table: %w", constants.TableWorker, err) + } + + return e, nil +} diff --git a/database/worker/worker_test.go b/database/worker/worker_test.go new file mode 100644 index 000000000..00096c2a9 --- /dev/null +++ b/database/worker/worker_test.go @@ -0,0 +1,186 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package worker + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestWorker_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateHostnameAddressIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{SkipCreation: false}, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithClient(test.client), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateHostnameAddressIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres worker engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite worker engine: %v", err) + } + + return _engine +} + +// testWorker is a test helper function to create a library +// Worker type with all fields set to their zero values. +func testWorker() *library.Worker { + return &library.Worker{ + ID: new(int64), + Hostname: new(string), + Address: new(string), + Routes: new([]string), + Active: new(bool), + Status: new(string), + LastStatusUpdateAt: new(int64), + RunningBuildIDs: new([]string), + LastBuildStartedAt: new(int64), + LastBuildFinishedAt: new(int64), + LastCheckedIn: new(int64), + BuildLimit: new(int64), + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 7d34df72f..6a596e45e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: # managing resources in the database and publishing # builds to the FIFO queue. # - # https://go-vela.github.io/docs/concepts/infrastructure/server/ + # https://go-vela.github.io/docs/administration/server/ server: build: context: . @@ -28,6 +28,7 @@ services: DATABASE_ENCRYPTION_KEY: 'C639A572E14D5075C526FDDD43E4ECF6' QUEUE_DRIVER: redis QUEUE_ADDR: 'redis://redis:6379' + QUEUE_PRIVATE_KEY: 'tCIevHOBq6DdN5SSBtteXUusjjd0fOqzk2eyi0DMq04NewmShNKQeUbbp3vkvIckb4pCxc+vxUo+mYf/vzOaSg==' SCM_DRIVER: github SCM_CONTEXT: 'continuous-integration/vela' SECRET_VAULT: 'true' @@ -37,10 +38,13 @@ services: VELA_WEBUI_ADDR: 'http://localhost:8888' VELA_LOG_LEVEL: trace VELA_SECRET: 'zB7mrKDTZqNeNTD8z47yG4DHywspAh' - VELA_REFRESH_TOKEN_DURATION: 90m - VELA_ACCESS_TOKEN_DURATION: 60m + VELA_SERVER_PRIVATE_KEY: 'F534FF2A080E45F38E05DC70752E6787' + VELA_USER_REFRESH_TOKEN_DURATION: 90m + VELA_USER_ACCESS_TOKEN_DURATION: 60m VELA_DISABLE_WEBHOOK_VALIDATION: 'true' VELA_ENABLE_SECURE_COOKIE: 'false' + VELA_REPO_ALLOWLIST: '*' + VELA_SCHEDULE_ALLOWLIST: '*' env_file: - .env restart: always @@ -56,23 +60,26 @@ services: # This component is used for pulling builds from the FIFO # queue and executing them based off their configuration. # - # https://go-vela.github.io/docs/concepts/infrastructure/worker/ + # https://go-vela.github.io/docs/administration/worker/ worker: container_name: worker image: target/vela-worker:latest networks: - vela environment: + EXECUTOR_DRIVER: linux QUEUE_DRIVER: redis QUEUE_ADDR: 'redis://redis:6379' + QUEUE_PUBLIC_KEY: 'DXsJkoTSkHlG26d75LyHJG+KQsXPr8VKPpmH/78zmko=' VELA_BUILD_LIMIT: 1 VELA_BUILD_TIMEOUT: 30m VELA_LOG_LEVEL: trace VELA_RUNTIME_DRIVER: docker + VELA_RUNTIME_PRIVILEGED_IMAGES: 'target/vela-docker' VELA_SERVER_ADDR: 'http://server:8080' VELA_SERVER_SECRET: 'zB7mrKDTZqNeNTD8z47yG4DHywspAh' WORKER_ADDR: 'http://worker:8080' - WORKER_CHECK_IN: 15m + WORKER_CHECK_IN: 5m restart: always ports: - '8081:8080' @@ -86,7 +93,7 @@ services: # This component is used for providing a user-friendly # interface for triggering actions in the Vela system. # - # https://go-vela.github.io/docs/concepts/infrastructure/ui/ + # https://go-vela.github.io/docs/administration/ui/ ui: container_name: ui image: target/vela-ui:latest @@ -109,7 +116,7 @@ services: # https://redis.io/ redis: container_name: redis - image: redis:6-alpine + image: redis:7-alpine networks: - vela ports: @@ -122,7 +129,7 @@ services: # https://www.postgresql.org/ postgres: container_name: postgres - image: postgres:14-alpine + image: postgres:15-alpine networks: - vela environment: @@ -138,7 +145,7 @@ services: # # https://www.vaultproject.io/ vault: - image: vault:latest + image: hashicorp/vault:latest container_name: vault command: server -dev networks: @@ -152,4 +159,4 @@ services: - IPC_LOCK networks: - vela: + vela: \ No newline at end of file diff --git a/go.mod b/go.mod index cced08cac..788f5cdb0 100644 --- a/go.mod +++ b/go.mod @@ -1,138 +1,130 @@ module github.com/go-vela/server -go 1.17 +go 1.19 require ( github.com/Bose/minisentinel v0.0.0-20200130220412-917c5a9223bb github.com/DATA-DOG/go-sqlmock v1.5.0 - github.com/Masterminds/semver/v3 v3.1.1 - github.com/Masterminds/sprig/v3 v3.2.2 - github.com/alicebob/miniredis/v2 v2.18.0 - github.com/aws/aws-sdk-go v1.42.27 + github.com/Masterminds/semver/v3 v3.2.1 + github.com/Masterminds/sprig/v3 v3.2.3 + github.com/adhocore/gronx v1.6.4 + github.com/alicebob/miniredis/v2 v2.30.4 + github.com/aws/aws-sdk-go v1.44.309 github.com/buildkite/yaml v0.0.0-20181016232759-0caa5f0796e3 github.com/drone/envsubst v1.0.3 - github.com/gin-gonic/gin v1.7.7 - github.com/go-playground/assert/v2 v2.0.1 - github.com/go-redis/redis/v8 v8.11.4 - github.com/go-vela/types v0.12.0-rc1 - github.com/golang-jwt/jwt/v4 v4.2.0 - github.com/google/go-cmp v0.5.6 - github.com/google/go-github/v42 v42.0.0 + github.com/gin-gonic/gin v1.9.1 + github.com/go-playground/assert/v2 v2.2.0 + github.com/go-vela/types v0.20.2-0.20230822144153-14b37585731d + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/google/go-cmp v0.5.9 + github.com/google/go-github/v53 v53.2.0 github.com/google/uuid v1.3.0 - github.com/goware/urlx v0.3.1 + github.com/goware/urlx v0.3.2 github.com/hashicorp/go-cleanhttp v0.5.2 - github.com/hashicorp/go-retryablehttp v0.7.0 - github.com/hashicorp/vault/api v1.3.1 - github.com/joho/godotenv v1.4.0 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-retryablehttp v0.7.4 + github.com/hashicorp/vault/api v1.9.2 + github.com/joho/godotenv v1.5.1 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.12.0 - github.com/sirupsen/logrus v1.8.1 - github.com/spf13/afero v1.8.0 - github.com/urfave/cli/v2 v2.3.0 - go.starlark.net v0.0.0-20211203141949-70c0e40ae128 - golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 + github.com/prometheus/client_golang v1.16.0 + github.com/redis/go-redis/v9 v9.0.5 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/afero v1.9.5 + github.com/urfave/cli/v2 v2.25.7 + go.starlark.net v0.0.0-20230725161458-0d7263928a74 + golang.org/x/crypto v0.11.0 + golang.org/x/oauth2 v0.9.0 + golang.org/x/sync v0.3.0 gopkg.in/square/go-jose.v2 v2.6.0 - gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 - gorm.io/driver/postgres v1.2.3 - gorm.io/driver/sqlite v1.2.6 - gorm.io/gorm v1.22.4 - k8s.io/apimachinery v0.23.1 + gorm.io/driver/postgres v1.5.2 + gorm.io/driver/sqlite v1.5.2 + gorm.io/gorm v1.25.2 + k8s.io/apimachinery v0.27.4 ) require ( github.com/Masterminds/goutils v1.1.1 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect - github.com/armon/go-metrics v0.3.9 // indirect - github.com/armon/go-radix v1.0.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/sonic v1.9.1 // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/cloudflare/circl v1.3.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fatih/color v1.10.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-logr/logr v1.2.0 // indirect - github.com/go-playground/locales v0.13.0 // indirect - github.com/go-playground/universal-translator v0.17.0 // indirect - github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/gorilla/css v1.0.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-hclog v0.16.2 // indirect - github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-plugin v1.4.3 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect - github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 // indirect - github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect github.com/hashicorp/go-sockaddr v1.0.2 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect - github.com/hashicorp/go-version v1.2.0 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/vault/sdk v0.3.0 // indirect - github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect - github.com/huandu/xstrings v1.3.2 // indirect + github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.11 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.10.1 // indirect - github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.2.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect - github.com/jackc/pgtype v1.9.0 // indirect - github.com/jackc/pgx/v4 v4.14.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.3.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.3 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kr/pretty v0.3.0 // indirect - github.com/leodido/go-urn v1.2.0 // indirect - github.com/lib/pq v1.10.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-colorable v0.1.8 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect - github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/microcosm-cc/bluemonday v1.0.17 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/microcosm-cc/bluemonday v1.0.25 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/go-testing-interface v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.4.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/oklog/run v1.0.0 // indirect - github.com/pierrec/lz4 v2.5.2+incompatible // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect - github.com/prometheus/procfs v0.7.3 // indirect - github.com/russross/blackfriday/v2 v2.0.1 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/spf13/cast v1.3.1 // indirect - github.com/ugorji/go/codec v1.1.11 // indirect - github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da // indirect - go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa // indirect - golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect - golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + github.com/yuin/gopher-lua v1.1.0 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/net v0.12.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 // indirect - google.golang.org/grpc v1.41.0 // indirect - google.golang.org/protobuf v1.27.1 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/klog/v2 v2.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.90.1 // indirect + k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect ) diff --git a/go.sum b/go.sum index 771fa64d8..f109e9873 100644 --- a/go.sum +++ b/go.sum @@ -42,155 +42,115 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/FZambia/sentinel v1.0.0 h1:KJ0ryjKTZk5WMp0dXvSdNqp3lFaW1fNFuEYfrkLOYIc= github.com/FZambia/sentinel v1.0.0/go.mod h1:ytL1Am/RLlAoAXG6Kj5LNuw/TRRQrv2rt2FT26vP5gI= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= -github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= 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/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/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/adhocore/gronx v1.6.4 h1:Bx5cNRVQsGquOOUJL3+2M5vlz1KCCMHrCECwb5UghNU= +github.com/adhocore/gronx v1.6.4/go.mod h1:7oUY1WAU8rEJWmAxXR2DN0JaO4gi9khSgKjiRypqteg= github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.11.1/go.mod h1:UA48pmi7aSazcGAvcdKcBB49z521IC9VjTTRz2nIaJE= -github.com/alicebob/miniredis/v2 v2.18.0 h1:EPUGD69ou4Uw4c81t9NLh0+dSou46k4tFEvf498FJ0g= -github.com/alicebob/miniredis/v2 v2.18.0/go.mod h1:gquAfGbzn92jvtrSC69+6zZnwSODVXVpYDRaGhWaL6I= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/go-metrics v0.3.9 h1:O2sNqxBdvq8Eq5xmzljcYzAORli6RWCvEym4cJf9m18= -github.com/armon/go-metrics v0.3.9/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/alicebob/miniredis/v2 v2.30.4 h1:8S4/o1/KoUArAGbGwPxcwf0krlzceva2XVOSchFS7Eo= +github.com/alicebob/miniredis/v2 v2.30.4/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -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-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.42.27 h1:kxsBXQg3ee6LLbqjp5/oUeDgG7TENFrWYDmEVnd7spU= -github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= +github.com/aws/aws-sdk-go v1.44.309 h1:IPJOFBzXekakxmEpDwd4RTKmmBR6LIAiXgNsM51bWbU= +github.com/aws/aws-sdk-go v1.44.309/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bradleyfalzon/ghinstallation/v2 v2.0.3/go.mod h1:tlgi+JWCXnKFx/Y4WtnDbZEINo31N5bcvnCoqieefmk= +github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= +github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= github.com/buildkite/yaml v0.0.0-20181016232759-0caa5f0796e3 h1:q+sMKdA6L8LyGVudTkpGoC73h6ak2iWSPFiFo/pFOU8= github.com/buildkite/yaml v0.0.0-20181016232759-0caa5f0796e3/go.mod h1:5hCug3EZaHXU3FdCA3gJm0YTNi+V+ooA2qNTiVpky4A= +github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 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/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= -github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 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-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 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.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.5.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= -github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk= -github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= -github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= -github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 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/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= -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/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= -github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= -github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-vela/types v0.12.0-rc1 h1:/qXnZ10AAlJ7l4Rr/FkAfhGFz8G9ww1VkedAXJatHu8= -github.com/go-vela/types v0.12.0-rc1/go.mod h1:nMZJ/0tb0HO8/AVaJXHuR5slG9UPuP9or+CnkuyFcL4= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/go-vela/types v0.20.2-0.20230822144153-14b37585731d h1:ag6trc3Ev+7hzifeWy0M9rHHjrO9nFCYgW8dlKdZ4j4= +github.com/go-vela/types v0.20.2-0.20230822144153-14b37585731d/go.mod h1:AXO4oQSygOBQ02fPapsKjQHkx2aQO3zTu7clpvVbXBY= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 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= @@ -217,10 +177,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/gomodule/redigo v1.7.1-0.20190322064113-39e2c31b7ca3/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= @@ -236,15 +194,13 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/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/go-github/v39 v39.0.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= -github.com/google/go-github/v42 v42.0.0 h1:YNT0FwjPrEysRkLIiKuEfSvBPCGKphW5aS5PxwaoLec= -github.com/google/go-github/v42 v42.0.0/go.mod h1:jgg/jvyI0YlDOM1/ps6XYh04HNQ3vKf0CVko62/EhRg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v53 v53.2.0 h1:wvz3FyF53v4BK+AsnvCmeNhf8AkTaeh2SoYu/XUvTtI= +github.com/google/go-github/v53 v53.2.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -267,383 +223,202 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/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/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/goware/urlx v0.3.1 h1:BbvKl8oiXtJAzOzMqAQ0GfIhf96fKeNEZfm9ocNSUBI= -github.com/goware/urlx v0.3.1/go.mod h1:h8uwbJy68o+tQXCGZNa9D73WN8n0r9OBae5bUnLcgjw= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/goware/urlx v0.3.2 h1:gdoo4kBHlkqZNaf6XlQ12LGtQOmpKJrR04Rc3RnpJEo= +github.com/goware/urlx v0.3.2/go.mod h1:h8uwbJy68o+tQXCGZNa9D73WN8n0r9OBae5bUnLcgjw= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= -github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= -github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= -github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4= -github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= +github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc= -github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI= -github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= -github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo= -github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8= github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U= -github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 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= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= -github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 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/vault/api v1.3.1 h1:pkDkcgTh47PRjY1NEFeofqR4W/HkNUi9qIakESO2aRM= -github.com/hashicorp/vault/api v1.3.1/go.mod h1:QeJoWxMFt+MsuWcYhmwRLwKEXrjwAFFywzhptMsTIUw= -github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZduTKEY= -github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -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/hashicorp/vault/api v1.9.2 h1:YjkZLJ7K3inKgMZ0wzCU9OHqc+UqMQyXsPXnf3Cl2as= +github.com/hashicorp/vault/api v1.9.2/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/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/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.10.1 h1:DzdIHIjG1AxGwoEEqS+mGsURyjt4enSmqzACXvVzOT8= -github.com/jackc/pgconn v1.10.1/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.2.0 h1:r7JypeP2D3onoQTCxWdTpCtJ4D+qpKr0TxvoyMhZ5ns= -github.com/jackc/pgproto3/v2 v2.2.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.9.0 h1:/SH1RxEtltvJgsDqp3TbiTFApD3mey3iygpuEGeuBXk= -github.com/jackc/pgtype v1.9.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.14.0 h1:TgdrmgnM7VY72EuSQzBbBd4JA1RLqJolrw9nQVZABVc= -github.com/jackc/pgx/v4 v4.14.0/go.mod h1:jT3ibf/A0ZVCp89rtCIN0zCJxcE74ypROmHEZYsG/j8= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.2.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= -github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jinzhu/now v1.1.3 h1:PlHq1bSCSZL9K0wUhbm2pGLoTWs2GwVhsP6emvGV/ZI= -github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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/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/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.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.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= -github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -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/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -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.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= -github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y= -github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= +github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 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.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 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/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.1 h1:FVzMWA5RllMAKIdUSC8mdWo3XtwoecrH79BY70sEEpE= github.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 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/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= -github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= -github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg= -github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -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/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= +github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -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.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60= -github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= 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/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= 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= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.1.11 h1:O5AKWOf+CnfWi6L1WtdBtZpA+YNjoQd2YfbtkowsMrs= -github.com/ugorji/go v1.1.11/go.mod h1:kbRrdMyHY64ADdazOwkrQP9btxt35Z26OJueD3Tq0/4= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.1.11 h1:GaQDxjNe1J3vCZvlVaDjUIHIbFuUByFXY7rMqnhB5ck= -github.com/ugorji/go/codec v1.1.11/go.mod h1:svMFxxx5FVQJPnJ9vbpAgscNufuiXDyldvzApI86qQo= -github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= -github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= github.com/yuin/gopher-lua v0.0.0-20191213034115-f46add6fdb5c/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= -github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da h1:NimzV1aGyq29m5ukMK0AMWEhFaL/lrEOaephfuoiARg= -github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= +github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 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/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.starlark.net v0.0.0-20211203141949-70c0e40ae128 h1:bxH+EXOo87zEOwKDdZ8Tevgi6irRbqheRm/fr293c58= -go.starlark.net v0.0.0-20211203141949-70c0e40ae128/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -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.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.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/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.starlark.net v0.0.0-20230725161458-0d7263928a74 h1:EL8MuNFlzO8vvpHgZxDGPaehP0ozoJ1j1zA768zKXUQ= +go.starlark.net v0.0.0-20230725161458-0d7263928a74/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 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= @@ -677,12 +452,9 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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-20181114220301-adae6a3d119a/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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -690,12 +462,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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-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= @@ -706,7 +475,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ 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-20200520004742-59133d7f0dd7/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-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -716,13 +484,12 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY 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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 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= @@ -732,9 +499,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ 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-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs= +golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw= 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= @@ -745,34 +511,23 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 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-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/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= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -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-20190606165138-5da285871e9c/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-20190904154756-749cb33beabd/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-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/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= @@ -785,48 +540,49 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w 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-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/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-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/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-20210603081109-ebe580a85c40/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -834,18 +590,14 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 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-20190823170909-c4a336ef6a2f/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-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-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -853,7 +605,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= @@ -866,7 +617,6 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 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-20200505023115-26f46d2f7ef8/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= @@ -878,18 +628,14 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u 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-20201224043029-2b0845dc783e/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.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 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/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -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= @@ -918,7 +664,6 @@ google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID 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-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 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= @@ -942,7 +687,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG 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= @@ -950,15 +694,12 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 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-20201019141844-1ed22bb0c154/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-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 h1:PYBmACG+YEv8uQPW0r1kJj8tR+gkF0UWq7iFdUezwEw= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -972,13 +713,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 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.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E= -google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= 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= @@ -991,45 +728,28 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj 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= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 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/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs= -gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= -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/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -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= -gorm.io/driver/postgres v1.2.3 h1:f4t0TmNMy9gh3TU2PX+EppoA6YsgFnyq8Ojtddb42To= -gorm.io/driver/postgres v1.2.3/go.mod h1:pJV6RgYQPG47aM1f0QeOzFH9HxQc8JcmAgjRCgS0wjs= -gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4= -gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY= -gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= -gorm.io/gorm v1.22.4 h1:8aPcyEJhY0MAt8aY6Dc524Pn+pO29K+ydu+e/cXSpQM= -gorm.io/gorm v1.22.4/go.mod h1:1aeVC+pe9ZmvKZban/gW4QPra7PRoTEssyc922qCAkk= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= +gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= +gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc= +gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= +gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 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= @@ -1037,21 +757,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 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= -k8s.io/apimachinery v0.23.1 h1:sfBjlDFwj2onG0Ijx5C+SrAoeUscPrmghm7wHP+uXlo= -k8s.io/apimachinery v0.23.1/go.mod h1:SADt2Kl8/sttJ62RRsi9MIV4o8f5S3coArm0Iu3fBno= -k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= -k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= -k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/apimachinery v0.27.4 h1:CdxflD4AF61yewuid0fLl6bM4a3q04jWel0IlP+aYjs= +k8s.io/apimachinery v0.27.4/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= +k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 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/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= diff --git a/internal/token/compose.go b/internal/token/compose.go new file mode 100644 index 000000000..61bea7a32 --- /dev/null +++ b/internal/token/compose.go @@ -0,0 +1,75 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package token + +import ( + "net/http" + "net/url" + + "github.com/gin-gonic/gin" + "github.com/go-vela/types" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" +) + +// Compose generates a refresh and access token pair unique +// to the provided user and sets a secure cookie. +// It uses the user's hash to sign the token. to +// guarantee the signature is unique per token. The refresh +// token is returned to store with the user +// in the database. +func (tm *Manager) Compose(c *gin.Context, u *library.User) (string, string, error) { + // grab the metadata from the context to pull in provided + // cookie duration information + m := c.MustGet("metadata").(*types.Metadata) + + // mint token options for refresh token + rmto := MintTokenOpts{ + User: u, + TokenType: constants.UserRefreshTokenType, + TokenDuration: tm.UserRefreshTokenDuration, + } + + // create a refresh token with the provided options + refreshToken, err := tm.MintToken(&rmto) + if err != nil { + return "", "", err + } + + // mint token options for access token + amto := MintTokenOpts{ + User: u, + TokenType: constants.UserAccessTokenType, + TokenDuration: tm.UserAccessTokenDuration, + } + + // create an access token with the provided options + accessToken, err := tm.MintToken(&amto) + if err != nil { + return "", "", err + } + + // parse the address for the backend server + // so we can set it for the cookie domain + addr, err := url.Parse(m.Vela.Address) + if err != nil { + return "", "", err + } + + refreshExpiry := int(tm.UserRefreshTokenDuration.Seconds()) + + // set the SameSite value for the cookie + // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#samesite-attribute + // We set to Lax because we will have links from source provider web UI. + // Setting this to Strict would force a login when navigating via source provider web UI links. + c.SetSameSite(http.SameSiteLaxMode) + // set the cookie with the refresh token as a HttpOnly, Secure cookie + // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#httponly-attribute + // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#secure-attribute + c.SetCookie(constants.RefreshTokenName, refreshToken, refreshExpiry, "/", addr.Hostname(), c.Value("securecookie").(bool), true) + + // return the refresh and access tokens + return refreshToken, accessToken, nil +} diff --git a/internal/token/compose_test.go b/internal/token/compose_test.go new file mode 100644 index 000000000..ff01b543d --- /dev/null +++ b/internal/token/compose_test.go @@ -0,0 +1,80 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package token + +import ( + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/types" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + + jwt "github.com/golang-jwt/jwt/v5" +) + +func TestToken_Compose(t *testing.T) { + // setup types + u := new(library.User) + u.SetID(1) + u.SetName("foo") + u.SetToken("bar") + u.SetHash("baz") + + tm := &Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + d := time.Minute * 5 + now := time.Now() + exp := now.Add(d) + + claims := &Claims{ + IsActive: u.GetActive(), + IsAdmin: u.GetAdmin(), + TokenType: constants.UserAccessTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: u.GetName(), + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(exp), + }, + } + + tkn := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + want, err := tkn.SignedString([]byte(tm.PrivateKey)) + if err != nil { + t.Errorf("Unable to create test token: %v", err) + } + + m := &types.Metadata{ + Vela: &types.Vela{ + AccessTokenDuration: d, + }, + } + + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, _ := gin.CreateTestContext(resp) + context.Set("metadata", m) + context.Set("securecookie", false) + + // run test + _, got, err := tm.Compose(context, u) + if err != nil { + t.Errorf("Compose returned err: %v", err) + } + + if !strings.EqualFold(got, want) { + t.Errorf("Compose is %v, want %v", got, want) + } +} diff --git a/internal/token/manager.go b/internal/token/manager.go new file mode 100644 index 000000000..22daf3636 --- /dev/null +++ b/internal/token/manager.go @@ -0,0 +1,34 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package token + +import ( + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type Manager struct { + // PrivateKey key used to sign tokens + PrivateKey string + + // SignMethod method to sign tokens + SignMethod jwt.SigningMethod + + // UserAccessTokenDuration specifies the token duration to use for users + UserAccessTokenDuration time.Duration + + // UserRefreshTokenDuration specifies the token duration for user refresh + UserRefreshTokenDuration time.Duration + + // BuildTokenBufferDuration specifies the additional token duration of build tokens beyond repo timeout + BuildTokenBufferDuration time.Duration + + // WorkerAuthTokenDuration specifies the token duration for worker auth (check in) + WorkerAuthTokenDuration time.Duration + + // WorkerRegisterTokenDuration specifies the token duration for worker register + WorkerRegisterTokenDuration time.Duration +} diff --git a/internal/token/mint.go b/internal/token/mint.go new file mode 100644 index 000000000..7d2ff8f64 --- /dev/null +++ b/internal/token/mint.go @@ -0,0 +1,96 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package token + +import ( + "errors" + "fmt" + "time" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/golang-jwt/jwt/v5" +) + +// Claims struct is an extension of the JWT standard claims. It +// includes information about the user. +type Claims struct { + BuildID int64 `json:"build_id"` + IsActive bool `json:"is_active"` + IsAdmin bool `json:"is_admin"` + Repo string `json:"repo"` + TokenType string `json:"token_type"` + jwt.RegisteredClaims +} + +// MintTokenOpts is a type to inform the token minter how to construct +// the token. +type MintTokenOpts struct { + BuildID int64 + Hostname string + Repo string + TokenDuration time.Duration + TokenType string + User *library.User +} + +// MintToken mints a Vela JWT Token given a set of options. +func (tm *Manager) MintToken(mto *MintTokenOpts) (string, error) { + // initialize claims struct + var claims = new(Claims) + + // apply claims based on token type + switch mto.TokenType { + case constants.UserAccessTokenType, constants.UserRefreshTokenType: + if mto.User == nil { + return "", fmt.Errorf("no user provided for user access token") + } + + claims.IsActive = mto.User.GetActive() + claims.IsAdmin = mto.User.GetAdmin() + claims.Subject = mto.User.GetName() + + case constants.WorkerBuildTokenType: + if mto.BuildID == 0 { + return "", errors.New("missing build id for build token") + } + + if len(mto.Repo) == 0 { + return "", errors.New("missing repo for build token") + } + + if len(mto.Hostname) == 0 { + return "", errors.New("missing host name for build token") + } + + claims.BuildID = mto.BuildID + claims.Repo = mto.Repo + claims.Subject = mto.Hostname + + case constants.WorkerAuthTokenType, constants.WorkerRegisterTokenType: + if len(mto.Hostname) == 0 { + return "", fmt.Errorf("missing host name for %s token", mto.TokenType) + } + + claims.Subject = mto.Hostname + + default: + return "", errors.New("invalid token type") + } + + claims.IssuedAt = jwt.NewNumericDate(time.Now()) + claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(mto.TokenDuration)) + claims.TokenType = mto.TokenType + + tk := jwt.NewWithClaims(tm.SignMethod, claims) + + //sign token with configured private signing key + token, err := tk.SignedString([]byte(tm.PrivateKey)) + if err != nil { + return "", fmt.Errorf("unable to sign token: %w", err) + } + + return token, nil +} diff --git a/internal/token/parse.go b/internal/token/parse.go new file mode 100644 index 000000000..ad5423ee6 --- /dev/null +++ b/internal/token/parse.go @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package token + +import ( + "errors" + + "github.com/golang-jwt/jwt/v5" +) + +// ParseToken scans the signed JWT token as a string and extracts +// the user login from the claims to be looked up in the database. +// This function will return an error for a few different reasons: +// +// * the token signature doesn't match what is expected +// * the token signing method doesn't match what is expected +// * the token is invalid (potentially expired or improper). +func (tm *Manager) ParseToken(token string) (*Claims, error) { + var claims = new(Claims) + + // create a new JWT parser + p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name})) + + // parse and validate given token + tkn, err := p.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) { + var err error + + // extract the claims from the token + claims = t.Claims.(*Claims) + name := claims.Subject + + // check if subject has a value in claims; + // we can save a db lookup attempt + if len(name) == 0 { + return nil, errors.New("no subject defined") + } + + // ParseWithClaims will skip expiration check + // if expiration has default value; + // forcing a check and exiting if not set + if claims.ExpiresAt == nil { + return nil, errors.New("token has no expiration") + } + + return []byte(tm.PrivateKey), err + }) + + if err != nil { + return nil, errors.New("failed parsing: " + err.Error()) + } + + if !tkn.Valid { + return nil, errors.New("invalid token") + } + + return claims, nil +} diff --git a/internal/token/parse_test.go b/internal/token/parse_test.go new file mode 100644 index 000000000..c7dc2422f --- /dev/null +++ b/internal/token/parse_test.go @@ -0,0 +1,299 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package token + +import ( + "reflect" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + + jwt "github.com/golang-jwt/jwt/v5" +) + +func TestTokenManager_ParseToken(t *testing.T) { + // setup types + u := new(library.User) + u.SetID(1) + u.SetName("foo") + u.SetToken("bar") + u.SetHash("baz") + + tm := &Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + now := time.Now() + + tests := []struct { + TokenType string + Mto *MintTokenOpts + Want *Claims + }{ + { + TokenType: constants.UserAccessTokenType, + Mto: &MintTokenOpts{ + User: u, + TokenType: constants.UserAccessTokenType, + TokenDuration: tm.UserAccessTokenDuration, + }, + Want: &Claims{ + IsActive: u.GetActive(), + IsAdmin: u.GetAdmin(), + TokenType: constants.UserAccessTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: u.GetName(), + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 5)), + }, + }, + }, + { + TokenType: constants.UserRefreshTokenType, + Mto: &MintTokenOpts{ + User: u, + TokenType: constants.UserRefreshTokenType, + TokenDuration: tm.UserRefreshTokenDuration, + }, + Want: &Claims{ + IsActive: u.GetActive(), + IsAdmin: u.GetAdmin(), + TokenType: constants.UserRefreshTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: u.GetName(), + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 30)), + }, + }, + }, + { + TokenType: constants.WorkerBuildTokenType, + Mto: &MintTokenOpts{ + BuildID: 1, + Repo: "foo/bar", + Hostname: "worker", + TokenType: constants.WorkerBuildTokenType, + TokenDuration: time.Minute * 90, + }, + Want: &Claims{ + BuildID: 1, + Repo: "foo/bar", + TokenType: constants.WorkerBuildTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "worker", + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 90)), + }, + }, + }, + } + + gin.SetMode(gin.TestMode) + + for _, tt := range tests { + t.Run(tt.TokenType, func(t *testing.T) { + tkn, err := tm.MintToken(tt.Mto) + if err != nil { + t.Errorf("Unable to create token: %v", err) + } + // run test + got, err := tm.ParseToken(tkn) + if err != nil { + t.Errorf("Parse returned err: %v", err) + } + + if !reflect.DeepEqual(got, tt.Want) { + t.Errorf("Parse is %v, want %v", got, tt.Want) + } + }) + } +} + +func TestTokenManager_ParseToken_Error_NoParse(t *testing.T) { + // setup types + u := new(library.User) + u.SetID(1) + u.SetName("foo") + u.SetToken("bar") + u.SetHash("baz") + + tm := &Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + // run test + got, err := tm.ParseToken("!@#$%^&*()") + if err == nil { + t.Errorf("Parse should have returned err") + } + + if got != nil { + t.Errorf("Parse is %v, want nil", got) + } +} + +func TestTokenManager_ParseToken_Expired(t *testing.T) { + // setup types + u := new(library.User) + u.SetID(1) + u.SetName("foo") + u.SetToken("bar") + u.SetHash("baz") + + tm := &Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + mto := &MintTokenOpts{ + User: u, + TokenType: constants.UserAccessTokenType, + TokenDuration: time.Minute * -1, + } + + tkn, err := tm.MintToken(mto) + if err != nil { + t.Errorf("Unable to create token: %v", err) + } + + // run test + _, err = tm.ParseToken(tkn) + if err == nil { + t.Errorf("Parse should return error due to expiration") + } +} + +func TestTokenManager_ParseToken_NoSubject(t *testing.T) { + // setup types + u := new(library.User) + u.SetID(1) + u.SetName("foo") + u.SetToken("bar") + u.SetHash("baz") + + tm := &Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + claims := &Claims{ + IsActive: u.GetActive(), + IsAdmin: u.GetAdmin(), + TokenType: constants.UserRefreshTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now()), + }, + } + tkn := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + token, err := tkn.SignedString([]byte(tm.PrivateKey)) + if err != nil { + t.Errorf("Unable to create test token: %v", err) + } + + // run test + got, err := tm.ParseToken(token) + if err == nil { + t.Errorf("Parse should have returned err") + } + + if got != nil { + t.Errorf("Parse is %v, want nil", got) + } +} + +func TestTokenManager_ParseToken_Error_InvalidSignature(t *testing.T) { + // setup types + u := new(library.User) + u.SetID(1) + u.SetName("foo") + u.SetToken("bar") + u.SetHash("baz") + + tm := &Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + claims := &Claims{ + IsActive: u.GetActive(), + IsAdmin: u.GetAdmin(), + TokenType: constants.UserAccessTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: u.GetName(), + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 1)), + }, + } + tkn := jwt.NewWithClaims(jwt.SigningMethodHS512, claims) + + token, err := tkn.SignedString([]byte(tm.PrivateKey)) + if err != nil { + t.Errorf("Unable to create test token: %v", err) + } + + // run test + got, err := tm.ParseToken(token) + if err == nil { + t.Errorf("Parse should have returned err") + } + + if got != nil { + t.Errorf("Parse is %v, want nil", got) + } +} + +func TestToken_Parse_AccessToken_NoExpiration(t *testing.T) { + // setup types + u := new(library.User) + u.SetID(1) + u.SetName("foo") + u.SetToken("bar") + u.SetHash("baz") + + tm := &Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + claims := &Claims{ + TokenType: constants.UserAccessTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "user", + }, + } + tkn := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + token, err := tkn.SignedString([]byte(u.GetHash())) + if err != nil { + t.Errorf("Unable to create test token: %v", err) + } + + // run test + got, err := tm.ParseToken(token) + if err == nil { + t.Errorf("Parse should have returned err") + } + + if got != nil { + t.Errorf("Parse is %v, want nil", got) + } +} diff --git a/internal/token/refresh.go b/internal/token/refresh.go new file mode 100644 index 000000000..8cb69f374 --- /dev/null +++ b/internal/token/refresh.go @@ -0,0 +1,43 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package token + +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/types/constants" +) + +// Refresh returns a new access token, if the provided refreshToken is valid. +func (tm *Manager) Refresh(c *gin.Context, refreshToken string) (string, error) { + // retrieve claims from token + claims, err := tm.ParseToken(refreshToken) + if err != nil { + return "", err + } + + // look up user in database given claims subject + u, err := database.FromContext(c).GetUserForName(claims.Subject) + if err != nil { + return "", fmt.Errorf("unable to retrieve user %s from database from claims subject: %w", claims.Subject, err) + } + + // options for user access token minting + amto := &MintTokenOpts{ + User: u, + TokenType: constants.UserAccessTokenType, + TokenDuration: tm.UserAccessTokenDuration, + } + + // create a new access token + at, err := tm.MintToken(amto) + if err != nil { + return "", err + } + + return at, nil +} diff --git a/internal/token/refresh_test.go b/internal/token/refresh_test.go new file mode 100644 index 000000000..e306f9ed6 --- /dev/null +++ b/internal/token/refresh_test.go @@ -0,0 +1,131 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package token + +import ( + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/golang-jwt/jwt/v5" +) + +func TestTokenManager_Refresh(t *testing.T) { + // setup types + u := new(library.User) + u.SetID(1) + u.SetName("foo") + u.SetToken("bar") + u.SetHash("baz") + + tm := &Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + mto := &MintTokenOpts{ + User: u, + TokenType: constants.UserRefreshTokenType, + TokenDuration: tm.UserRefreshTokenDuration, + } + + rt, err := tm.MintToken(mto) + if err != nil { + t.Errorf("unable to create refresh token") + } + + u.SetRefreshToken(rt) + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteUser(u) + db.Close() + }() + + _ = db.CreateUser(u) + + // set up context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, _ := gin.CreateTestContext(resp) + context.Set("database", db) + + // run tests + got, err := tm.Refresh(context, rt) + if err != nil { + t.Error("Refresh should not error") + } + + if len(got) == 0 { + t.Errorf("Refresh should have returned an access token") + } +} + +func TestTokenManager_Refresh_Expired(t *testing.T) { + // setup types + u := new(library.User) + u.SetID(1) + u.SetName("foo") + u.SetToken("bar") + u.SetHash("baz") + + tm := &Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + mto := &MintTokenOpts{ + User: u, + TokenType: constants.UserRefreshTokenType, + TokenDuration: time.Minute * -1, + } + + rt, err := tm.MintToken(mto) + if err != nil { + t.Errorf("unable to create refresh token") + } + + u.SetRefreshToken(rt) + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteUser(u) + db.Close() + }() + + _ = db.CreateUser(u) + + // set up context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, _ := gin.CreateTestContext(resp) + context.Set("database", db) + + // run tests + _, err = tm.Refresh(context, rt) + if err == nil { + t.Error("Refresh with expired token should error") + } +} diff --git a/mock/server/authentication.go b/mock/server/authentication.go index 2d100f0d4..ea7bb76a1 100644 --- a/mock/server/authentication.go +++ b/mock/server/authentication.go @@ -16,7 +16,7 @@ import ( const ( // TokenRefreshResp represents a JSON return for a token refresh. - // nolint:gosec // not a hardcoded credential + //nolint:gosec // not a hardcoded credential TokenRefreshResp = `{ "token": "header.payload.signature" }` @@ -26,7 +26,7 @@ const ( func getTokenRefresh(c *gin.Context) { data := []byte(TokenRefreshResp) - var body library.Login + var body library.Token _ = json.Unmarshal(data, &body) c.JSON(http.StatusOK, body) @@ -48,7 +48,7 @@ func getAuthenticate(c *gin.Context) { return } - var body library.Login + var body library.Token _ = json.Unmarshal(data, &body) c.SetCookie(constants.RefreshTokenName, "refresh", 2, "/", "", true, true) @@ -68,8 +68,22 @@ func getAuthenticateFromToken(c *gin.Context) { c.AbortWithStatusJSON(http.StatusUnauthorized, types.Error{Message: &err}) } - var body library.Login + var body library.Token _ = json.Unmarshal(data, &body) c.JSON(http.StatusOK, body) } + +// validateToken returns mock response for a http GET. +// +// Don't pass "Authorization" in header to receive an unauthorized error message. +func validateToken(c *gin.Context) { + err := "error" + + token := c.Request.Header.Get("Authorization") + if len(token) == 0 { + c.AbortWithStatusJSON(http.StatusUnauthorized, types.Error{Message: &err}) + } + + c.JSON(http.StatusOK, "vela-server") +} diff --git a/mock/server/build.go b/mock/server/build.go index 19f12a950..cc24af60c 100644 --- a/mock/server/build.go +++ b/mock/server/build.go @@ -142,6 +142,23 @@ const ( "full_name": "github/octocat" } ]` + + // BuildTokenResp represents a JSON return for requesting a build token + // + //nolint:gosec // not actual credentials + BuildTokenResp = `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJidWlsZF9pZCI6MSwicmVwbyI6ImZvby9iYXIiLCJzdWIiOiJPY3RvY2F0IiwiaWF0IjoxNTE2MjM5MDIyfQ.hD7gXpaf9acnLBdOBa4GOEa5KZxdzd0ZvK6fGwaN4bc" + }` + + // BuildExecutableResp represents a JSON return for requesting a build executable. + BuildExecutableResp = `{ + "id": 1 + "build_id": 1, + "data": "eyAKICAgICJpZCI6ICJzdGVwX25hbWUiLAogICAgInZlcnNpb24iOiAiMSIsCiAgICAibWV0YWRhdGEiOnsKICAgICAgICAiY2xvbmUiOnRydWUsCiAgICAgICAgImVudmlyb25tZW50IjpbInN0ZXBzIiwic2VydmljZXMiLCJzZWNyZXRzIl19LAogICAgIndvcmtlciI6e30sCiAgICAic3RlcHMiOlsKICAgICAgICB7CiAgICAgICAgICAgICJpZCI6InN0ZXBfZ2l0aHViX29jdG9jYXRfMV9pbml0IiwKICAgICAgICAgICAgImRpcmVjdG9yeSI6Ii92ZWxhL3NyYy9naXRodWIuY29tL2dpdGh1Yi9vY3RvY2F0IiwKICAgICAgICAgICAgImVudmlyb25tZW50IjogeyJCVUlMRF9BVVRIT1IiOiJPY3RvY2F0In0KICAgICAgICB9CiAgICBdCn0KCg==" + }` + + // CleanResourcesResp represents a string return for cleaning resources as an admin. + CleanResourcesResp = "42 builds cleaned. 42 services cleaned. 42 steps cleaned." ) // getBuilds returns mock JSON for a http GET. @@ -305,3 +322,72 @@ func buildQueue(c *gin.Context) { c.JSON(http.StatusOK, body) } + +// buildToken has a param :build returns mock JSON for a http GET. +// +// Pass "0" to :build to test receiving a http 404 response. Pass "2" +// to :build to test receiving a http 400 response. +func buildToken(c *gin.Context) { + b := c.Param("build") + + if strings.EqualFold(b, "0") { + c.AbortWithStatusJSON(http.StatusNotFound, "") + + return + } + + if strings.EqualFold(b, "2") { + c.AbortWithStatusJSON(http.StatusBadRequest, "") + + return + } + + data := []byte(BuildTokenResp) + + var body library.Token + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + +// buildExecutable has a param :build returns mock JSON for a http GET. +// +// Pass "0" to :build to test receiving a http 500 response. +func buildExecutable(c *gin.Context) { + b := c.Param("build") + + if strings.EqualFold(b, "0") { + msg := fmt.Sprintf("unable to get build executable for build %s", b) + + c.AbortWithStatusJSON(http.StatusInternalServerError, types.Error{Message: &msg}) + + return + } + + data := []byte(BuildExecutableResp) + + var body library.BuildExecutable + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + +// cleanResources has a query param :before returns mock JSON for a http PUT +// +// Pass "1" to :before to test receiving a http 500 response. Pass "2" to :before +// to test receiving a http 401 response. +func cleanResoures(c *gin.Context) { + before := c.Query("before") + + if strings.EqualFold(before, "1") { + c.AbortWithStatusJSON(http.StatusInternalServerError, "") + + return + } + + if strings.EqualFold(before, "2") { + c.AbortWithStatusJSON(http.StatusUnauthorized, "") + } + + c.JSON(http.StatusOK, CleanResourcesResp) +} diff --git a/mock/server/hook.go b/mock/server/hook.go index 0c356c3c5..d2c21da46 100644 --- a/mock/server/hook.go +++ b/mock/server/hook.go @@ -2,6 +2,7 @@ // // Use of this source code is governed by the LICENSE file in this repository. +//nolint:dupl // ignore duplicate with user code package server import ( diff --git a/mock/server/log.go b/mock/server/log.go index 8a101c4d2..3becfff38 100644 --- a/mock/server/log.go +++ b/mock/server/log.go @@ -51,12 +51,7 @@ func getServiceLog(c *gin.Context) { // addServiceLog returns mock JSON for a http GET. func addServiceLog(c *gin.Context) { - data := []byte(LogResp) - - var body library.Log - _ = json.Unmarshal(data, &body) - - c.JSON(http.StatusCreated, body) + c.JSON(http.StatusCreated, nil) } // updateServiceLog has a param :service returns mock JSON for a http PUT. @@ -73,12 +68,7 @@ func updateServiceLog(c *gin.Context) { return } - data := []byte(LogResp) - - var body library.Log - _ = json.Unmarshal(data, &body) - - c.JSON(http.StatusOK, body) + c.JSON(http.StatusOK, nil) } // removeServiceLog has a param :service returns mock JSON for a http DELETE. @@ -122,12 +112,7 @@ func getStepLog(c *gin.Context) { // addStepLog returns mock JSON for a http GET. func addStepLog(c *gin.Context) { - data := []byte(LogResp) - - var body library.Log - _ = json.Unmarshal(data, &body) - - c.JSON(http.StatusCreated, body) + c.JSON(http.StatusCreated, nil) } // updateStepLog has a param :step returns mock JSON for a http PUT. @@ -144,12 +129,7 @@ func updateStepLog(c *gin.Context) { return } - data := []byte(LogResp) - - var body library.Log - _ = json.Unmarshal(data, &body) - - c.JSON(http.StatusOK, body) + c.JSON(http.StatusOK, nil) } // removeStepLog has a param :step returns mock JSON for a http DELETE. diff --git a/mock/server/pipeline.go b/mock/server/pipeline.go index 6d886e0e7..021960ea3 100644 --- a/mock/server/pipeline.go +++ b/mock/server/pipeline.go @@ -5,10 +5,13 @@ package server import ( + "encoding/json" "fmt" "net/http" "strings" + "github.com/go-vela/types/library" + "github.com/gin-gonic/gin" "github.com/go-vela/types" "github.com/go-vela/types/yaml" @@ -102,37 +105,62 @@ templates: source: github.com/go-vela/vela-tutorials/templates/sample.yml type: github ` - - // PipelineResp represents a YAML return for a single pipeline. - PipelineResp = `--- -version: "1" - -secrets: - - name: docker_username - key: go-vela/docker/username - engine: native - type: org - - - name: docker_password - key: go-vela/docker/password - engine: native - type: org - -steps: - - name: go - template: - name: sample - - - name: non-template-echo - image: golang:latest - commands: - - echo hello - -templates: - - name: sample - source: github.com/go-vela/vela-tutorials/templates/sample.yml - type: github -` + // PipelineResp represents a JSON return for a single pipeline. + PipelineResp = `{ + "id": 1, + "repo_id": 1, + "commit": "48afb5bdc41ad69bf22588491333f7cf71135163", + "flavor": "", + "platform": "", + "ref": "refs/heads/master", + "type": "yaml", + "version": "1", + "external_secrets": false, + "internal_secrets": false, + "services": false, + "stages": false, + "steps": true, + "templates": false, + "data": "LS0tCnZlcnNpb246ICIxIgoKc3RlcHM6CiAgLSBuYW1lOiBlY2hvCiAgICBpbWFnZTogYWxwaW5lOmxhdGVzdAogICAgY29tbWFuZHM6IFtlY2hvIGZvb10=" +}` + + // PipelinesResp represents a JSON return for one to many hooks. + PipelinesResp = `[ + { + "id": 2 + "repo_id": 1, + "commit": "a49aaf4afae6431a79239c95247a2b169fd9f067", + "flavor": "", + "platform": "", + "ref": "refs/heads/master", + "type": "yaml", + "version": "1", + "external_secrets": false, + "internal_secrets": false, + "services": false, + "stages": false, + "steps": true, + "templates": false, + "data": "LS0tCnZlcnNpb246ICIxIgoKc3RlcHM6CiAgLSBuYW1lOiBlY2hvCiAgICBpbWFnZTogYWxwaW5lOmxhdGVzdAogICAgY29tbWFuZHM6IFtlY2hvIGZvb10=" + }, + { + "id": 1, + "repo_id": 1, + "commit": "48afb5bdc41ad69bf22588491333f7cf71135163", + "flavor": "", + "platform": "", + "ref": "refs/heads/master", + "type": "yaml", + "version": "1", + "external_secrets": false, + "internal_secrets": false, + "services": false, + "stages": false, + "steps": true, + "templates": false, + "data": "LS0tCnZlcnNpb246ICIxIgoKc3RlcHM6CiAgLSBuYW1lOiBlY2hvCiAgICBpbWFnZTogYWxwaW5lOmxhdGVzdAogICAgY29tbWFuZHM6IFtlY2hvIGZvb10=" + } +]` // TemplateResp represents a YAML return for templates in a pipeline. TemplateResp = `--- @@ -143,14 +171,24 @@ sample: ` ) -// getPipeline has a param :repo returns mock YAML for a http GET. +// getPipelines returns mock JSON for a http GET. +func getPipelines(c *gin.Context) { + data := []byte(PipelinesResp) + + var body []library.Pipeline + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + +// getPipeline has a param :pipeline returns mock YAML for a http GET. // -// Pass "not-found" to :repo to test receiving a http 404 response. +// Pass "0" to :pipeline to test receiving a http 404 response. func getPipeline(c *gin.Context) { - r := c.Param("repo") + p := c.Param("pipeline") - if strings.Contains(r, "not-found") { - msg := fmt.Sprintf("Repo %s does not exist", r) + if strings.EqualFold(p, "0") { + msg := fmt.Sprintf("Pipeline %s does not exist", p) c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) @@ -159,20 +197,71 @@ func getPipeline(c *gin.Context) { data := []byte(PipelineResp) - var body yaml.Build - _ = yml.Unmarshal(data, &body) + var body library.Pipeline + _ = json.Unmarshal(data, &body) - c.YAML(http.StatusOK, body) + c.JSON(http.StatusOK, body) +} + +// addPipeline returns mock JSON for a http POST. +func addPipeline(c *gin.Context) { + data := []byte(PipelineResp) + + var body library.Pipeline + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusCreated, body) +} + +// updatePipeline has a param :pipeline returns mock JSON for a http PUT. +// +// Pass "0" to :pipeline to test receiving a http 404 response. +func updatePipeline(c *gin.Context) { + if !strings.Contains(c.FullPath(), "admin") { + p := c.Param("pipeline") + + if strings.EqualFold(p, "0") { + msg := fmt.Sprintf("Pipeline %s does not exist", p) + + c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) + + return + } + } + + data := []byte(PipelineResp) + + var body library.Pipeline + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + +// removePipeline has a param :pipeline returns mock JSON for a http DELETE. +// +// Pass "0" to :pipeline to test receiving a http 404 response. +func removePipeline(c *gin.Context) { + p := c.Param("pipeline") + + if strings.EqualFold(p, "0") { + msg := fmt.Sprintf("Pipeline %s does not exist", p) + + c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("Pipeline %s removed", p)) } -// compilePipeline has a param :repo returns mock YAML for a http GET. +// compilePipeline has a param :pipeline returns mock YAML for a http GET. // -// Pass "not-found" to :repo to test receiving a http 404 response. +// Pass "0" to :pipeline to test receiving a http 404 response. func compilePipeline(c *gin.Context) { - r := c.Param("repo") + p := c.Param("pipeline") - if strings.Contains(r, "not-found") { - msg := fmt.Sprintf("Repo %s does not exist", r) + if strings.EqualFold(p, "0") { + msg := fmt.Sprintf("Pipeline %s does not exist", p) c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) @@ -187,14 +276,14 @@ func compilePipeline(c *gin.Context) { c.YAML(http.StatusOK, body) } -// expandPipeline has a param :repo returns mock YAML for a http GET. +// expandPipeline has a param :pipeline returns mock YAML for a http GET. // -// Pass "not-found" to :repo to test receiving a http 404 response. +// Pass "0" to :pipeline to test receiving a http 404 response. func expandPipeline(c *gin.Context) { - r := c.Param("repo") + p := c.Param("pipeline") - if strings.Contains(r, "not-found") { - msg := fmt.Sprintf("Repo %s does not exist", r) + if strings.EqualFold(p, "0") { + msg := fmt.Sprintf("Pipeline %s does not exist", p) c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) @@ -209,14 +298,14 @@ func expandPipeline(c *gin.Context) { c.YAML(http.StatusOK, body) } -// getTemplates has a param :repo returns mock YAML for a http GET. +// getTemplates has a param :pipeline returns mock YAML for a http GET. // -// Pass "not-found" to :repo to test receiving a http 404 response. +// Pass "0" to :pipeline to test receiving a http 404 response. func getTemplates(c *gin.Context) { - r := c.Param("repo") + p := c.Param("pipeline") - if strings.Contains(r, "not-found") { - msg := fmt.Sprintf("Repo %s does not exist", r) + if strings.EqualFold(p, "0") { + msg := fmt.Sprintf("Pipeline %s does not exist", p) c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) @@ -231,14 +320,14 @@ func getTemplates(c *gin.Context) { c.YAML(http.StatusOK, body) } -// validatePipeline has a param :repo returns mock YAML for a http GET. +// validatePipeline has a param :pipeline returns mock YAML for a http GET. // -// Pass "not-found" to :repo to test receiving a http 404 response. +// Pass "0" to :pipeline to test receiving a http 404 response. func validatePipeline(c *gin.Context) { - r := c.Param("repo") + p := c.Param("pipeline") - if strings.Contains(r, "not-found") { - msg := fmt.Sprintf("Repo %s does not exist", r) + if strings.EqualFold(p, "0") { + msg := fmt.Sprintf("Pipeline %s does not exist", p) c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) diff --git a/mock/server/schedule.go b/mock/server/schedule.go new file mode 100644 index 000000000..a1acc54f7 --- /dev/null +++ b/mock/server/schedule.go @@ -0,0 +1,213 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package server + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/go-vela/types" + "github.com/go-vela/types/library" +) + +const ( + // ScheduleResp represents a JSON return for a single schedule. + ScheduleResp = `{ + "id": 2, + "active": true, + "name": "foo", + "entry": "@weekly", + "created_at": 1683154980, + "created_by": "octocat", + "updated_at": 1683154980, + "updated_by": "octocat", + "scheduled_at": 0, + "repo": { + "id": 1, + "user_id": 1, + "org": "github", + "name": "octocat", + "full_name": "github/octocat", + "link": "https://github.com/github/octocat", + "clone": "https://github.com/github/octocat.git", + "branch": "main", + "topics": [], + "build_limit": 10, + "timeout": 30, + "counter": 0, + "visibility": "public", + "private": false, + "trusted": false, + "active": true, + "allow_pull": false, + "allow_push": true, + "allow_deploy": false, + "allow_tag": false, + "allow_comment": false, + "pipeline_type": "yaml", + "previous_name": "" + } +}` + SchedulesResp = `[ + { + "id": 2, + "active": true, + "name": "foo", + "entry": "@weekly", + "created_at": 1683154980, + "created_by": "octocat", + "updated_at": 1683154980, + "updated_by": "octocat", + "scheduled_at": 0, + "repo": { + "id": 1, + "user_id": 1, + "org": "github", + "name": "octokitty", + "full_name": "github/octokitty", + "link": "https://github.com/github/octokitty", + "clone": "https://github.com/github/octokitty.git", + "branch": "main", + "topics": [], + "build_limit": 10, + "timeout": 30, + "counter": 0, + "visibility": "public", + "private": false, + "trusted": false, + "active": true, + "allow_pull": false, + "allow_push": true, + "allow_deploy": false, + "allow_tag": false, + "allow_comment": false, + "pipeline_type": "yaml", + "previous_name": "" + } + }, + { + "id": 1, + "active": true, + "name": "bar", + "entry": "@weekly", + "created_at": 1683154974, + "created_by": "octocat", + "updated_at": 1683154974, + "updated_by": "octocat", + "scheduled_at": 0, + "repo": { + "id": 1, + "user_id": 1, + "org": "github", + "name": "octokitty", + "full_name": "github/octokitty", + "link": "https://github.com/github/octokitty", + "clone": "https://github.com/github/octokitty.git", + "branch": "main", + "topics": [], + "build_limit": 10, + "timeout": 30, + "counter": 0, + "visibility": "public", + "private": false, + "trusted": false, + "active": true, + "allow_pull": false, + "allow_push": true, + "allow_deploy": false, + "allow_tag": false, + "allow_comment": false, + "pipeline_type": "yaml", + "previous_name": "" + } + } +]` +) + +// getSchedules returns mock JSON for a http GET. +func getSchedules(c *gin.Context) { + data := []byte(SchedulesResp) + + var body []library.Schedule + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + +// getSchedule has a param :schedule returns mock JSON for a http GET. +// +// Pass "not-found" to :schedule to test receiving a http 404 response. +func getSchedule(c *gin.Context) { + s := c.Param("schedule") + + if strings.Contains(s, "not-found") { + msg := fmt.Sprintf("Schedule %s does not exist", s) + + c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) + + return + } + + data := []byte(ScheduleResp) + + var body library.Schedule + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + +// addSchedule returns mock JSON for a http POST. +func addSchedule(c *gin.Context) { + data := []byte(ScheduleResp) + + var body library.Schedule + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusCreated, body) +} + +// updateSchedule has a param :schedule returns mock JSON for a http PUT. +// +// Pass "not-found" to :schedule to test receiving a http 404 response. +func updateSchedule(c *gin.Context) { + if !strings.Contains(c.FullPath(), "admin") { + s := c.Param("schedule") + + if strings.Contains(s, "not-found") { + msg := fmt.Sprintf("Schedule %s does not exist", s) + + c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) + + return + } + } + + data := []byte(ScheduleResp) + + var body library.Schedule + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + +// removeSchedule has a param :schedule returns mock JSON for a http DELETE. +// +// Pass "not-found" to :schedule to test receiving a http 404 response. +func removeSchedule(c *gin.Context) { + s := c.Param("schedule") + + if strings.Contains(s, "not-found") { + msg := fmt.Sprintf("Schedule %s does not exist", s) + + c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) + + return + } + + c.JSON(http.StatusOK, fmt.Sprintf("schedule %s deleted", s)) +} diff --git a/mock/server/secret.go b/mock/server/secret.go index 3ef924d67..eb1bcd72a 100644 --- a/mock/server/secret.go +++ b/mock/server/secret.go @@ -2,6 +2,7 @@ // // Use of this source code is governed by the LICENSE file in this repository. +//nolint:dupl // ignore duplicate with user code package server import ( @@ -15,7 +16,7 @@ import ( "github.com/go-vela/types/library" ) -// nolint:gosec // these are mock responses +//nolint:gosec // these are mock responses const ( // SecretResp represents a JSON return for a single secret. SecretResp = `{ diff --git a/mock/server/server.go b/mock/server/server.go index e36b1076e..7bc668719 100644 --- a/mock/server/server.go +++ b/mock/server/server.go @@ -12,30 +12,25 @@ import ( // FakeHandler returns an http.Handler that is capable of handling // Vela API requests and returning mock responses. -// nolint:funlen // number of endpoints is causing linter warning +// +//nolint:funlen // number of endpoints is causing linter warning func FakeHandler() http.Handler { gin.SetMode(gin.TestMode) e := gin.New() // mock endpoints for admin calls - e.GET("/api/v1/admin/builds", getBuilds) e.PUT("/api/v1/admin/build", updateBuild) e.GET("/api/v1/admin/builds/queue", buildQueue) - e.GET("/api/v1/admin/deployments", getDeployments) e.PUT("/api/v1/admin/deployment", updateDeployment) - e.GET("/api/v1/admin/hooks", getHooks) e.PUT("/api/v1/admin/hook", updateHook) - e.GET("/api/v1/admin/repos", getRepos) e.PUT("/api/v1/admin/repo", updateRepo) - e.GET("/api/v1/admin/secrets", getSecrets) e.PUT("/api/v1/admin/secret", updateSecret) - e.GET("/api/v1/admin/services", getServices) e.PUT("/api/v1/admin/service", updateService) - e.GET("/api/v1/admin/steps", getSteps) e.PUT("/api/v1/admin/step", updateStep) - e.GET("/api/v1/admin/users", getUsers) e.PUT("/api/v1/admin/user", updateUser) + e.POST("/api/v1/admin/workers/:worker/register-token", registerToken) + e.PUT("api/v1/admin/clean", cleanResoures) // mock endpoints for build calls e.GET("/api/v1/repos/:org/:repo/builds/:build", getBuild) @@ -46,6 +41,8 @@ func FakeHandler() http.Handler { e.POST("/api/v1/repos/:org/:repo/builds", addBuild) e.PUT("/api/v1/repos/:org/:repo/builds/:build", updateBuild) e.DELETE("/api/v1/repos/:org/:repo/builds/:build", removeBuild) + e.GET("/api/v1/repos/:org/:repo/builds/:build/token", buildToken) + e.GET("/api/v1/repos/:org/:repo/builds/:build/executable", buildExecutable) // mock endpoints for deployment calls e.GET("/api/v1/deployments/:org/:repo", getDeployments) @@ -70,11 +67,15 @@ func FakeHandler() http.Handler { e.DELETE("/api/v1/repos/:org/:repo/builds/:build/steps/:step/logs", removeStepLog) // mock endpoints for pipeline calls - e.GET("/api/v1/pipelines/:org/:repo", getPipeline) - e.POST("/api/v1/pipelines/:org/:repo/compile", compilePipeline) - e.POST("/api/v1/pipelines/:org/:repo/expand", expandPipeline) - e.GET("/api/v1/pipelines/:org/:repo/templates", getTemplates) - e.POST("/api/v1/pipelines/:org/:repo/validate", validatePipeline) + e.POST("/api/v1/pipelines/:org/:repo", addPipeline) + e.GET("/api/v1/pipelines/:org/:repo", getPipelines) + e.GET("/api/v1/pipelines/:org/:repo/:pipeline", getPipeline) + e.PUT("/api/v1/pipelines/:org/:repo/:pipeline", updatePipeline) + e.DELETE("/api/v1/pipelines/:org/:repo/:pipeline", removePipeline) + e.POST("/api/v1/pipelines/:org/:repo/:pipeline/compile", compilePipeline) + e.POST("/api/v1/pipelines/:org/:repo/:pipeline/expand", expandPipeline) + e.GET("/api/v1/pipelines/:org/:repo/:pipeline/templates", getTemplates) + e.POST("/api/v1/pipelines/:org/:repo/:pipeline/validate", validatePipeline) // mock endpoints for repo calls e.GET("/api/v1/repos/:org/:repo", getRepo) @@ -100,7 +101,6 @@ func FakeHandler() http.Handler { e.POST("/api/v1/repos/:org/:repo/builds/:build/steps", addStep) e.PUT("/api/v1/repos/:org/:repo/builds/:build/steps/:step", updateStep) e.DELETE("/api/v1/repos/:org/:repo/builds/:build/steps/:step", removeStep) - e.POST("/api/v1/repos/:org/:repo/builds/:build/steps/:step/stream", postStepStream) // mock endpoints for service calls e.GET("/api/v1/repos/:org/:repo/builds/:build/services/:service", getService) @@ -108,7 +108,6 @@ func FakeHandler() http.Handler { e.POST("/api/v1/repos/:org/:repo/builds/:build/services", addService) e.PUT("/api/v1/repos/:org/:repo/builds/:build/services/:service", updateService) e.DELETE("/api/v1/repos/:org/:repo/builds/:build/services/:service", removeService) - e.POST("/api/v1/repos/:org/:repo/builds/:build/services/:service/stream", postServiceStream) // mock endpoints for user calls e.GET("/api/v1/users/:user", getUser) @@ -122,12 +121,21 @@ func FakeHandler() http.Handler { e.GET("/api/v1/workers/:worker", getWorker) e.POST("/api/v1/workers", addWorker) e.PUT("/api/v1/workers/:worker", updateWorker) + e.POST("/api/v1/workers/:worker/refresh", refreshWorkerAuth) e.DELETE("/api/v1/workers/:worker", removeWorker) + // mock endpoints for schedule calls + e.GET("/api/v1/schedules/:org/:repo", getSchedules) + e.GET("/api/v1/schedules/:org/:repo/:schedule", getSchedule) + e.POST("/api/v1/schedules/:org/:repo", addSchedule) + e.PUT("/api/v1/schedules/:org/:repo/:schedule", updateSchedule) + e.DELETE("/api/v1/schedules/:org/:repo/:schedule", removeSchedule) + // mock endpoints for authentication calls e.GET("/token-refresh", getTokenRefresh) e.GET("/authenticate", getAuthenticate) e.POST("/authenticate/token", getAuthenticateFromToken) + e.GET("/validate-token", validateToken) return e } diff --git a/mock/server/service.go b/mock/server/service.go index b461bde65..0d4ce36f1 100644 --- a/mock/server/service.go +++ b/mock/server/service.go @@ -2,6 +2,7 @@ // // Use of this source code is governed by the LICENSE file in this repository. +//nolint:dupl // ignore duplicate with user code package server import ( diff --git a/mock/server/step.go b/mock/server/step.go index e0826d45a..c7111aded 100644 --- a/mock/server/step.go +++ b/mock/server/step.go @@ -2,6 +2,7 @@ // // Use of this source code is governed by the LICENSE file in this repository. +//nolint:dupl // ignore duplicate with user code package server import ( diff --git a/mock/server/stream.go b/mock/server/stream.go deleted file mode 100644 index 05209fb1a..000000000 --- a/mock/server/stream.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package server - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -// postServiceStream returns a nock response for an http POST. -func postServiceStream(c *gin.Context) { - c.JSON(http.StatusNoContent, nil) -} - -// postStepStream returns a nock response for an http POST. -func postStepStream(c *gin.Context) { - c.JSON(http.StatusNoContent, nil) -} diff --git a/mock/server/user.go b/mock/server/user.go index 3d883274d..85029962b 100644 --- a/mock/server/user.go +++ b/mock/server/user.go @@ -2,6 +2,7 @@ // // Use of this source code is governed by the LICENSE file in this repository. +//nolint:dupl // ignore duplicate with user code package server import ( diff --git a/mock/server/worker.go b/mock/server/worker.go index ed79507bb..51aedfa80 100644 --- a/mock/server/worker.go +++ b/mock/server/worker.go @@ -58,6 +58,23 @@ const ( "last_checked_in": 1602612590 } ]` + + // AddWorkerResp represents a JSON return for adding a worker. + AddWorkerResp = `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3b3JrZXIiLCJpYXQiOjE1MTYyMzkwMjIsInRva2VuX3R5cGUiOiJXb3JrZXJBdXRoIn0.qeULIimCJlrwsE0JykNpzBmMaHUbvfk0vkyAz2eEo38" + }` + + // RefreshWorkerAuthResp represents a JSON return for refreshing a worker's authentication. + RefreshWorkerAuthResp = `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3b3JrZXIiLCJpYXQiOjE1MTYyMzkwMjIsInRva2VuX3R5cGUiOiJXb3JrZXJBdXRoIn0.qeULIimCJlrwsE0JykNpzBmMaHUbvfk0vkyAz2eEo38" + }` + + // RegisterTokenResp represents a JSON return for an admin requesting a registration token. + // + //nolint:gosec // not actual credentials + RegisterTokenResp = `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3b3JrZXIiLCJpYXQiOjE1MTYyMzkwMjIsInRva2VuX3R5cGUiOiJXb3JrZXJSZWdpc3RlciJ9.gEzKaZB-sDd_gFCVF5uGo2mcf3iy9CrXDTLPZ6PTsTc" + }` ) // getWorkers returns mock JSON for a http GET. @@ -92,9 +109,9 @@ func getWorker(c *gin.Context) { // addWorker returns mock JSON for a http POST. func addWorker(c *gin.Context) { - data := []byte(WorkerResp) + data := []byte(AddWorkerResp) - var body library.Worker + var body library.Token _ = json.Unmarshal(data, &body) c.JSON(http.StatusCreated, body) @@ -122,6 +139,28 @@ func updateWorker(c *gin.Context) { c.JSON(http.StatusOK, body) } +// refreshWorkerAuth has a param :worker returns mock JSON for a http PUT. +// +// Pass "0" to :worker to test receiving a http 404 response. +func refreshWorkerAuth(c *gin.Context) { + w := c.Param("worker") + + if strings.EqualFold(w, "0") { + msg := fmt.Sprintf("Worker %s does not exist", w) + + c.AbortWithStatusJSON(http.StatusNotFound, types.Error{Message: &msg}) + + return + } + + data := []byte(RefreshWorkerAuthResp) + + var body library.Token + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusOK, body) +} + // removeWorker has a param :worker returns mock JSON for a http DELETE. // // Pass "0" to :worker to test receiving a http 404 response. @@ -138,3 +177,25 @@ func removeWorker(c *gin.Context) { c.JSON(http.StatusOK, fmt.Sprintf("Worker %s removed", w)) } + +// registerToken has a param :worker returns mock JSON for a http POST. +// +// Pass "0" to :worker to test receiving a http 401 response. +func registerToken(c *gin.Context) { + w := c.Param("worker") + + if strings.EqualFold(w, "0") { + msg := fmt.Sprintf("user %s is not a platform admin", w) + + c.AbortWithStatusJSON(http.StatusUnauthorized, types.Error{Message: &msg}) + + return + } + + data := []byte(RegisterTokenResp) + + var body library.Token + _ = json.Unmarshal(data, &body) + + c.JSON(http.StatusCreated, body) +} diff --git a/queue/context.go b/queue/context.go index ea9c2d0fe..e5bf48c78 100644 --- a/queue/context.go +++ b/queue/context.go @@ -56,7 +56,7 @@ func WithContext(c context.Context, s Service) context.Context { // // https://pkg.go.dev/context?tab=doc#WithValue // - // nolint: golint,staticcheck // ignore using string with context value + //nolint:staticcheck,revive // ignore using string with context value return context.WithValue(c, key, s) } diff --git a/queue/context_test.go b/queue/context_test.go index 4a6ba4c90..92218a1f4 100644 --- a/queue/context_test.go +++ b/queue/context_test.go @@ -22,7 +22,7 @@ func TestExecutor_FromContext(t *testing.T) { want Service }{ { - // nolint: golint,staticcheck // ignore using string with context value + //nolint:staticcheck,revive // ignore using string with context value context: context.WithValue(context.Background(), key, _service), want: _service, }, @@ -31,7 +31,7 @@ func TestExecutor_FromContext(t *testing.T) { want: nil, }, { - // nolint: golint,staticcheck // ignore using string with context value + //nolint:staticcheck,revive // ignore using string with context value context: context.WithValue(context.Background(), key, "foo"), want: nil, }, @@ -92,7 +92,7 @@ func TestExecutor_WithContext(t *testing.T) { // setup types _service, _ := New(&Setup{}) - // nolint: golint,staticcheck // ignore using string with context value + //nolint:staticcheck,revive // ignore using string with context value want := context.WithValue(context.Background(), key, _service) // run test diff --git a/queue/doc.go b/queue/doc.go index ca03c8c84..5c08b1b0d 100644 --- a/queue/doc.go +++ b/queue/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/queue" +// import "github.com/go-vela/server/queue" package queue diff --git a/queue/flags.go b/queue/flags.go index 0fd1e23a6..b5ede4733 100644 --- a/queue/flags.go +++ b/queue/flags.go @@ -50,4 +50,16 @@ var Flags = []cli.Flag{ Usage: "timeout for requests that pop items off the queue", Value: 60 * time.Second, }, + &cli.StringFlag{ + EnvVars: []string{"QUEUE_PRIVATE_KEY"}, + FilePath: "/vela/signing.key", + Name: "queue.private-key", + Usage: "set value of base64 encoded queue signing private key", + }, + &cli.StringFlag{ + EnvVars: []string{"QUEUE_PUBLIC_KEY"}, + FilePath: "/vela/signing.pub", + Name: "queue.public-key", + Usage: "set value of base64 encoded queue signing public key", + }, } diff --git a/queue/queue.go b/queue/queue.go index f665c74ba..fca90b8a2 100644 --- a/queue/queue.go +++ b/queue/queue.go @@ -11,13 +11,12 @@ import ( "github.com/sirupsen/logrus" ) -// nolint: godot // ignore period at end for comment ending in a list -// // New creates and returns a Vela service capable of // integrating with the configured queue environment. // Currently, the following queues are supported: // // * redis +// . func New(s *Setup) (Service, error) { // validate the setup being provided // diff --git a/queue/queue_test.go b/queue/queue_test.go index 8e690920c..61bcfd527 100644 --- a/queue/queue_test.go +++ b/queue/queue_test.go @@ -13,7 +13,6 @@ import ( func TestQueue_New(t *testing.T) { // setup types - // create a local fake redis instance // // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run diff --git a/queue/redis/doc.go b/queue/redis/doc.go index ad6afe54c..d75e65027 100644 --- a/queue/redis/doc.go +++ b/queue/redis/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/queue/redis" +// import "github.com/go-vela/server/queue/redis" package redis diff --git a/queue/redis/driver_test.go b/queue/redis/driver_test.go index 85582775c..2e5d4bd96 100644 --- a/queue/redis/driver_test.go +++ b/queue/redis/driver_test.go @@ -16,7 +16,6 @@ import ( func TestRedis_Driver(t *testing.T) { // setup types - // create a local fake redis instance // // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run diff --git a/queue/redis/length.go b/queue/redis/length.go new file mode 100644 index 000000000..7ed4ecd8b --- /dev/null +++ b/queue/redis/length.go @@ -0,0 +1,27 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package redis + +import ( + "context" +) + +// Length tallies all items present in the configured channels in the queue. +func (c *client) Length(ctx context.Context) (int64, error) { + c.Logger.Tracef("reading length of all configured channels in queue") + + total := int64(0) + + for _, channel := range c.config.Channels { + items, err := c.Redis.LLen(ctx, channel).Result() + if err != nil { + return 0, err + } + + total += items + } + + return total, nil +} diff --git a/queue/redis/length_test.go b/queue/redis/length_test.go new file mode 100644 index 000000000..d9622594c --- /dev/null +++ b/queue/redis/length_test.go @@ -0,0 +1,74 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package redis + +import ( + "context" + "reflect" + "testing" + + "github.com/go-vela/types" + "gopkg.in/square/go-jose.v2/json" +) + +func TestRedis_Length(t *testing.T) { + // setup types + // use global variables in redis_test.go + _item := &types.Item{ + Build: _build, + Repo: _repo, + User: _user, + } + + // setup queue item + bytes, err := json.Marshal(_item) + if err != nil { + t.Errorf("unable to marshal queue item: %v", err) + } + + // setup redis mock + _redis, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela", "vela:second", "vela:third") + if err != nil { + t.Errorf("unable to create queue service: %v", err) + } + + // setup tests + tests := []struct { + channels []string + want int64 + }{ + { + channels: []string{"vela"}, + want: 1, + }, + { + channels: []string{"vela", "vela:second", "vela:third"}, + want: 4, + }, + { + channels: []string{"vela", "vela:second", "phony"}, + want: 6, + }, + } + + // run tests + for _, test := range tests { + for _, channel := range test.channels { + err := _redis.Push(context.Background(), channel, bytes) + if err != nil { + t.Errorf("unable to push item to queue: %v", err) + } + } + got, err := _redis.Length(context.Background()) + + if err != nil { + t.Errorf("Length returned err: %v", err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("Length is %v, want %v", got, test.want) + } + } +} diff --git a/queue/redis/opts.go b/queue/redis/opts.go index aabcfea05..5589eeb92 100644 --- a/queue/redis/opts.go +++ b/queue/redis/opts.go @@ -5,6 +5,8 @@ package redis import ( + "encoding/base64" + "errors" "fmt" "time" ) @@ -69,3 +71,75 @@ func WithTimeout(timeout time.Duration) ClientOpt { return nil } } + +// WithPrivateKey sets the private key in the queue client for Redis. +// +//nolint:dupl // ignore similar code +func WithPrivateKey(key string) ClientOpt { + return func(c *client) error { + c.Logger.Trace("configuring private key in redis queue client") + + if len(key) == 0 { + c.Logger.Warn("unable to base64 decode private key, provided key is empty. queue service will be unable to sign items") + return nil + } + + decoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return err + } + + if len(decoded) == 0 { + return errors.New("unable to base64 decode private key, decoded key is empty") + } + + c.config.PrivateKey = new([64]byte) + copy(c.config.PrivateKey[:], decoded) + + if c.config.PrivateKey == nil { + return errors.New("unable to copy decoded queue signing private key, copied key is nil") + } + + if len(c.config.PrivateKey) == 0 { + return errors.New("unable to copy decoded queue signing private key, copied key is empty") + } + + return nil + } +} + +// WithPublicKey sets the public key in the queue client for Redis. +// +//nolint:dupl // ignore similar code +func WithPublicKey(key string) ClientOpt { + return func(c *client) error { + c.Logger.Tracef("configuring public key in redis queue client") + + if len(key) == 0 { + c.Logger.Warn("unable to base64 decode public key, provided key is empty. queue service will be unable to open items") + return nil + } + + decoded, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return err + } + + if len(decoded) == 0 { + return errors.New("unable to base64 decode public key, decoded key is empty") + } + + c.config.PublicKey = new([32]byte) + copy(c.config.PublicKey[:], decoded) + + if c.config.PublicKey == nil { + return errors.New("unable to copy decoded queue signing public key, copied key is nil") + } + + if len(c.config.PublicKey) == 0 { + return errors.New("unable to copy decoded queue signing public key, copied key is empty") + } + + return nil + } +} diff --git a/queue/redis/opts_test.go b/queue/redis/opts_test.go index 72c91c5eb..3f99e3857 100644 --- a/queue/redis/opts_test.go +++ b/queue/redis/opts_test.go @@ -5,6 +5,7 @@ package redis import ( + "encoding/base64" "fmt" "reflect" "testing" @@ -15,7 +16,6 @@ import ( func TestRedis_ClientOpt_WithAddress(t *testing.T) { // setup tests - // create a local fake redis instance // // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run @@ -68,7 +68,6 @@ func TestRedis_ClientOpt_WithAddress(t *testing.T) { func TestRedis_ClientOpt_WithChannels(t *testing.T) { // setup tests - // create a local fake redis instance // // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run @@ -122,7 +121,6 @@ func TestRedis_ClientOpt_WithChannels(t *testing.T) { func TestRedis_ClientOpt_WithCluster(t *testing.T) { // setup tests - // create a local fake redis instance // // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run @@ -183,3 +181,139 @@ func TestRedis_ClientOpt_WithCluster(t *testing.T) { } } } + +func TestRedis_ClientOpt_WithSigningPrivateKey(t *testing.T) { + // setup tests + // create a local fake redis instance + // + // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run + _redis, err := miniredis.Run() + if err != nil { + t.Errorf("unable to create miniredis instance: %v", err) + } + defer _redis.Close() + + tests := []struct { + failure bool + privKey string + want string + }{ + { //valid key input + failure: false, + privKey: "tCIevHOBq6DdN5SSBtteXUusjjd0fOqzk2eyi0DMq04NewmShNKQeUbbp3vkvIckb4pCxc+vxUo+mYf/vzOaSg==", + want: "tCIevHOBq6DdN5SSBtteXUusjjd0fOqzk2eyi0DMq04NewmShNKQeUbbp3vkvIckb4pCxc+vxUo+mYf/vzOaSg==", + }, + { //empty key input + failure: false, + privKey: "", + want: "", + }, + { //invalid base64 encoded input + failure: true, + privKey: "abc123", + want: "", + }, + } + + // run tests + for _, test := range tests { + _service, err := New( + WithAddress(fmt.Sprintf("redis://%s", _redis.Addr())), + WithPrivateKey(test.privKey), + ) + + if test.failure { + if err == nil { + t.Errorf("WithPrivateKey should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("WithPrivateKey returned err: %v", err) + } + + got := "" + if _service.config.PrivateKey != nil { + got = fmt.Sprintf("%s", *_service.config.PrivateKey) + } else { + got = "" + } + + w, _ := base64.StdEncoding.DecodeString(test.want) + + want := string(w) + if !reflect.DeepEqual(got, want) { + t.Errorf("WithPrivateKey is %v, want %v", got, want) + } + } +} + +func TestRedis_ClientOpt_WithSigningPublicKey(t *testing.T) { + // setup tests + // create a local fake redis instance + // + // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run + _redis, err := miniredis.Run() + if err != nil { + t.Errorf("unable to create miniredis instance: %v", err) + } + defer _redis.Close() + + tests := []struct { + failure bool + pubKey string + want string + }{ + { //valid key input + failure: false, + pubKey: "DXsJkoTSkHlG26d75LyHJG+KQsXPr8VKPpmH/78zmko=", + want: "DXsJkoTSkHlG26d75LyHJG+KQsXPr8VKPpmH/78zmko=", + }, + { //empty key input + failure: false, + pubKey: "", + want: "", + }, + { //invalid base64 encoded input + failure: true, + pubKey: "abc123", + want: "", + }, + } + + // run tests + for _, test := range tests { + _service, err := New( + WithAddress(fmt.Sprintf("redis://%s", _redis.Addr())), + WithPublicKey(test.pubKey), + ) + + if test.failure { + if err == nil { + t.Errorf("WithPublicKey should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("WithPublicKey returned err: %v", err) + } + + got := "" + if _service.config.PublicKey != nil { + got = fmt.Sprintf("%s", *_service.config.PublicKey) + } else { + got = "" + } + + w, _ := base64.StdEncoding.DecodeString(test.want) + + want := string(w) + if !reflect.DeepEqual(got, want) { + t.Errorf("SigningPublicKey is %v, want %v", got, want) + } + } +} diff --git a/queue/redis/pop.go b/queue/redis/pop.go index e55b3e253..732092c32 100644 --- a/queue/redis/pop.go +++ b/queue/redis/pop.go @@ -7,9 +7,11 @@ package redis import ( "context" "encoding/json" + "errors" - "github.com/go-redis/redis/v8" "github.com/go-vela/types" + "github.com/redis/go-redis/v9" + "golang.org/x/crypto/nacl/sign" ) // Pop grabs an item from the specified channel off the queue. @@ -26,18 +28,36 @@ func (c *client) Pop(ctx context.Context) (*types.Item, error) { // https://pkg.go.dev/github.com/go-redis/redis?tab=doc#StringSliceCmd.Result result, err := popCmd.Result() if err != nil { - switch err { - case redis.Nil: // BLPOP timeout + // BLPOP timeout + if errors.Is(err, redis.Nil) { return nil, nil - default: - return nil, err } + + return nil, err } - item := new(types.Item) + // this should already be validated on startup + if c.config.PublicKey == nil || len(*c.config.PublicKey) != 32 { + return nil, errors.New("no valid signing public key provided") + } + + // extract signed item from pop results + signed := []byte(result[1]) + + var opened, out []byte + + // open the item using the public key generated using sign + // + // https://pkg.go.dev/golang.org/x/crypto@v0.1.0/nacl/sign + opened, ok := sign.Open(out, signed, c.config.PublicKey) + if !ok { + return nil, errors.New("unable to open signed item") + } // unmarshal result into queue item - err = json.Unmarshal([]byte(result[1]), item) + item := new(types.Item) + + err = json.Unmarshal(opened, item) if err != nil { return nil, err } diff --git a/queue/redis/pop_test.go b/queue/redis/pop_test.go index cacda6557..1f19bf2c3 100644 --- a/queue/redis/pop_test.go +++ b/queue/redis/pop_test.go @@ -11,20 +11,22 @@ import ( "time" "github.com/go-vela/types" + "golang.org/x/crypto/nacl/sign" "gopkg.in/square/go-jose.v2/json" ) func TestRedis_Pop(t *testing.T) { // setup types - // use global variables in redis_test.go _item := &types.Item{ - Build: _build, - Pipeline: _steps, - Repo: _repo, - User: _user, + Build: _build, + Repo: _repo, + User: _user, } + var signed []byte + var out []byte + // setup queue item bytes, err := json.Marshal(_item) if err != nil { @@ -32,19 +34,21 @@ func TestRedis_Pop(t *testing.T) { } // setup redis mock - _redis, err := NewTest("vela") + _redis, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela") if err != nil { t.Errorf("unable to create queue service: %v", err) } + signed = sign.Sign(out, bytes, _redis.config.PrivateKey) + // push item to queue - err = _redis.Redis.RPush(context.Background(), "vela", bytes).Err() + err = _redis.Redis.RPush(context.Background(), "vela", signed).Err() if err != nil { t.Errorf("unable to push item to queue: %v", err) } // setup timeout redis mock - timeout, err := NewTest("vela") + timeout, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela") if err != nil { t.Errorf("unable to create queue service: %v", err) } @@ -52,27 +56,17 @@ func TestRedis_Pop(t *testing.T) { timeout.config.Timeout = 1 * time.Second // setup badChannel redis mock - badChannel, err := NewTest("vela") + badChannel, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela") if err != nil { t.Errorf("unable to create queue service: %v", err) } // overwrite channel to be invalid badChannel.config.Channels = nil - // push nothing to queue - err = badChannel.Redis.RPush(context.Background(), "vela", nil).Err() - if err != nil { - t.Errorf("unable to push item to queue: %v", err) - } - - // setup badItem redis mock - badItem, err := NewTest("vela") - if err != nil { - t.Errorf("unable to create queue service: %v", err) - } + signed = sign.Sign(out, bytes, badChannel.config.PrivateKey) - // push nothing to queue - err = badItem.Redis.RPush(context.Background(), "vela", nil).Err() + // push something to badChannel queue + err = badChannel.Redis.RPush(context.Background(), "vela", signed).Err() if err != nil { t.Errorf("unable to push item to queue: %v", err) } @@ -98,11 +92,6 @@ func TestRedis_Pop(t *testing.T) { redis: badChannel, want: nil, }, - { - failure: true, - redis: badItem, - want: nil, - }, } // run tests diff --git a/queue/redis/push.go b/queue/redis/push.go index 620e1174a..66c8a386a 100644 --- a/queue/redis/push.go +++ b/queue/redis/push.go @@ -6,16 +6,43 @@ package redis import ( "context" + "errors" + + "golang.org/x/crypto/nacl/sign" ) // Push inserts an item to the specified channel in the queue. func (c *client) Push(ctx context.Context, channel string, item []byte) error { c.Logger.Tracef("pushing item to queue %s", channel) + // ensure the item to be pushed is valid + // go-redis RPush does not support nil as of v9.0.2 + // + // https://github.com/redis/go-redis/pull/1960 + if item == nil { + return errors.New("item is nil") + } + + var signed []byte + + var out []byte + + // this should already be validated on startup + if c.config.PrivateKey == nil || len(*c.config.PrivateKey) != 64 { + return errors.New("no valid signing private key provided") + } + + c.Logger.Tracef("signing item for queue %s", channel) + + // sign the item using the private key generated using sign + // + // https://pkg.go.dev/golang.org/x/crypto@v0.1.0/nacl/sign + signed = sign.Sign(out, item, c.config.PrivateKey) + // build a redis queue command to push an item to queue // // https://pkg.go.dev/github.com/go-redis/redis?tab=doc#Client.RPush - pushCmd := c.Redis.RPush(ctx, channel, item) + pushCmd := c.Redis.RPush(ctx, channel, signed) // blocking call to push an item to queue and return err // diff --git a/queue/redis/push_test.go b/queue/redis/push_test.go index 97ed47a7a..e61792ea9 100644 --- a/queue/redis/push_test.go +++ b/queue/redis/push_test.go @@ -14,23 +14,27 @@ import ( func TestRedis_Push(t *testing.T) { // setup types - // use global variables in redis_test.go _item := &types.Item{ - Build: _build, - Pipeline: _steps, - Repo: _repo, - User: _user, + Build: _build, + Repo: _repo, + User: _user, } // setup queue item - bytes, err := json.Marshal(_item) + _bytes, err := json.Marshal(_item) if err != nil { t.Errorf("unable to marshal queue item: %v", err) } // setup redis mock - _redis, err := NewTest("vela") + _redis, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela") + if err != nil { + t.Errorf("unable to create queue service: %v", err) + } + + // setup redis mock + badItem, err := NewTest(_signingPrivateKey, _signingPublicKey, "vela") if err != nil { t.Errorf("unable to create queue service: %v", err) } @@ -39,16 +43,23 @@ func TestRedis_Push(t *testing.T) { tests := []struct { failure bool redis *client + bytes []byte }{ { failure: false, redis: _redis, + bytes: _bytes, + }, + { + failure: true, + redis: badItem, + bytes: nil, }, } // run tests for _, test := range tests { - err := _redis.Push(context.Background(), "vela", bytes) + err := test.redis.Push(context.Background(), "vela", test.bytes) if test.failure { if err == nil { diff --git a/queue/redis/redis.go b/queue/redis/redis.go index 1d2c1467b..4e4f68aea 100644 --- a/queue/redis/redis.go +++ b/queue/redis/redis.go @@ -12,7 +12,7 @@ import ( "time" "github.com/alicebob/miniredis/v2" - "github.com/go-redis/redis/v8" + "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" ) @@ -25,6 +25,10 @@ type config struct { Cluster bool // specifies the timeout to use for the Redis client Timeout time.Duration + // key for signing items pushed to the Redis client + PrivateKey *[64]byte + // key for opening items popped from the Redis client + PublicKey *[32]byte } type client struct { @@ -38,7 +42,7 @@ type client struct { // New returns a Queue implementation that // integrates with a Redis queue instance. // -// nolint: revive // ignore returning unexported client +//nolint:revive // ignore returning unexported client func New(opts ...ClientOpt) (*client, error) { // create new Redis client c := new(client) @@ -97,22 +101,22 @@ func New(opts ...ClientOpt) (*client, error) { // the failover options from the parse options. func failoverFromOptions(source *redis.Options) *redis.FailoverOptions { target := &redis.FailoverOptions{ - OnConnect: source.OnConnect, - Password: source.Password, - DB: source.DB, - MaxRetries: source.MaxRetries, - MinRetryBackoff: source.MinRetryBackoff, - MaxRetryBackoff: source.MaxRetryBackoff, - DialTimeout: source.DialTimeout, - ReadTimeout: source.ReadTimeout, - WriteTimeout: source.WriteTimeout, - PoolSize: source.PoolSize, - MinIdleConns: source.MinIdleConns, - MaxConnAge: source.MaxConnAge, - PoolTimeout: source.PoolTimeout, - IdleTimeout: source.IdleTimeout, - IdleCheckFrequency: source.IdleCheckFrequency, - TLSConfig: source.TLSConfig, + OnConnect: source.OnConnect, + Password: source.Password, + DB: source.DB, + MaxRetries: source.MaxRetries, + MinRetryBackoff: source.MinRetryBackoff, + MaxRetryBackoff: source.MaxRetryBackoff, + DialTimeout: source.DialTimeout, + ReadTimeout: source.ReadTimeout, + WriteTimeout: source.WriteTimeout, + PoolSize: source.PoolSize, + MinIdleConns: source.MinIdleConns, + MaxIdleConns: source.MaxIdleConns, + ConnMaxLifetime: source.ConnMaxLifetime, + PoolTimeout: source.PoolTimeout, + ConnMaxIdleTime: source.ConnMaxIdleTime, + TLSConfig: source.TLSConfig, } // trim auto appended :6379 from address @@ -172,8 +176,8 @@ func pingQueue(c *client) error { // // This function is intended for running tests only. // -// nolint: revive // ignore returning unexported client -func NewTest(channels ...string) (*client, error) { +//nolint:revive // ignore returning unexported client +func NewTest(signingPrivateKey, signingPublicKey string, channels ...string) (*client, error) { // create a local fake redis instance // // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run @@ -186,5 +190,7 @@ func NewTest(channels ...string) (*client, error) { WithAddress(fmt.Sprintf("redis://%s", _redis.Addr())), WithChannels(channels...), WithCluster(false), + WithPrivateKey(signingPrivateKey), + WithPublicKey(signingPublicKey), ) } diff --git a/queue/redis/redis_test.go b/queue/redis/redis_test.go index 1e607b856..6935a676d 100644 --- a/queue/redis/redis_test.go +++ b/queue/redis/redis_test.go @@ -15,7 +15,7 @@ import ( ) // The following functions were taken from -// https://github.com/go-vela/sdk-go/blob/master/vela/go +// https://github.com/go-vela/sdk-go/blob/main/vela/go // which is the only reason go-vela/sdk-go is // a dependency for go-vela/server // TODO: consider moving to go-vela/types? @@ -46,7 +46,9 @@ func Strings(v []string) *[]string { return &v } // setup global variables used for testing. var ( - _build = &library.Build{ + _signingPrivateKey = "tCIevHOBq6DdN5SSBtteXUusjjd0fOqzk2eyi0DMq04NewmShNKQeUbbp3vkvIckb4pCxc+vxUo+mYf/vzOaSg==" + _signingPublicKey = "DXsJkoTSkHlG26d75LyHJG+KQsXPr8VKPpmH/78zmko=" + _build = &library.Build{ ID: Int64(1), Number: Int(1), Parent: Int(1), @@ -121,7 +123,7 @@ var ( ID: "step_github_octocat_1_clone", Directory: "/home/github/octocat", Environment: map[string]string{"FOO": "bar"}, - Image: "target/vela-git:v0.3.0", + Image: "target/vela-git:v0.5.1", Name: "clone", Number: 2, Pull: "always", @@ -151,7 +153,6 @@ var ( func TestRedis_New(t *testing.T) { // setup types - // create a local fake redis instance // // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run diff --git a/queue/redis/route.go b/queue/redis/route.go index fa1a3b4f3..1da4b19db 100644 --- a/queue/redis/route.go +++ b/queue/redis/route.go @@ -22,7 +22,7 @@ func (c *client) Route(w *pipeline.Worker) (string, error) { // if pipline does not specify route information return default // - // https://github.com/go-vela/types/blob/master/constants/queue.go#L10 + // https://github.com/go-vela/types/blob/main/constants/queue.go#L10 if w.Empty() { return constants.DefaultRoute, nil } @@ -37,5 +37,13 @@ func (c *client) Route(w *pipeline.Worker) (string, error) { buf.WriteString(fmt.Sprintf(":%s", w.Platform)) } - return strings.TrimLeft(buf.String(), ":"), nil + route := strings.TrimLeft(buf.String(), ":") + + for _, r := range c.config.Channels { + if strings.EqualFold(route, r) { + return route, nil + } + } + + return "", fmt.Errorf("invalid route %s provided", route) } diff --git a/queue/redis/route_test.go b/queue/redis/route_test.go index 9956ca02d..23328df43 100644 --- a/queue/redis/route_test.go +++ b/queue/redis/route_test.go @@ -14,32 +14,48 @@ import ( func TestRedis_Client_Route(t *testing.T) { // setup - client, _ := NewTest("vela") + client, _ := NewTest(_signingPrivateKey, _signingPublicKey, "vela", "16cpu8gb", "16cpu8gb:gcp", "gcp") tests := []struct { - want string - worker pipeline.Worker + success bool + want string + worker pipeline.Worker }{ // pipeline with not worker passed { - want: constants.DefaultRoute, - worker: pipeline.Worker{}, + success: true, + want: constants.DefaultRoute, + worker: pipeline.Worker{}, }, { - want: "vela", - worker: pipeline.Worker{}, + success: true, + want: "vela", + worker: pipeline.Worker{}, }, { - want: "16cpu8gb", - worker: pipeline.Worker{Flavor: "16cpu8gb"}, + success: true, + want: "16cpu8gb", + worker: pipeline.Worker{Flavor: "16cpu8gb"}, }, { - want: "16cpu8gb:gcp", - worker: pipeline.Worker{Flavor: "16cpu8gb", Platform: "gcp"}, + success: true, + want: "16cpu8gb:gcp", + worker: pipeline.Worker{Flavor: "16cpu8gb", Platform: "gcp"}, }, { - want: "gcp", - worker: pipeline.Worker{Platform: "gcp"}, + success: true, + want: "gcp", + worker: pipeline.Worker{Platform: "gcp"}, + }, + { + success: false, + want: "", + worker: pipeline.Worker{Flavor: "bad", Platform: "route"}, + }, + { + success: false, + want: "", + worker: pipeline.Worker{Flavor: "bad"}, }, } @@ -47,10 +63,14 @@ func TestRedis_Client_Route(t *testing.T) { for _, test := range tests { got, err := client.Route(&test.worker) - if err != nil { + if test.success && err != nil { t.Errorf("Route returned err: %v", err) } + if !test.success && err == nil { + t.Errorf("Route returned %s, want err", got) + } + if !strings.EqualFold(got, test.want) { t.Errorf("Route is %v, want %v", got, test.want) } diff --git a/queue/service.go b/queue/service.go index 3974b76a3..418b3a919 100644 --- a/queue/service.go +++ b/queue/service.go @@ -20,6 +20,10 @@ type Service interface { // the configured queue driver. Driver() string + // Length defines a function that outputs + // the length of a queue channel + Length(context.Context) (int64, error) + // Pop defines a function that grabs an // item off the queue. Pop(context.Context) (*types.Item, error) diff --git a/queue/setup.go b/queue/setup.go index ae69a1429..ed0f131c8 100644 --- a/queue/setup.go +++ b/queue/setup.go @@ -30,6 +30,10 @@ type Setup struct { Routes []string // specifies the timeout for pop requests for the queue client Timeout time.Duration + // private key in base64 used for signing items pushed to the queue + PrivateKey string + // public key in base64 used for opening items popped from the queue + PublicKey string } // Redis creates and returns a Vela service capable @@ -45,6 +49,8 @@ func (s *Setup) Redis() (Service, error) { redis.WithChannels(s.Routes...), redis.WithCluster(s.Cluster), redis.WithTimeout(s.Timeout), + redis.WithPrivateKey(s.PrivateKey), + redis.WithPublicKey(s.PublicKey), ) } diff --git a/queue/setup_test.go b/queue/setup_test.go index 317753b91..4b2e35ede 100644 --- a/queue/setup_test.go +++ b/queue/setup_test.go @@ -13,7 +13,6 @@ import ( func TestQueue_Setup_Redis(t *testing.T) { // setup types - // create a local fake redis instance // // https://pkg.go.dev/github.com/alicebob/miniredis/v2#Run diff --git a/random/random.go b/random/random.go index 96f324cd3..0a5953ed0 100644 --- a/random/random.go +++ b/random/random.go @@ -1,3 +1,7 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + package random import ( diff --git a/router/admin.go b/router/admin.go index f8c259795..96764617f 100644 --- a/router/admin.go +++ b/router/admin.go @@ -13,58 +13,52 @@ import ( // AdminHandlers is a function that extends the provided base router group // with the API handlers for admin functionality. // -// GET /api/v1/admin/builds // GET /api/v1/admin/builds/queue +// GET /api/v1/admin/build/:id // PUT /api/v1/admin/build -// GET /api/v1/admin/deployments +// PUT /api/v1/admin/clean // PUT /api/v1/admin/deployment -// GET /api/v1/admin/hooks // PUT /api/v1/admin/hook -// GET /api/v1/admin/repos // PUT /api/v1/admin/repo -// GET /api/v1/admin/secrets // PUT /api/v1/admin/secret -// GET /api/v1/admin/services // PUT /api/v1/admin/service -// GET /api/v1/admin/steps // PUT /api/v1/admin/step -// GET /api/v1/admin/users // PUT /api/v1/admin/user. func AdminHandlers(base *gin.RouterGroup) { // Admin endpoints _admin := base.Group("/admin", perm.MustPlatformAdmin()) { - // Admin build endpoints - _admin.GET("/builds", admin.AllBuilds) + // Admin build queue endpoint _admin.GET("/builds/queue", admin.AllBuildsQueue) + + // Admin build endpoint _admin.PUT("/build", admin.UpdateBuild) - // Admin deployment endpoints - _admin.GET("/deployments", admin.AllDeployments) + // Admin clean endpoint + _admin.PUT("/clean", admin.CleanResources) + + // Admin deployment endpoint _admin.PUT("/deployment", admin.UpdateDeployment) - // Admin hook endpoints - _admin.GET("/hooks", admin.AllHooks) + // Admin hook endpoint _admin.PUT("/hook", admin.UpdateHook) - // Admin repo endpoints - _admin.GET("/repos", admin.AllRepos) + // Admin repo endpoint _admin.PUT("/repo", admin.UpdateRepo) - // Admin secret endpoints - _admin.GET("/secrets", admin.AllSecrets) + // Admin secret endpoint _admin.PUT("/secret", admin.UpdateSecret) - // Admin service endpoints - _admin.GET("/services", admin.AllServices) + // Admin service endpoint _admin.PUT("/service", admin.UpdateService) - // Admin step endpoints - _admin.GET("/steps", admin.AllSteps) + // Admin step endpoint _admin.PUT("/step", admin.UpdateStep) - // Admin user endpoints - _admin.GET("/users", admin.AllUsers) + // Admin user endpoint _admin.PUT("/user", admin.UpdateUser) + + // Admin worker endpoint + _admin.POST("/workers/:worker/register-token", admin.RegisterToken) } // end of admin endpoints } diff --git a/router/build.go b/router/build.go index 153f61ad5..c8cc0d688 100644 --- a/router/build.go +++ b/router/build.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -6,9 +6,10 @@ package router import ( "github.com/gin-gonic/gin" - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/build" + "github.com/go-vela/server/api/log" "github.com/go-vela/server/router/middleware" - "github.com/go-vela/server/router/middleware/build" + bmiddleware "github.com/go-vela/server/router/middleware/build" "github.com/go-vela/server/router/middleware/executors" "github.com/go-vela/server/router/middleware/perm" ) @@ -24,6 +25,8 @@ import ( // DELETE /api/v1/repos/:org/:repo/builds/:build // DELETE /api/v1/repos/:org/:repo/builds/:build/cancel // GET /api/v1/repos/:org/:repo/builds/:build/logs +// GET /api/v1/repos/:org/:repo/builds/:build/token +// GET /api/v1/repos/:org/:repo/builds/:build/executable // POST /api/v1/repos/:org/:repo/builds/:build/services // GET /api/v1/repos/:org/:repo/builds/:build/services // GET /api/v1/repos/:org/:repo/builds/:build/services/:service @@ -46,26 +49,28 @@ func BuildHandlers(base *gin.RouterGroup) { // Builds endpoints builds := base.Group("/builds") { - builds.POST("", perm.MustAdmin(), middleware.Payload(), api.CreateBuild) - builds.GET("", perm.MustRead(), api.GetBuilds) + builds.POST("", perm.MustAdmin(), middleware.Payload(), build.CreateBuild) + builds.GET("", perm.MustRead(), build.ListBuildsForRepo) // Build endpoints - build := builds.Group("/:build", build.Establish()) + b := builds.Group("/:build", bmiddleware.Establish()) { - build.POST("", perm.MustWrite(), api.RestartBuild) - build.GET("", perm.MustRead(), api.GetBuild) - build.PUT("", perm.MustWrite(), middleware.Payload(), api.UpdateBuild) - build.DELETE("", perm.MustPlatformAdmin(), api.DeleteBuild) - build.DELETE("/cancel", executors.Establish(), perm.MustWrite(), api.CancelBuild) - build.GET("/logs", perm.MustRead(), api.GetBuildLogs) + b.POST("", perm.MustWrite(), build.RestartBuild) + b.GET("", perm.MustRead(), build.GetBuild) + b.PUT("", perm.MustBuildAccess(), middleware.Payload(), build.UpdateBuild) + b.DELETE("", perm.MustPlatformAdmin(), build.DeleteBuild) + b.DELETE("/cancel", executors.Establish(), perm.MustWrite(), build.CancelBuild) + b.GET("/logs", perm.MustRead(), log.ListLogsForBuild) + b.GET("/token", perm.MustWorkerAuthToken(), build.GetBuildToken) + b.GET("/executable", perm.MustBuildAccess(), build.GetBuildExecutable) // Service endpoints // * Log endpoints - ServiceHandlers(build) + ServiceHandlers(b) // Step endpoints // * Log endpoints - StepHandlers(build) + StepHandlers(b) } // end of build endpoints } // end of builds endpoints } diff --git a/router/deployment.go b/router/deployment.go index 9a5b15e27..eabbc48c6 100644 --- a/router/deployment.go +++ b/router/deployment.go @@ -8,7 +8,7 @@ import ( "github.com/gin-gonic/gin" "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/deployment" "github.com/go-vela/server/router/middleware/perm" "github.com/go-vela/server/router/middleware/repo" ) @@ -23,8 +23,8 @@ func DeploymentHandlers(base *gin.RouterGroup) { // Deployments endpoints deployments := base.Group("/deployments/:org/:repo", org.Establish(), repo.Establish()) { - deployments.POST("", perm.MustWrite(), api.CreateDeployment) - deployments.GET("", perm.MustRead(), api.GetDeployments) - deployments.GET("/:deployment", perm.MustRead(), api.GetDeployment) + deployments.POST("", perm.MustWrite(), deployment.CreateDeployment) + deployments.GET("", perm.MustRead(), deployment.ListDeployments) + deployments.GET("/:deployment", perm.MustRead(), deployment.GetDeployment) } // end of deployments endpoints } diff --git a/router/doc.go b/router/doc.go index 41ce9fb80..17052a2b5 100644 --- a/router/doc.go +++ b/router/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/router" +// import "github.com/go-vela/server/router" package router diff --git a/router/hook.go b/router/hook.go index 97c7d53f3..f5e1aa1db 100644 --- a/router/hook.go +++ b/router/hook.go @@ -6,7 +6,7 @@ package router import ( "github.com/gin-gonic/gin" - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/hook" "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/perm" "github.com/go-vela/server/router/middleware/repo" @@ -19,15 +19,17 @@ import ( // GET /api/v1/hooks/:org/:repo // GET /api/v1/hooks/:org/:repo/:hook // PUT /api/v1/hooks/:org/:repo/:hook -// DELETE /api/v1/hooks/:org/:repo/:hook . +// DELETE /api/v1/hooks/:org/:repo/:hook +// POST /api/v1/hooks/:org/:repo/:hook/redeliver . func HookHandlers(base *gin.RouterGroup) { // Hooks endpoints - hooks := base.Group("/hooks/:org/:repo", org.Establish(), repo.Establish()) + _hooks := base.Group("/hooks/:org/:repo", org.Establish(), repo.Establish()) { - hooks.POST("", perm.MustPlatformAdmin(), api.CreateHook) - hooks.GET("", perm.MustRead(), api.GetHooks) - hooks.GET("/:hook", perm.MustRead(), api.GetHook) - hooks.PUT("/:hook", perm.MustPlatformAdmin(), api.UpdateHook) - hooks.DELETE("/:hook", perm.MustPlatformAdmin(), api.DeleteHook) + _hooks.POST("", perm.MustPlatformAdmin(), hook.CreateHook) + _hooks.GET("", perm.MustRead(), hook.ListHooks) + _hooks.GET("/:hook", perm.MustRead(), hook.GetHook) + _hooks.PUT("/:hook", perm.MustPlatformAdmin(), hook.UpdateHook) + _hooks.DELETE("/:hook", perm.MustPlatformAdmin(), hook.DeleteHook) + _hooks.POST("/:hook/redeliver", perm.MustWrite(), hook.RedeliverHook) } // end of hooks endpoints } diff --git a/router/log.go b/router/log.go index d7e8dc8db..71c6674c8 100644 --- a/router/log.go +++ b/router/log.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -6,7 +6,7 @@ package router import ( "github.com/gin-gonic/gin" - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/log" "github.com/go-vela/server/router/middleware/perm" ) @@ -21,10 +21,10 @@ func LogServiceHandlers(base *gin.RouterGroup) { // Logs endpoints logs := base.Group("/logs") { - logs.POST("", perm.MustAdmin(), api.CreateServiceLog) - logs.GET("", perm.MustRead(), api.GetServiceLog) - logs.PUT("", perm.MustWrite(), api.UpdateServiceLog) - logs.DELETE("", perm.MustPlatformAdmin(), api.DeleteServiceLog) + logs.POST("", perm.MustAdmin(), log.CreateServiceLog) + logs.GET("", perm.MustRead(), log.GetServiceLog) + logs.PUT("", perm.MustBuildAccess(), log.UpdateServiceLog) + logs.DELETE("", perm.MustPlatformAdmin(), log.DeleteServiceLog) } // end of logs endpoints } @@ -39,9 +39,9 @@ func LogStepHandlers(base *gin.RouterGroup) { // Logs endpoints logs := base.Group("/logs") { - logs.POST("", perm.MustAdmin(), api.CreateStepLog) - logs.GET("", perm.MustRead(), api.GetStepLog) - logs.PUT("", perm.MustWrite(), api.UpdateStepLog) - logs.DELETE("", perm.MustPlatformAdmin(), api.DeleteStepLog) + logs.POST("", perm.MustAdmin(), log.CreateStepLog) + logs.GET("", perm.MustRead(), log.GetStepLog) + logs.PUT("", perm.MustBuildAccess(), log.UpdateStepLog) + logs.DELETE("", perm.MustPlatformAdmin(), log.DeleteStepLog) } // end of logs endpoints } diff --git a/router/middleware/allowlist_schedule.go b/router/middleware/allowlist_schedule.go new file mode 100644 index 000000000..8872d0361 --- /dev/null +++ b/router/middleware/allowlist_schedule.go @@ -0,0 +1,18 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +// AllowlistSchedule is a middleware function that attaches the allowlistschedule used +// to limit which repos can utilize the schedule feature within the system. +func AllowlistSchedule(allowlistschedule []string) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("allowlistschedule", allowlistschedule) + c.Next() + } +} diff --git a/router/middleware/allowlist_schedule_test.go b/router/middleware/allowlist_schedule_test.go new file mode 100644 index 000000000..a9b03ae28 --- /dev/null +++ b/router/middleware/allowlist_schedule_test.go @@ -0,0 +1,46 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package middleware + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestMiddleware_AllowlistSchedule(t *testing.T) { + // setup types + got := []string{""} + want := []string{"foobar"} + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/health", nil) + + // setup mock server + engine.Use(AllowlistSchedule(want)) + engine.GET("/health", func(c *gin.Context) { + got = c.Value("allowlistschedule").([]string) + + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("AllowlistSchedule returned %v, want %v", resp.Code, http.StatusOK) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("AllowlistSchedule is %v, want %v", got, want) + } +} diff --git a/router/middleware/auth/auth.go b/router/middleware/auth/auth.go new file mode 100644 index 000000000..968ceabfb --- /dev/null +++ b/router/middleware/auth/auth.go @@ -0,0 +1,31 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package auth + +import ( + "fmt" + "net/http" + + "github.com/go-vela/types/constants" + + "github.com/golang-jwt/jwt/v5/request" +) + +// RetrieveAccessToken gets the passed in access token from the header in the request. +func RetrieveAccessToken(r *http.Request) (accessToken string, err error) { + return request.AuthorizationHeaderExtractor.ExtractToken(r) +} + +// RetrieveRefreshToken gets the refresh token sent along with the request as a cookie. +func RetrieveRefreshToken(r *http.Request) (string, error) { + refreshToken, err := r.Cookie(constants.RefreshTokenName) + + if refreshToken == nil || len(refreshToken.Value) == 0 { + // cookie will not be sent if it has expired + return "", fmt.Errorf("refresh token expired or not provided") + } + + return refreshToken.Value, err +} diff --git a/router/middleware/auth/auth_test.go b/router/middleware/auth/auth_test.go new file mode 100644 index 000000000..850309c04 --- /dev/null +++ b/router/middleware/auth/auth_test.go @@ -0,0 +1,85 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package auth + +import ( + "context" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/go-vela/types/constants" +) + +func TestToken_Retrieve_Refresh(t *testing.T) { + // setup types + want := "fresh" + + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", nil) + request.AddCookie(&http.Cookie{ + Name: constants.RefreshTokenName, + Value: want, + }) + + // run test + got, err := RetrieveRefreshToken(request) + if err != nil { + t.Errorf("Retrieve returned err: %v", err) + } + + if !strings.EqualFold(got, want) { + t.Errorf("Retrieve is %v, want %v", got, want) + } +} + +func TestToken_Retrieve_Access(t *testing.T) { + // setup types + want := "foobar" + + header := fmt.Sprintf("Bearer %s", want) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", nil) + request.Header.Set("Authorization", header) + + // run test + got, err := RetrieveAccessToken(request) + if err != nil { + t.Errorf("Retrieve returned err: %v", err) + } + + if !strings.EqualFold(got, want) { + t.Errorf("Retrieve is %v, want %v", got, want) + } +} + +func TestToken_Retrieve_Access_Error(t *testing.T) { + // setup types + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", nil) + + // run test + got, err := RetrieveAccessToken(request) + if err == nil { + t.Errorf("Retrieve should have returned err") + } + + if len(got) > 0 { + t.Errorf("Retrieve is %v, want \"\"", got) + } +} + +func TestToken_Retrieve_Refresh_Error(t *testing.T) { + // setup types + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", nil) + + // run test + got, err := RetrieveRefreshToken(request) + if err == nil { + t.Errorf("Retrieve should have returned err") + } + + if len(got) > 0 { + t.Errorf("Retrieve is %v, want \"\"", got) + } +} diff --git a/router/middleware/token/doc.go b/router/middleware/auth/doc.go similarity index 80% rename from router/middleware/token/doc.go rename to router/middleware/auth/doc.go index e673c8f5e..e50ae3a1a 100644 --- a/router/middleware/token/doc.go +++ b/router/middleware/auth/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/router/middleware/token" -package token +// import "github.com/go-vela/server/router/middleware/auth" +package auth diff --git a/router/middleware/build/build.go b/router/middleware/build/build.go index ff4bcd4ca..3b0b4f0d8 100644 --- a/router/middleware/build/build.go +++ b/router/middleware/build/build.go @@ -9,15 +9,13 @@ import ( "net/http" "strconv" - "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/router/middleware/user" - + "github.com/gin-gonic/gin" "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" "github.com/go-vela/server/util" "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) @@ -32,17 +30,20 @@ func Establish() gin.HandlerFunc { o := org.Retrieve(c) r := repo.Retrieve(c) u := user.Retrieve(c) + ctx := c.Request.Context() if r == nil { - retErr := fmt.Errorf("repo %s/%s not found", c.Param("org"), c.Param("repo")) + retErr := fmt.Errorf("repo %s/%s not found", util.PathParameter(c, "org"), util.PathParameter(c, "repo")) util.HandleError(c, http.StatusNotFound, retErr) + return } - bParam := c.Param("build") + bParam := util.PathParameter(c, "build") if len(bParam) == 0 { retErr := fmt.Errorf("no build parameter provided") util.HandleError(c, http.StatusBadRequest, retErr) + return } @@ -50,6 +51,7 @@ func Establish() gin.HandlerFunc { if err != nil { retErr := fmt.Errorf("invalid build parameter provided: %s", bParam) util.HandleError(c, http.StatusBadRequest, retErr) + return } @@ -63,10 +65,11 @@ func Establish() gin.HandlerFunc { "user": u.GetName(), }).Debugf("reading build %s/%d", r.GetFullName(), number) - b, err := database.FromContext(c).GetBuild(number, r) + b, err := database.FromContext(c).GetBuildForRepo(ctx, r, number) if err != nil { - retErr := fmt.Errorf("unable to read build %s/%d: %v", r.GetFullName(), number, err) + retErr := fmt.Errorf("unable to read build %s/%d: %w", r.GetFullName(), number, err) util.HandleError(c, http.StatusNotFound, retErr) + return } diff --git a/router/middleware/build/build_test.go b/router/middleware/build/build_test.go index 2940c17d7..40e52f52f 100644 --- a/router/middleware/build/build_test.go +++ b/router/middleware/build/build_test.go @@ -5,16 +5,15 @@ package build import ( + "context" "net/http" "net/http/httptest" "reflect" "testing" - "github.com/go-vela/server/router/middleware/org" - "github.com/gin-gonic/gin" "github.com/go-vela/server/database" - "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" "github.com/go-vela/types/library" ) @@ -52,9 +51,11 @@ func TestBuild_Establish(t *testing.T) { want := new(library.Build) want.SetID(1) want.SetRepoID(1) + want.SetPipelineID(0) want.SetNumber(1) want.SetParent(1) want.SetEvent("") + want.SetEventAction("") want.SetStatus("") want.SetError("") want.SetEnqueued(0) @@ -83,17 +84,19 @@ func TestBuild_Establish(t *testing.T) { got := new(library.Build) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from builds;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteBuild(context.TODO(), want) + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) - _ = db.CreateBuild(want) + _, _ = db.CreateRepo(context.TODO(), r) + _, _ = db.CreateBuild(context.TODO(), want) // setup context gin.SetMode(gin.TestMode) @@ -127,8 +130,11 @@ func TestBuild_Establish(t *testing.T) { func TestBuild_Establish_NoRepo(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // setup context gin.SetMode(gin.TestMode) @@ -161,15 +167,17 @@ func TestBuild_Establish_NoBuildParameter(t *testing.T) { r.SetVisibility("public") // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(context.TODO(), r) // setup context gin.SetMode(gin.TestMode) @@ -207,15 +215,17 @@ func TestBuild_Establish_InvalidBuildParameter(t *testing.T) { r.SetVisibility("public") // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(context.TODO(), r) // setup context gin.SetMode(gin.TestMode) @@ -253,15 +263,17 @@ func TestBuild_Establish_NoBuild(t *testing.T) { r.SetVisibility("public") // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(context.TODO(), r) // setup context gin.SetMode(gin.TestMode) diff --git a/router/middleware/build/doc.go b/router/middleware/build/doc.go index 662549772..bbe90cf70 100644 --- a/router/middleware/build/doc.go +++ b/router/middleware/build/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/router/middleware/build" +// import "github.com/go-vela/server/router/middleware/build" package build diff --git a/router/middleware/claims/claims.go b/router/middleware/claims/claims.go new file mode 100644 index 000000000..5ba2784f6 --- /dev/null +++ b/router/middleware/claims/claims.go @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package claims + +import ( + "net/http" + "strings" + + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/auth" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" + + "github.com/gin-gonic/gin" +) + +// Retrieve gets the claims in the given context. +func Retrieve(c *gin.Context) *token.Claims { + return FromContext(c) +} + +// Establish sets the claims in the given context. +func Establish() gin.HandlerFunc { + return func(c *gin.Context) { + tm := c.MustGet("token-manager").(*token.Manager) + // get the access token from the request + at, err := auth.RetrieveAccessToken(c.Request) + if err != nil { + util.HandleError(c, http.StatusUnauthorized, err) + return + } + + claims := new(token.Claims) + + // special handling for workers if symmetric token is provided + if secret, ok := c.Value("secret").(string); ok { + if strings.EqualFold(at, secret) { + claims.Subject = "vela-worker" + claims.TokenType = constants.ServerWorkerTokenType + ToContext(c, claims) + c.Next() + + return + } + } + + // parse and validate the token and return the associated the user + claims, err = tm.ParseToken(at) + if err != nil { + util.HandleError(c, http.StatusUnauthorized, err) + return + } + + ToContext(c, claims) + c.Next() + } +} diff --git a/router/middleware/claims/claims_test.go b/router/middleware/claims/claims_test.go new file mode 100644 index 000000000..da28eb319 --- /dev/null +++ b/router/middleware/claims/claims_test.go @@ -0,0 +1,304 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package claims + +import ( + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "strings" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/golang-jwt/jwt/v5" +) + +func TestClaims_Retrieve(t *testing.T) { + // setup types + now := time.Now() + want := &token.Claims{ + TokenType: constants.UserAccessTokenType, + IsAdmin: false, + IsActive: true, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "octocat", + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 1)), + }, + } + + // setup context + gin.SetMode(gin.TestMode) + + context, _ := gin.CreateTestContext(nil) + ToContext(context, want) + + // run test + got := Retrieve(context) + + if got != want { + t.Errorf("Retrieve is %v, want %v", got, want) + } +} + +func TestClaims_Establish(t *testing.T) { + // setup types + user := new(library.User) + user.SetID(1) + user.SetName("foo") + user.SetRefreshToken("fresh") + user.SetToken("bar") + user.SetHash("baz") + user.SetActive(true) + user.SetAdmin(false) + user.SetFavorites([]string{}) + + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + WorkerAuthTokenDuration: time.Minute * 20, + WorkerRegisterTokenDuration: time.Minute * 1, + } + + now := time.Now() + + tests := []struct { + TokenType string + WantClaims *token.Claims + Mto *token.MintTokenOpts + CtxRequest string + Endpoint string + }{ + { + TokenType: constants.UserAccessTokenType, + WantClaims: &token.Claims{ + TokenType: constants.UserAccessTokenType, + IsAdmin: false, + IsActive: true, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "foo", + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 5)), + }, + }, + Mto: &token.MintTokenOpts{ + User: user, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + }, + CtxRequest: "/repos/foo/bar/builds/1", + Endpoint: "repos/:org/:repo/builds/:build", + }, + { + TokenType: constants.WorkerBuildTokenType, + WantClaims: &token.Claims{ + TokenType: constants.WorkerBuildTokenType, + BuildID: 1, + Repo: "foo/bar", + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "host", + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 35)), + }, + }, + Mto: &token.MintTokenOpts{ + Hostname: "host", + BuildID: 1, + Repo: "foo/bar", + TokenDuration: time.Minute * 35, + TokenType: constants.WorkerBuildTokenType, + }, + CtxRequest: "/repos/foo/bar/builds/1", + Endpoint: "repos/:org/:repo/builds/:build", + }, + { + TokenType: constants.WorkerAuthTokenType, + WantClaims: &token.Claims{ + TokenType: constants.WorkerAuthTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "host", + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(tm.WorkerAuthTokenDuration)), + }, + }, + Mto: &token.MintTokenOpts{ + Hostname: "host", + TokenDuration: tm.WorkerAuthTokenDuration, + TokenType: constants.WorkerAuthTokenType, + }, + CtxRequest: "/workers/host", + Endpoint: "/workers/:hostname", + }, + { + TokenType: constants.WorkerRegisterTokenType, + WantClaims: &token.Claims{ + TokenType: constants.WorkerRegisterTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "host", + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(tm.WorkerRegisterTokenDuration)), + }, + }, + Mto: &token.MintTokenOpts{ + Hostname: "host", + TokenDuration: tm.WorkerRegisterTokenDuration, + TokenType: constants.WorkerRegisterTokenType, + }, + CtxRequest: "/workers/host/register", + Endpoint: "workers/:hostname/register", + }, + { + TokenType: constants.ServerWorkerTokenType, + WantClaims: &token.Claims{ + TokenType: constants.ServerWorkerTokenType, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "vela-worker", + }, + }, + CtxRequest: "/repos/foo/bar/builds/1", + Endpoint: "repos/:org/:repo/builds/:build", + }, + } + + got := new(token.Claims) + + gin.SetMode(gin.TestMode) + + for _, tt := range tests { + t.Run(tt.TokenType, func(t *testing.T) { + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodPut, tt.CtxRequest, nil) + + var tkn string + + if strings.EqualFold(tt.TokenType, constants.ServerWorkerTokenType) { + tkn = "very-secret" + engine.Use(func(c *gin.Context) { c.Set("secret", "very-secret") }) + } else { + tkn, _ = tm.MintToken(tt.Mto) + } + + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) + + // setup context + gin.SetMode(gin.TestMode) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(Establish()) + engine.PUT(tt.Endpoint, func(c *gin.Context) { + got = Retrieve(c) + + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusOK) + } + + if !reflect.DeepEqual(got, tt.WantClaims) { + t.Errorf("Establish is %v, want %v", got, tt.WantClaims) + } + + s1.Close() + }) + } +} + +func TestClaims_Establish_NoToken(t *testing.T) { + // setup types + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/workers/host", nil) + + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(Establish()) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusUnauthorized { + t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusUnauthorized) + } +} + +func TestClaims_Establish_BadToken(t *testing.T) { + // setup types + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/workers/host", nil) + + u := new(library.User) + u.SetID(1) + u.SetName("octocat") + u.SetHash("abc") + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteUser(u) + db.Close() + }() + + _ = db.CreateUser(u) + + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: time.Minute * -1, + TokenType: constants.UserRefreshTokenType, + } + + tkn, _ := tm.MintToken(mto) + + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) + + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(func(c *gin.Context) { c.Set("secret", "very-secret") }) + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(Establish()) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusUnauthorized { + t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusUnauthorized) + } +} diff --git a/router/middleware/claims/context.go b/router/middleware/claims/context.go new file mode 100644 index 000000000..5e51b2b4f --- /dev/null +++ b/router/middleware/claims/context.go @@ -0,0 +1,39 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package claims + +import ( + "context" + + "github.com/go-vela/server/internal/token" +) + +const key = "claims" + +// Setter defines a context that enables setting values. +type Setter interface { + Set(string, interface{}) +} + +// FromContext returns the Claims associated with this context. +func FromContext(c context.Context) *token.Claims { + value := c.Value(key) + if value == nil { + return nil + } + + cl, ok := value.(*token.Claims) + if !ok { + return nil + } + + return cl +} + +// ToContext adds the Claims to this context if it supports +// the Setter interface. +func ToContext(c Setter, cl *token.Claims) { + c.Set(key, cl) +} diff --git a/router/middleware/claims/context_test.go b/router/middleware/claims/context_test.go new file mode 100644 index 000000000..b29fd739c --- /dev/null +++ b/router/middleware/claims/context_test.go @@ -0,0 +1,110 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package claims + +import ( + "testing" + "time" + + "github.com/go-vela/server/internal/token" + "github.com/go-vela/types/constants" + "github.com/golang-jwt/jwt/v5" + + "github.com/gin-gonic/gin" +) + +func TestClaims_FromContext(t *testing.T) { + now := time.Now() + want := &token.Claims{ + TokenType: constants.UserAccessTokenType, + IsAdmin: false, + IsActive: true, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "octocat", + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 1)), + }, + } + + // setup context + gin.SetMode(gin.TestMode) + context, _ := gin.CreateTestContext(nil) + context.Set(key, want) + + // run test + got := FromContext(context) + + if got != want { + t.Errorf("FromContext is %v, want %v", got, want) + } +} + +func TestClaims_FromContext_Bad(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + context, _ := gin.CreateTestContext(nil) + context.Set(key, nil) + + // run test + got := FromContext(context) + + if got != nil { + t.Errorf("FromContext is %v, want nil", got) + } +} + +func TestClaims_FromContext_WrongType(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + context, _ := gin.CreateTestContext(nil) + context.Set(key, 1) + + // run test + got := FromContext(context) + + if got != nil { + t.Errorf("FromContext is %v, want nil", got) + } +} + +func TestClaims_FromContext_Empty(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + context, _ := gin.CreateTestContext(nil) + + // run test + got := FromContext(context) + + if got != nil { + t.Errorf("FromContext is %v, want nil", got) + } +} + +func TestClaims_ToContext(t *testing.T) { + // setup types + now := time.Now() + want := &token.Claims{ + TokenType: constants.UserAccessTokenType, + IsAdmin: false, + IsActive: true, + RegisteredClaims: jwt.RegisteredClaims{ + Subject: "octocat", + IssuedAt: jwt.NewNumericDate(now), + ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 1)), + }, + } + + // setup context + gin.SetMode(gin.TestMode) + context, _ := gin.CreateTestContext(nil) + ToContext(context, want) + + // run test + got := context.Value(key) + + if got != want { + t.Errorf("ToContext is %v, want %v", got, want) + } +} diff --git a/router/middleware/claims/doc.go b/router/middleware/claims/doc.go new file mode 100644 index 000000000..f91ec309c --- /dev/null +++ b/router/middleware/claims/doc.go @@ -0,0 +1,12 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package claims provides the ability for inserting +// token claims resources into or extracting token claims +// resources from the middleware chain for the API. +// +// Usage: +// +// import "github.com/go-vela/server/router/middleware/claims" +package claims diff --git a/router/middleware/database.go b/router/middleware/database.go index f4037b7df..fbbf91eea 100644 --- a/router/middleware/database.go +++ b/router/middleware/database.go @@ -11,7 +11,7 @@ import ( // Database is a middleware function that initializes the database and // attaches to the context of every http.Request. -func Database(d database.Service) gin.HandlerFunc { +func Database(d database.Interface) gin.HandlerFunc { return func(c *gin.Context) { database.ToContext(c, d) c.Next() diff --git a/router/middleware/database_test.go b/router/middleware/database_test.go index e8f6496bb..b0ba77b04 100644 --- a/router/middleware/database_test.go +++ b/router/middleware/database_test.go @@ -12,15 +12,17 @@ import ( "github.com/gin-gonic/gin" "github.com/go-vela/server/database" - "github.com/go-vela/server/database/sqlite" ) func TestMiddleware_Database(t *testing.T) { // setup types - var got database.Service + var got database.Interface - want, _ := sqlite.NewTest() - defer func() { _sql, _ := want.Sqlite.DB(); _sql.Close() }() + want, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer want.Close() // setup context gin.SetMode(gin.TestMode) diff --git a/router/middleware/default_build_limit_test.go b/router/middleware/default_build_limit_test.go index 8c0ec2923..a0ed48c2e 100644 --- a/router/middleware/default_build_limit_test.go +++ b/router/middleware/default_build_limit_test.go @@ -16,6 +16,7 @@ import ( func TestMiddleware_DefaultBuildLimit(t *testing.T) { // setup types var got int64 + want := int64(10) // setup context diff --git a/router/middleware/default_repo_events.go b/router/middleware/default_repo_events.go new file mode 100644 index 000000000..65e5ed5b0 --- /dev/null +++ b/router/middleware/default_repo_events.go @@ -0,0 +1,18 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +// DefaultRepoEvents is a middleware function that attaches the defaultRepoEvents +// to enable the server to override the default repo event. +func DefaultRepoEvents(defaultRepoEvents []string) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("defaultRepoEvents", defaultRepoEvents) + c.Next() + } +} diff --git a/router/middleware/default_repo_events_test.go b/router/middleware/default_repo_events_test.go new file mode 100644 index 000000000..83cfc1703 --- /dev/null +++ b/router/middleware/default_repo_events_test.go @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package middleware + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/go-vela/types/constants" + + "github.com/gin-gonic/gin" +) + +func TestMiddleware_DefaultRepoEvents(t *testing.T) { + // setup types + var got []string + + want := []string{constants.EventPush} + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/health", nil) + + // setup mock server + engine.Use(DefaultRepoEvents(want)) + engine.GET("/health", func(c *gin.Context) { + got = c.Value("defaultRepoEvents").([]string) + + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("DefaultRepoEvents returned %v, want %v", resp.Code, http.StatusOK) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("DefaultRepoEvents is %v, want %v", got, want) + } +} diff --git a/router/middleware/default_timeout_test.go b/router/middleware/default_timeout_test.go index 25be3bce8..2c80a9dc2 100644 --- a/router/middleware/default_timeout_test.go +++ b/router/middleware/default_timeout_test.go @@ -16,6 +16,7 @@ import ( func TestMiddleware_DefaultTimeout(t *testing.T) { // setup types var got int64 + want := int64(60) // setup context diff --git a/router/middleware/doc.go b/router/middleware/doc.go index fc9e8ae67..36ab883b0 100644 --- a/router/middleware/doc.go +++ b/router/middleware/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/router/middleware" +// import "github.com/go-vela/server/router/middleware" package middleware diff --git a/router/middleware/executors/doc.go b/router/middleware/executors/doc.go index faddc1d94..5a75b0cf3 100644 --- a/router/middleware/executors/doc.go +++ b/router/middleware/executors/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/router/middleware/executor" +// import "github.com/go-vela/server/router/middleware/executor" package executors diff --git a/router/middleware/executors/executors.go b/router/middleware/executors/executors.go index 20061022b..777da7c37 100644 --- a/router/middleware/executors/executors.go +++ b/router/middleware/executors/executors.go @@ -5,20 +5,20 @@ package executors import ( + "context" "encoding/json" - "io/ioutil" + "fmt" + "io" + "net/http" "time" - "github.com/go-vela/types/library" - + "github.com/gin-gonic/gin" "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" "github.com/go-vela/server/router/middleware/build" "github.com/go-vela/server/util" - - "fmt" - "net/http" - - "github.com/gin-gonic/gin" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" ) // Retrieve gets the executors in the given context. @@ -31,11 +31,21 @@ func Establish() gin.HandlerFunc { return func(c *gin.Context) { e := new([]library.Executor) b := build.Retrieve(c) + + // if build has no host, we cannot establish executors + if len(b.GetHost()) == 0 { + ToContext(c, *e) + c.Next() + + return + } + // retrieve the worker - w, err := database.FromContext(c).GetWorker(b.GetHost()) + w, err := database.FromContext(c).GetWorkerForHostname(b.GetHost()) if err != nil { retErr := fmt.Errorf("unable to get worker: %w", err) util.HandleError(c, http.StatusNotFound, retErr) + return } @@ -43,15 +53,35 @@ func Establish() gin.HandlerFunc { client := http.DefaultClient client.Timeout = 30 * time.Second endpoint := fmt.Sprintf("%s/api/v1/executors", w.GetAddress()) - req, err := http.NewRequest("GET", endpoint, nil) + + req, err := http.NewRequestWithContext(context.Background(), "GET", endpoint, nil) if err != nil { retErr := fmt.Errorf("unable to form request to %s: %w", endpoint, err) util.HandleError(c, http.StatusBadRequest, retErr) + return } - // add the token to authenticate to the worker as a header - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.MustGet("secret").(string))) + tm := c.MustGet("token-manager").(*token.Manager) + + // set mint token options + mto := &token.MintTokenOpts{ + Hostname: "vela-server", + TokenType: constants.WorkerAuthTokenType, + TokenDuration: time.Minute * 1, + } + + // mint token + tkn, err := tm.MintToken(mto) + if err != nil { + retErr := fmt.Errorf("unable to generate auth token: %w", err) + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + + // add the token to authenticate to the worker + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tkn)) // make the request to the worker and check the response resp, err := client.Do(req) @@ -64,10 +94,11 @@ func Establish() gin.HandlerFunc { defer resp.Body.Close() // Read Response Body - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { retErr := fmt.Errorf("unable to read response from %s: %w", endpoint, err) util.HandleError(c, http.StatusBadRequest, retErr) + return } @@ -76,6 +107,7 @@ func Establish() gin.HandlerFunc { if err != nil { retErr := fmt.Errorf("unable to parse response from %s: %w", endpoint, err) util.HandleError(c, http.StatusBadRequest, retErr) + return } diff --git a/router/middleware/header.go b/router/middleware/header.go index 9eea7c22a..39ae9b68c 100644 --- a/router/middleware/header.go +++ b/router/middleware/header.go @@ -51,13 +51,9 @@ func Secure(c *gin.Context) { c.Header("X-Frame-Options", "DENY") c.Header("X-Content-Type-Options", "nosniff") c.Header("X-XSS-Protection", "1; mode=block") - - if c.Request.TLS != nil { - c.Header("Strict-Transport-Security", "max-age=31536000") - } - - // Also consider adding Content-Security-Policy headers + // TODO: consider adding Content-Security-Policy headers // c.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com") + c.Header("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload") } // Cors is a middleware function that appends headers for @@ -67,10 +63,12 @@ func Cors(c *gin.Context) { m := c.MustGet("metadata").(*types.Metadata) c.Header("Access-Control-Allow-Origin", "*") + if len(m.Vela.WebAddress) > 0 { c.Header("Access-Control-Allow-Origin", m.Vela.WebAddress) c.Header("Access-Control-Allow-Credentials", "true") } + c.Header("Access-Control-Expose-Headers", "link, x-total-count") } diff --git a/router/middleware/header_test.go b/router/middleware/header_test.go index e17e24c18..86f79d41a 100644 --- a/router/middleware/header_test.go +++ b/router/middleware/header_test.go @@ -267,7 +267,7 @@ func TestMiddleware_Secure_TLS(t *testing.T) { wantFrameOptions := "DENY" wantContentTypeOptions := "nosniff" wantProtection := "1; mode=block" - wantSecurity := "max-age=31536000" + wantSecurity := "max-age=63072000; includeSubDomains; preload" // setup context gin.SetMode(gin.TestMode) diff --git a/router/middleware/logger.go b/router/middleware/logger.go index 40f2e7d7b..a6cb7f835 100644 --- a/router/middleware/logger.go +++ b/router/middleware/logger.go @@ -7,15 +7,16 @@ package middleware import ( "time" - "github.com/go-vela/server/router/middleware/org" - "github.com/gin-gonic/gin" "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" "github.com/go-vela/server/router/middleware/service" "github.com/go-vela/server/router/middleware/step" "github.com/go-vela/server/router/middleware/user" "github.com/go-vela/server/router/middleware/worker" + "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" "github.com/sirupsen/logrus" ) @@ -25,36 +26,34 @@ import ( // Requests without errors are logged using logrus.Info(). // // It receives: -// 1. A time package format string (e.g. time.RFC3339). -// 2. A boolean stating whether to use UTC time zone or local. -func Logger(logger *logrus.Logger, timeFormat string, utc bool) gin.HandlerFunc { +// 1. A time package format string (e.g. time.RFC3339). +func Logger(logger *logrus.Logger, timeFormat string) gin.HandlerFunc { return func(c *gin.Context) { start := time.Now() // some evil middlewares modify this values - path := c.Request.URL.Path + path := util.EscapeValue(c.Request.URL.Path) c.Next() end := time.Now() + latency := end.Sub(start) - if utc { - end = end.UTC() - } // prevent us from logging the health endpoint if c.Request.URL.Path != "/health" { fields := logrus.Fields{ - "ip": c.ClientIP(), + "ip": util.EscapeValue(c.ClientIP()), "latency": latency, "method": c.Request.Method, "path": path, "status": c.Writer.Status(), - "user-agent": c.Request.UserAgent(), - "version": c.GetHeader("X-Vela-Version"), + "user-agent": util.EscapeValue(c.Request.UserAgent()), + "version": util.EscapeValue(c.GetHeader("X-Vela-Version")), } body := c.Value("payload") if body != nil { + body = sanitize(body) fields["body"] = body } @@ -104,3 +103,14 @@ func Logger(logger *logrus.Logger, timeFormat string, utc bool) gin.HandlerFunc } } } + +func sanitize(body interface{}) interface{} { + if m, ok := body.(map[string]interface{}); ok { + if _, ok = m["email"]; ok { + m["email"] = constants.SecretMask + body = m + } + } + + return body +} diff --git a/router/middleware/logger_test.go b/router/middleware/logger_test.go index c4ef3a1e3..5204763e7 100644 --- a/router/middleware/logger_test.go +++ b/router/middleware/logger_test.go @@ -11,6 +11,7 @@ import ( "net/http" "net/http/httptest" "reflect" + "strings" "testing" "time" @@ -86,7 +87,7 @@ func TestMiddleware_Logger(t *testing.T) { engine.Use(func(c *gin.Context) { user.ToContext(c, u) }) engine.Use(func(c *gin.Context) { worker.ToContext(c, w) }) engine.Use(Payload()) - engine.Use(Logger(logger, time.RFC3339, true)) + engine.Use(Logger(logger, time.RFC3339)) engine.POST("/foobar", func(c *gin.Context) { c.Status(http.StatusOK) }) @@ -126,9 +127,9 @@ func TestMiddleware_Logger_Error(t *testing.T) { context.Request, _ = http.NewRequest(http.MethodGet, "/foobar", nil) // setup mock server - engine.Use(Logger(logger, time.RFC3339, true)) + engine.Use(Logger(logger, time.RFC3339)) engine.GET("/foobar", func(c *gin.Context) { - // nolint: errcheck // ignore checking error + //nolint:errcheck // ignore checking error c.Error(fmt.Errorf("test error")) c.Status(http.StatusOK) }) @@ -151,3 +152,68 @@ func TestMiddleware_Logger_Error(t *testing.T) { t.Errorf("Logger Message is %v, want %v", gotMessage, wantMessage) } } + +func TestMiddleware_Logger_Sanitize(t *testing.T) { + var logBody, logWant map[string]interface{} + + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + logRepo, _ := json.Marshal(r) + + b := new(library.Build) + b.SetID(1) + b.SetRepoID(1) + b.SetNumber(1) + b.SetEmail("octocat@github.com") + logBuild, _ := json.Marshal(b) + + sanitizeBuild := *b + sanitizeBuild.SetEmail("[secure]") + logSBuild, _ := json.Marshal(&sanitizeBuild) + + tests := []struct { + dataType string + body []byte + want []byte + }{ + { + dataType: "stringMap", + body: logRepo, + want: logRepo, + }, + { + dataType: "stringMap", + body: logBuild, + want: logSBuild, + }, + { + dataType: "string", + body: []byte("successfully updated step"), + want: []byte("successfully updated step"), + }, + } + + for _, test := range tests { + if strings.EqualFold(test.dataType, "stringMap") { + err := json.Unmarshal(test.body, &logBody) + if err != nil { + t.Errorf("unable to unmarshal log body data") + } + + err = json.Unmarshal(test.want, &logWant) + if err != nil { + t.Errorf("unable to unmarshal log want data") + } + } + + got := sanitize(logBody) + + if !reflect.DeepEqual(got, logWant) { + t.Errorf("Logger returned %v, want %v", got, logWant) + } + } +} diff --git a/router/middleware/max_build_limit_test.go b/router/middleware/max_build_limit_test.go index f22df3846..00e3516c9 100644 --- a/router/middleware/max_build_limit_test.go +++ b/router/middleware/max_build_limit_test.go @@ -16,6 +16,7 @@ import ( func TestMiddleware_MaxBuildLimit(t *testing.T) { // setup types var got int64 + want := int64(30) // setup context diff --git a/router/middleware/org/doc.go b/router/middleware/org/doc.go index e3ddee377..32c3d410a 100644 --- a/router/middleware/org/doc.go +++ b/router/middleware/org/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/router/middleware/org" +// import "github.com/go-vela/server/router/middleware/org" package org diff --git a/router/middleware/org/org.go b/router/middleware/org/org.go index 6f5ab5b84..153807e4b 100644 --- a/router/middleware/org/org.go +++ b/router/middleware/org/org.go @@ -5,12 +5,11 @@ package org import ( - "github.com/go-vela/server/util" - "fmt" "net/http" "github.com/gin-gonic/gin" + "github.com/go-vela/server/util" ) // Retrieve gets the org in the given context. @@ -21,14 +20,16 @@ func Retrieve(c *gin.Context) string { // Establish used to check if org param is used only. func Establish() gin.HandlerFunc { return func(c *gin.Context) { - oParam := c.Param("org") + oParam := util.PathParameter(c, "org") if len(oParam) == 0 { retErr := fmt.Errorf("no org parameter provided") util.HandleError(c, http.StatusBadRequest, retErr) + return } ToContext(c, oParam) + c.Next() } } diff --git a/router/middleware/org/org_test.go b/router/middleware/org/org_test.go index bfecf125b..7e6007bc1 100644 --- a/router/middleware/org/org_test.go +++ b/router/middleware/org/org_test.go @@ -5,6 +5,7 @@ package org import ( + "context" "net/http" "net/http/httptest" "reflect" @@ -12,7 +13,6 @@ import ( "github.com/gin-gonic/gin" "github.com/go-vela/server/database" - "github.com/go-vela/server/database/sqlite" "github.com/go-vela/types/library" ) @@ -35,7 +35,6 @@ func TestOrg_Retrieve(t *testing.T) { func TestOrg_Establish(t *testing.T) { // setup types - r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -61,15 +60,17 @@ func TestOrg_Establish(t *testing.T) { got := "" // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(context.TODO(), r) // setup context gin.SetMode(gin.TestMode) @@ -101,8 +102,11 @@ func TestOrg_Establish(t *testing.T) { func TestOrg_Establish_NoOrgParameter(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // setup context gin.SetMode(gin.TestMode) diff --git a/router/middleware/payload.go b/router/middleware/payload.go index 2ef7e5e22..c2241fa9f 100644 --- a/router/middleware/payload.go +++ b/router/middleware/payload.go @@ -7,7 +7,7 @@ package middleware import ( "bytes" "encoding/json" - "io/ioutil" + "io" "github.com/gin-gonic/gin" ) @@ -24,7 +24,7 @@ func Payload() gin.HandlerFunc { c.Set("payload", payload) - c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + c.Request.Body = io.NopCloser(bytes.NewBuffer(body)) c.Next() } diff --git a/router/middleware/perm/doc.go b/router/middleware/perm/doc.go index daf86955b..4cb5ad472 100644 --- a/router/middleware/perm/doc.go +++ b/router/middleware/perm/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/router/middleware/perm" +// import "github.com/go-vela/server/router/middleware/perm" package perm diff --git a/router/middleware/perm/perm.go b/router/middleware/perm/perm.go index c5a80502a..8028275ae 100644 --- a/router/middleware/perm/perm.go +++ b/router/middleware/perm/perm.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -9,38 +9,165 @@ import ( "net/http" "strings" + "github.com/gin-gonic/gin" "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/claims" "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" "github.com/go-vela/server/router/middleware/user" "github.com/go-vela/server/scm" "github.com/go-vela/server/util" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) // MustPlatformAdmin ensures the user has admin access to the platform. func MustPlatformAdmin() gin.HandlerFunc { return func(c *gin.Context) { - u := user.Retrieve(c) + cl := claims.Retrieve(c) // update engine logger with API metadata // // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields logrus.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Debugf("verifying user %s is a platform admin", u.GetName()) + "user": cl.Subject, + }).Debugf("verifying user %s is a platform admin", cl.Subject) switch { - case globalPerms(u): + case cl.IsAdmin: + return + + default: + if strings.EqualFold(cl.TokenType, constants.WorkerBuildTokenType) { + logrus.WithFields(logrus.Fields{ + "user": cl.Subject, + "repo": cl.Repo, + "build": cl.BuildID, + }).Warnf("attempted access of admin endpoint with build token from %s", cl.Subject) + } + + retErr := fmt.Errorf("user %s is not a platform admin", cl.Subject) + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + } +} + +// MustWorkerRegisterToken ensures the token is a registration token retrieved by a platform admin. +func MustWorkerRegisterToken() gin.HandlerFunc { + return func(c *gin.Context) { + cl := claims.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "user": cl.Subject, + }).Debugf("verifying user %s has a registration token for worker", cl.Subject) + + switch cl.TokenType { + case constants.WorkerRegisterTokenType: + return + case constants.ServerWorkerTokenType: + if strings.EqualFold(cl.Subject, "vela-worker") { + return + } + + retErr := fmt.Errorf("server-worker token provided but does not match configuration") + util.HandleError(c, http.StatusBadRequest, retErr) + + return + default: + retErr := fmt.Errorf("invalid token type: must provide a worker registration token") + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + } +} + +// MustWorkerAuthToken ensures the token is a worker auth token. +func MustWorkerAuthToken() gin.HandlerFunc { + return func(c *gin.Context) { + cl := claims.Retrieve(c) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "worker": cl.Subject, + }).Debugf("verifying worker %s has a valid auth token", cl.Subject) + + // global permissions bypass + if cl.IsAdmin { + logrus.WithFields(logrus.Fields{ + "user": cl.Subject, + }).Debugf("user %s has platform admin permissions", cl.Subject) + + return + } + + switch cl.TokenType { + case constants.WorkerAuthTokenType, constants.WorkerRegisterTokenType: + return + case constants.ServerWorkerTokenType: + if strings.EqualFold(cl.Subject, "vela-worker") { + return + } + + retErr := fmt.Errorf("server-worker token provided but does not match configuration") + util.HandleError(c, http.StatusBadRequest, retErr) + + return + default: + retErr := fmt.Errorf("invalid token type: must provide a worker auth token") + util.HandleError(c, http.StatusUnauthorized, retErr) + return + } + } +} + +// MustBuildAccess ensures the token is a build token for the appropriate build. +func MustBuildAccess() gin.HandlerFunc { + return func(c *gin.Context) { + cl := claims.Retrieve(c) + b := build.Retrieve(c) + + // global permissions bypass + if cl.IsAdmin { + logrus.WithFields(logrus.Fields{ + "user": cl.Subject, + }).Debugf("user %s has platform admin permissions", cl.Subject) + + return + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "worker": cl.Subject, + }).Debugf("verifying worker %s has a valid build token", cl.Subject) + // validate token type and match build id in request with build id in token claims + switch cl.TokenType { + case constants.WorkerBuildTokenType: + if b.GetID() == cl.BuildID { + return + } + + logrus.WithFields(logrus.Fields{ + "user": cl.Subject, + "repo": cl.Repo, + "build": cl.BuildID, + }).Warnf("build token for build %d attempted to be used for build %d by %s", cl.BuildID, b.GetID(), cl.Subject) + + fallthrough default: - retErr := fmt.Errorf("user %s is not a platform admin", u.GetName()) + retErr := fmt.Errorf("invalid token: must provide matching worker build token") util.HandleError(c, http.StatusUnauthorized, retErr) return @@ -50,14 +177,16 @@ func MustPlatformAdmin() gin.HandlerFunc { // MustSecretAdmin ensures the user has admin access to the org, repo or team. // -// nolint: funlen // ignore function length due to comments +//nolint:funlen // ignore function length func MustSecretAdmin() gin.HandlerFunc { return func(c *gin.Context) { + cl := claims.Retrieve(c) u := user.Retrieve(c) - e := c.Param("engine") - t := c.Param("type") - o := c.Param("org") - n := c.Param("name") + e := util.PathParameter(c, "engine") + t := util.PathParameter(c, "type") + o := util.PathParameter(c, "org") + n := util.PathParameter(c, "name") + s := util.PathParameter(c, "secret") m := c.Request.Method // create log fields from API metadata @@ -86,10 +215,49 @@ func MustSecretAdmin() gin.HandlerFunc { // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields logger := logrus.WithFields(fields) - if globalPerms(u) { + if u.GetAdmin() { return } + // if caller is worker with build token, verify it has access to requested secret + if strings.EqualFold(cl.TokenType, constants.WorkerBuildTokenType) { + org, repo := util.SplitFullName(cl.Repo) + + switch t { + case constants.SecretShared: + return + case constants.SecretOrg: + logger.Debugf("verifying subject %s has token permissions for org %s", cl.Subject, o) + + if strings.EqualFold(org, o) { + return + } + + logger.Warnf("build token for build %s/%d attempted to be used for secret %s/%s by %s", cl.Repo, cl.BuildID, o, s, cl.Subject) + + retErr := fmt.Errorf("subject %s does not have token permissions for the org %s", cl.Subject, o) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + + case constants.SecretRepo: + logger.Debugf("verifying subject %s has token permissions for repo %s/%s", cl.Subject, o, n) + + if strings.EqualFold(org, o) && strings.EqualFold(repo, n) { + return + } + + logger.Warnf("build token for build %s/%d attempted to be used for secret %s/%s/%s by %s", cl.Repo, cl.BuildID, o, n, s, cl.Subject) + + retErr := fmt.Errorf("subject %s does not have token permissions for the repo %s/%s", cl.Subject, o, n) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + } + switch t { case constants.SecretOrg: logger.Debugf("verifying user %s has 'admin' permissions for org %s", u.GetName(), o) @@ -115,7 +283,6 @@ func MustSecretAdmin() gin.HandlerFunc { } if !strings.EqualFold(perm, "admin") { - // nolint: lll // ignore long line length due to error message retErr := fmt.Errorf("user %s does not have 'admin' permissions for the repo %s/%s", u.GetName(), o, n) util.HandleError(c, http.StatusUnauthorized, retErr) @@ -124,6 +291,16 @@ func MustSecretAdmin() gin.HandlerFunc { } case constants.SecretShared: if n == "*" && m == "GET" { + // check if user is accessing shared secrets in personal org + if strings.EqualFold(o, u.GetName()) { + logger.WithFields(logrus.Fields{ + "org": o, + "user": u.GetName(), + }).Debugf("skipping gathering teams for user %s with org %s", u.GetName(), o) + + return + } + logger.Debugf("gathering teams user %s is a member of in the org %s", u.GetName(), o) teams, err := scm.FromContext(c).ListUsersTeamsForOrg(u, o) @@ -147,7 +324,6 @@ func MustSecretAdmin() gin.HandlerFunc { } if !strings.EqualFold(perm, "admin") { - // nolint: lll // ignore long line length due to error message retErr := fmt.Errorf("user %s does not have 'admin' permissions for the team %s/%s", u.GetName(), o, n) util.HandleError(c, http.StatusUnauthorized, retErr) @@ -182,10 +358,9 @@ func MustAdmin() gin.HandlerFunc { "user": u.GetName(), }) - // nolint: lll // ignore long line length due to parameters logger.Debugf("verifying user %s has 'admin' permissions for repo %s", u.GetName(), r.GetFullName()) - if globalPerms(u) { + if u.GetAdmin() { return } @@ -212,11 +387,10 @@ func MustAdmin() gin.HandlerFunc { } switch perm { - // nolint: goconst // ignore making constant + //nolint:goconst // ignore making constant case "admin": return default: - // nolint: lll // ignore long line length due to error message retErr := fmt.Errorf("user %s does not have 'admin' permissions for the repo %s", u.GetName(), r.GetFullName()) util.HandleError(c, http.StatusUnauthorized, retErr) @@ -242,10 +416,9 @@ func MustWrite() gin.HandlerFunc { "user": u.GetName(), }) - // nolint: lll // ignore long line length due to log message logger.Debugf("verifying user %s has 'write' permissions for repo %s", u.GetName(), r.GetFullName()) - if globalPerms(u) { + if u.GetAdmin() { return } @@ -277,7 +450,6 @@ func MustWrite() gin.HandlerFunc { case "write": return default: - // nolint: lll // ignore long line length due to error message retErr := fmt.Errorf("user %s does not have 'write' permissions for the repo %s", u.GetName(), r.GetFullName()) util.HandleError(c, http.StatusUnauthorized, retErr) @@ -290,6 +462,7 @@ func MustWrite() gin.HandlerFunc { // MustRead ensures the user has admin, write or read access to the repo. func MustRead() gin.HandlerFunc { return func(c *gin.Context) { + cl := claims.Retrieve(c) o := org.Retrieve(c) r := repo.Retrieve(c) u := user.Retrieve(c) @@ -305,16 +478,29 @@ func MustRead() gin.HandlerFunc { // check if the repo visibility field is set to public if strings.EqualFold(r.GetVisibility(), constants.VisibilityPublic) { - // nolint: lll // ignore long line length due to log message logger.Debugf("skipping 'read' check for repo %s with %s visibility for user %s", r.GetFullName(), r.GetVisibility(), u.GetName()) return } - // nolint: lll // ignore long line length due to log message + // return if request is from worker with build token access + if strings.EqualFold(cl.TokenType, constants.WorkerBuildTokenType) { + b := build.Retrieve(c) + if cl.BuildID == b.GetID() { + return + } + + retErr := fmt.Errorf("subject %s does not have 'read' permissions for repo %s", cl.Subject, r.GetFullName()) + + util.HandleError(c, http.StatusUnauthorized, retErr) + + return + } + logger.Debugf("verifying user %s has 'read' permissions for repo %s", u.GetName(), r.GetFullName()) - if globalPerms(u) { + // return if user is platform admin + if u.GetAdmin() { return } @@ -348,7 +534,6 @@ func MustRead() gin.HandlerFunc { case "read": return default: - // nolint: lll // ignore long line length due to error message retErr := fmt.Errorf("user %s does not have 'read' permissions for repo %s", u.GetName(), r.GetFullName()) util.HandleError(c, http.StatusUnauthorized, retErr) @@ -357,17 +542,3 @@ func MustRead() gin.HandlerFunc { } } } - -// helper function to check if the user is a platform admin. -func globalPerms(user *library.User) bool { - switch { - // Agents have full access to endpoints - case user.GetName() == "vela-worker": - return true - // platform admins have full access to endpoints - case user.GetAdmin(): - return true - } - - return false -} diff --git a/router/middleware/perm/perm_test.go b/router/middleware/perm/perm_test.go index 1b560a63a..b1495ab19 100644 --- a/router/middleware/perm/perm_test.go +++ b/router/middleware/perm/perm_test.go @@ -1,54 +1,782 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. package perm import ( + _context "context" "fmt" "net/http" "net/http/httptest" "testing" "time" + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/claims" "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/scm/github" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/golang-jwt/jwt/v5" +) + +func TestPerm_MustPlatformAdmin(t *testing.T) { + // setup types + secret := "superSecret" + + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + u := new(library.User) + u.SetID(1) + u.SetName("foob") + u.SetToken("bar") + u.SetHash("baz") + u.SetAdmin(true) + + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteUser(u) + db.Close() + }() + + _ = db.CreateUser(u) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + context.Request, _ = http.NewRequest(http.MethodGet, "/admin/users", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + + // setup github mock server + engine.GET("/api/v3/user", func(c *gin.Context) { + c.String(http.StatusOK, userPayload) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup client + client, _ := github.NewTest(s.URL) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(MustPlatformAdmin()) + engine.GET("/admin/users", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("MustPlatAdmin returned %v, want %v", resp.Code, http.StatusOK) + } +} + +func TestPerm_MustPlatformAdmin_NotAdmin(t *testing.T) { + // setup types + secret := "superSecret" + + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + u := new(library.User) + u.SetID(1) + u.SetName("foob") + u.SetToken("bar") + u.SetHash("baz") + u.SetAdmin(false) + + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteUser(u) + db.Close() + }() + + _ = db.CreateUser(u) + + context.Request, _ = http.NewRequest(http.MethodGet, "/admin/users", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + + // setup github mock server + engine.GET("/api/v3/user", func(c *gin.Context) { + c.String(http.StatusOK, userPayload) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup client + client, _ := github.NewTest(s.URL) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(MustPlatformAdmin()) + engine.GET("/admin/users", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusUnauthorized { + t.Errorf("MustPlatAdmin returned %v, want %v", resp.Code, http.StatusUnauthorized) + } +} + +func TestPerm_MustWorkerRegisterToken(t *testing.T) { + // setup types + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + WorkerRegisterTokenDuration: time.Minute * 1, + WorkerAuthTokenDuration: time.Minute * 15, + } + + mto := &token.MintTokenOpts{ + Hostname: "worker", + TokenDuration: tm.WorkerRegisterTokenDuration, + TokenType: constants.WorkerRegisterTokenType, + } + + tok, _ := tm.MintToken(mto) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(MustWorkerRegisterToken()) + engine.GET("/test/:org/:repo", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("MustWorkerRegisterToken returned %v, want %v", resp.Code, http.StatusOK) + } +} + +func TestPerm_MustWorkerRegisterToken_PlatAdmin(t *testing.T) { + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + u := new(library.User) + u.SetID(1) + u.SetName("vela-worker") + u.SetToken("bar") + u.SetHash("baz") + u.SetAdmin(true) + + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteUser(u) + db.Close() + }() + + _ = db.CreateUser(u) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + + // setup vela mock server + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(MustWorkerRegisterToken()) + engine.GET("/test/:org/:repo", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusUnauthorized { + t.Errorf("MustWorkerRegisterToken returned %v, want %v", resp.Code, http.StatusUnauthorized) + } +} + +func TestPerm_MustWorkerAuthToken(t *testing.T) { + // setup types + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + WorkerRegisterTokenDuration: time.Minute * 1, + WorkerAuthTokenDuration: time.Minute * 15, + } + + mto := &token.MintTokenOpts{ + Hostname: "worker", + TokenDuration: tm.WorkerAuthTokenDuration, + TokenType: constants.WorkerAuthTokenType, + } + + tok, _ := tm.MintToken(mto) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(MustWorkerAuthToken()) + engine.GET("/test/:org/:repo", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("MustWorkerAuthToken returned %v, want %v", resp.Code, http.StatusOK) + } +} + +func TestPerm_MustWorkerAuth_ServerWorkerToken(t *testing.T) { + // setup types + secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + WorkerRegisterTokenDuration: time.Minute * 1, + WorkerAuthTokenDuration: time.Minute * 15, + } + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", secret)) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(MustWorkerAuthToken()) + engine.GET("/test/:org/:repo", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("MustWorkerAuthToken returned %v, want %v", resp.Code, http.StatusOK) + } +} + +func TestPerm_MustBuildAccess(t *testing.T) { + // setup types + secret := "superSecret" + + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + b := new(library.Build) + b.SetID(1) + b.SetRepoID(1) + b.SetNumber(1) + + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + mto := &token.MintTokenOpts{ + Hostname: "worker", + BuildID: 1, + Repo: "foo/bar", + TokenDuration: time.Minute * 30, + TokenType: constants.WorkerBuildTokenType, + } + + tok, _ := tm.MintToken(mto) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + ctx := _context.TODO() + + defer func() { + db.DeleteBuild(ctx, b) + db.DeleteRepo(_context.TODO(), r) + db.Close() + }() + + _, _ = db.CreateRepo(_context.TODO(), r) + _, _ = db.CreateBuild(ctx, b) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar/builds/1", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(build.Establish()) + engine.Use(MustBuildAccess()) + engine.GET("/test/:org/:repo/builds/:build", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("MustBuildAccess returned %v, want %v", resp.Code, http.StatusOK) + } +} + +func TestPerm_MustBuildAccess_PlatAdmin(t *testing.T) { + // setup types + secret := "superSecret" + + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + b := new(library.Build) + b.SetID(1) + b.SetRepoID(1) + b.SetNumber(1) + + u := new(library.User) + u.SetID(1) + u.SetName("admin") + u.SetToken("bar") + u.SetHash("baz") + u.SetAdmin(true) + + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + ctx := _context.TODO() + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteBuild(ctx, b) + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() + }() + + _, _ = db.CreateRepo(_context.TODO(), r) + _, _ = db.CreateBuild(ctx, b) + _ = db.CreateUser(u) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar/builds/1", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(build.Establish()) + engine.Use(MustBuildAccess()) + engine.GET("/test/:org/:repo/builds/:build", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("MustBuildAccess returned %v, want %v", resp.Code, http.StatusOK) + } +} + +func TestPerm_MustBuildToken_WrongBuild(t *testing.T) { + // setup types + secret := "superSecret" + + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + b := new(library.Build) + b.SetID(1) + b.SetRepoID(1) + b.SetNumber(1) + + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + mto := &token.MintTokenOpts{ + Hostname: "worker", + BuildID: 2, + Repo: "foo/bar", + TokenDuration: time.Minute * 30, + TokenType: constants.WorkerBuildTokenType, + } + + tok, _ := tm.MintToken(mto) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + ctx := _context.TODO() + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteBuild(ctx, b) + db.DeleteRepo(_context.TODO(), r) + db.Close() + }() + + _, _ = db.CreateRepo(_context.TODO(), r) + _, _ = db.CreateBuild(ctx, b) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar/builds/1", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(build.Establish()) + engine.Use(MustBuildAccess()) + engine.GET("/test/:org/:repo/builds/:build", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusUnauthorized { + t.Errorf("MustBuildAccess returned %v, want %v", resp.Code, http.StatusOK) + } +} + +func TestPerm_MustSecretAdmin_BuildToken_Repo(t *testing.T) { + // setup types + secret := "superSecret" + + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + b := new(library.Build) + b.SetID(1) + b.SetRepoID(1) + b.SetNumber(1) + + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + mto := &token.MintTokenOpts{ + Hostname: "worker", + BuildID: 1, + Repo: "foo/bar", + TokenDuration: time.Minute * 30, + TokenType: constants.WorkerBuildTokenType, + } + + tok, _ := tm.MintToken(mto) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + ctx := _context.TODO() + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteBuild(ctx, b) + db.DeleteRepo(_context.TODO(), r) + db.Close() + }() + + _, _ = db.CreateRepo(_context.TODO(), r) + _, _ = db.CreateBuild(ctx, b) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/native/repo/foo/bar/baz", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(MustSecretAdmin()) + engine.GET("/test/:engine/:type/:org/:name/:secret", func(c *gin.Context) { + c.Status(http.StatusOK) + }) - "github.com/gin-gonic/gin" - "github.com/go-vela/server/database" - "github.com/go-vela/server/database/sqlite" - "github.com/go-vela/server/router/middleware/repo" - "github.com/go-vela/server/router/middleware/token" - "github.com/go-vela/server/router/middleware/user" - "github.com/go-vela/server/scm" - "github.com/go-vela/server/scm/github" - "github.com/go-vela/types/library" -) + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) -const accessTokenDuration = time.Minute * 15 + if resp.Code != http.StatusOK { + t.Errorf("MustBuildAccess returned %v, want %v", resp.Code, http.StatusOK) + } +} -func TestPerm_MustPlatformAdmin(t *testing.T) { +func TestPerm_MustSecretAdmin_BuildToken_Org(t *testing.T) { // setup types secret := "superSecret" - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - u.SetAdmin(true) + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + b := new(library.Build) + b.SetID(1) + b.SetRepoID(1) + b.SetNumber(1) - // setup database - db, _ := sqlite.NewTest() + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() + mto := &token.MintTokenOpts{ + Hostname: "worker", + BuildID: 1, + Repo: "foo/bar", + TokenDuration: time.Minute * 30, + TokenType: constants.WorkerBuildTokenType, + } - _ = db.CreateUser(u) + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -56,27 +784,34 @@ func TestPerm_MustPlatformAdmin(t *testing.T) { resp := httptest.NewRecorder() context, engine := gin.CreateTestContext(resp) - context.Request, _ = http.NewRequest(http.MethodGet, "/admin/users", nil) - context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + ctx := _context.TODO() - // setup github mock server - engine.GET("/api/v3/user", func(c *gin.Context) { - c.String(http.StatusOK, userPayload) - }) + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } - s := httptest.NewServer(engine) - defer s.Close() + defer func() { + db.DeleteBuild(ctx, b) + db.DeleteRepo(_context.TODO(), r) + db.Close() + }() - // setup client - client, _ := github.NewTest(s.URL) + _, _ = db.CreateRepo(_context.TODO(), r) + _, _ = db.CreateBuild(ctx, b) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/native/org/foo/*/baz", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) - engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) - engine.Use(MustPlatformAdmin()) - engine.GET("/admin/users", func(c *gin.Context) { + engine.Use(MustSecretAdmin()) + engine.GET("/test/:engine/:type/:org/:name/:secret", func(c *gin.Context) { c.Status(http.StatusOK) }) @@ -87,22 +822,44 @@ func TestPerm_MustPlatformAdmin(t *testing.T) { engine.ServeHTTP(context.Writer, context.Request) if resp.Code != http.StatusOK { - t.Errorf("MustPlatAdmin returned %v, want %v", resp.Code, http.StatusOK) + t.Errorf("MustSecretAdmin returned %v, want %v", resp.Code, http.StatusOK) } } -func TestPerm_MustPlatformAdmin_NotAdmin(t *testing.T) { +func TestPerm_MustSecretAdmin_BuildToken_Shared(t *testing.T) { // setup types secret := "superSecret" - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - u.SetAdmin(false) + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + b := new(library.Build) + b.SetID(1) + b.SetRepoID(1) + b.SetNumber(1) + + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + mto := &token.MintTokenOpts{ + Hostname: "worker", + BuildID: 1, + Repo: "foo/bar", + TokenDuration: time.Minute * 30, + TokenType: constants.WorkerBuildTokenType, + } - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -110,38 +867,34 @@ func TestPerm_MustPlatformAdmin_NotAdmin(t *testing.T) { resp := httptest.NewRecorder() context, engine := gin.CreateTestContext(resp) + ctx := _context.TODO() + // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteBuild(ctx, b) + db.DeleteRepo(_context.TODO(), r) + db.Close() }() - _ = db.CreateUser(u) + _, _ = db.CreateRepo(_context.TODO(), r) + _, _ = db.CreateBuild(ctx, b) - context.Request, _ = http.NewRequest(http.MethodGet, "/admin/users", nil) + context.Request, _ = http.NewRequest(http.MethodGet, "/test/native/shared/foo/*/*", nil) context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) - // setup github mock server - engine.GET("/api/v3/user", func(c *gin.Context) { - c.String(http.StatusOK, userPayload) - }) - - s := httptest.NewServer(engine) - defer s.Close() - - // setup client - client, _ := github.NewTest(s.URL) - // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) - engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) - engine.Use(MustPlatformAdmin()) - engine.GET("/admin/users", func(c *gin.Context) { + engine.Use(MustSecretAdmin()) + engine.GET("/test/:engine/:type/:org/:name/:secret", func(c *gin.Context) { c.Status(http.StatusOK) }) @@ -151,8 +904,8 @@ func TestPerm_MustPlatformAdmin_NotAdmin(t *testing.T) { // run test engine.ServeHTTP(context.Writer, context.Request) - if resp.Code != http.StatusUnauthorized { - t.Errorf("MustPlatAdmin returned %v, want %v", resp.Code, http.StatusUnauthorized) + if resp.Code != http.StatusOK { + t.Errorf("MustSecretAdmin returned %v, want %v", resp.Code, http.StatusOK) } } @@ -160,6 +913,13 @@ func TestPerm_MustAdmin(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -171,12 +931,18 @@ func TestPerm_MustAdmin(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(false) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -185,16 +951,18 @@ func TestPerm_MustAdmin(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -216,8 +984,10 @@ func TestPerm_MustAdmin(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -241,6 +1011,13 @@ func TestPerm_MustAdmin_PlatAdmin(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -252,12 +1029,18 @@ func TestPerm_MustAdmin_PlatAdmin(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(true) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -266,16 +1049,18 @@ func TestPerm_MustAdmin_PlatAdmin(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -297,8 +1082,10 @@ func TestPerm_MustAdmin_PlatAdmin(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -322,6 +1109,13 @@ func TestPerm_MustAdmin_NotAdmin(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -333,12 +1127,18 @@ func TestPerm_MustAdmin_NotAdmin(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(false) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -347,16 +1147,18 @@ func TestPerm_MustAdmin_NotAdmin(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -378,8 +1180,10 @@ func TestPerm_MustAdmin_NotAdmin(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -403,6 +1207,13 @@ func TestPerm_MustWrite(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -414,12 +1225,18 @@ func TestPerm_MustWrite(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(false) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -428,16 +1245,18 @@ func TestPerm_MustWrite(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -459,8 +1278,10 @@ func TestPerm_MustWrite(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -484,6 +1305,13 @@ func TestPerm_MustWrite_PlatAdmin(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -495,12 +1323,18 @@ func TestPerm_MustWrite_PlatAdmin(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(true) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -509,16 +1343,18 @@ func TestPerm_MustWrite_PlatAdmin(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -540,8 +1376,10 @@ func TestPerm_MustWrite_PlatAdmin(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -565,6 +1403,13 @@ func TestPerm_MustWrite_RepoAdmin(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -576,12 +1421,18 @@ func TestPerm_MustWrite_RepoAdmin(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(false) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -590,16 +1441,18 @@ func TestPerm_MustWrite_RepoAdmin(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -621,8 +1474,10 @@ func TestPerm_MustWrite_RepoAdmin(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -646,6 +1501,13 @@ func TestPerm_MustWrite_NotWrite(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -657,12 +1519,18 @@ func TestPerm_MustWrite_NotWrite(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(false) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -671,16 +1539,18 @@ func TestPerm_MustWrite_NotWrite(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -702,8 +1572,10 @@ func TestPerm_MustWrite_NotWrite(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -727,6 +1599,13 @@ func TestPerm_MustRead(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -738,12 +1617,18 @@ func TestPerm_MustRead(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(false) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -752,16 +1637,18 @@ func TestPerm_MustRead(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -783,8 +1670,10 @@ func TestPerm_MustRead(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -808,6 +1697,13 @@ func TestPerm_MustRead_PlatAdmin(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -819,12 +1715,18 @@ func TestPerm_MustRead_PlatAdmin(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(true) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -833,16 +1735,18 @@ func TestPerm_MustRead_PlatAdmin(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -864,8 +1768,10 @@ func TestPerm_MustRead_PlatAdmin(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -885,10 +1791,103 @@ func TestPerm_MustRead_PlatAdmin(t *testing.T) { } } +func TestPerm_MustRead_WorkerBuildToken(t *testing.T) { + // setup types + secret := "superSecret" + + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("private") + + b := new(library.Build) + b.SetID(1) + b.SetRepoID(1) + b.SetNumber(1) + + mto := &token.MintTokenOpts{ + Hostname: "worker", + TokenDuration: time.Minute * 35, + TokenType: constants.WorkerBuildTokenType, + BuildID: 1, + Repo: "foo/bar", + } + + tok, _ := tm.MintToken(mto) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + + ctx := _context.TODO() + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteBuild(ctx, b) + db.DeleteRepo(_context.TODO(), r) + db.Close() + }() + + _, _ = db.CreateBuild(ctx, b) + _, _ = db.CreateRepo(_context.TODO(), r) + + context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar/builds/1", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", tok)) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(claims.Establish()) + engine.Use(user.Establish()) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(build.Establish()) + engine.Use(MustRead()) + engine.GET("/test/:org/:repo/builds/:build", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + s1 := httptest.NewServer(engine) + defer s1.Close() + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("MustRead returned %v, want %v", resp.Code, http.StatusOK) + } +} + func TestPerm_MustRead_RepoAdmin(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -900,12 +1899,18 @@ func TestPerm_MustRead_RepoAdmin(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(false) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -914,16 +1919,18 @@ func TestPerm_MustRead_RepoAdmin(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -945,8 +1952,10 @@ func TestPerm_MustRead_RepoAdmin(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -970,6 +1979,13 @@ func TestPerm_MustRead_RepoWrite(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -981,12 +1997,18 @@ func TestPerm_MustRead_RepoWrite(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(false) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -995,16 +2017,18 @@ func TestPerm_MustRead_RepoWrite(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -1026,8 +2050,10 @@ func TestPerm_MustRead_RepoWrite(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -1051,6 +2077,13 @@ func TestPerm_MustRead_RepoPublic(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -1062,12 +2095,18 @@ func TestPerm_MustRead_RepoPublic(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(false) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -1076,16 +2115,18 @@ func TestPerm_MustRead_RepoPublic(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -1107,8 +2148,10 @@ func TestPerm_MustRead_RepoPublic(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -1132,6 +2175,13 @@ func TestPerm_MustRead_NotRead(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + r := new(library.Repo) r.SetID(1) r.SetUserID(1) @@ -1143,12 +2193,18 @@ func TestPerm_MustRead_NotRead(t *testing.T) { u := new(library.User) u.SetID(1) - u.SetName("foo") + u.SetName("foob") u.SetToken("bar") u.SetHash("baz") u.SetAdmin(false) - tok, _ := token.CreateAccessToken(u, accessTokenDuration) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + tok, _ := tm.MintToken(mto) // setup context gin.SetMode(gin.TestMode) @@ -1157,16 +2213,18 @@ func TestPerm_MustRead_NotRead(t *testing.T) { context, engine := gin.CreateTestContext(resp) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(_context.TODO(), r) + db.DeleteUser(u) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(_context.TODO(), r) _ = db.CreateUser(u) context.Request, _ = http.NewRequest(http.MethodGet, "/test/foo/bar", nil) @@ -1188,8 +2246,10 @@ func TestPerm_MustRead_NotRead(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(user.Establish()) engine.Use(org.Establish()) engine.Use(repo.Establish()) @@ -1209,57 +2269,6 @@ func TestPerm_MustRead_NotRead(t *testing.T) { } } -func TestPerm_globalPerms(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - u.SetAdmin(false) - - // run test - got := globalPerms(u) - - if got { - t.Errorf("globalPerms returned %v, want false", got) - } -} - -func TestPerm_globalPerms_Agent(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("vela-worker") - u.SetToken("bar") - u.SetHash("baz") - u.SetAdmin(false) - - // run test - got := globalPerms(u) - - if !got { - t.Errorf("globalPerms returned %v, want true", got) - } -} - -func TestPerm_globalPerms_Admin(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - u.SetAdmin(true) - - // run test - got := globalPerms(u) - - if !got { - t.Errorf("globalPerms returned %v, want true", got) - } -} - const permAdminPayload = ` { "permission": "admin", @@ -1366,7 +2375,7 @@ const permNonePayload = ` const userPayload = ` { - "login": "foo", + "login": "foob", "id": 1, "node_id": "MDQ6VXNlcjE=", "avatar_url": "https://github.com/images/error/octocat_happy.gif", diff --git a/router/middleware/pipeline/context.go b/router/middleware/pipeline/context.go new file mode 100644 index 000000000..6aab4037e --- /dev/null +++ b/router/middleware/pipeline/context.go @@ -0,0 +1,39 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + + "github.com/go-vela/types/library" +) + +const key = "pipeline" + +// Setter defines a context that enables setting values. +type Setter interface { + Set(string, interface{}) +} + +// FromContext returns the Pipeline associated with this context. +func FromContext(c context.Context) *library.Pipeline { + value := c.Value(key) + if value == nil { + return nil + } + + b, ok := value.(*library.Pipeline) + if !ok { + return nil + } + + return b +} + +// ToContext adds the Pipeline to this context if it supports +// the Setter interface. +func ToContext(c Setter, b *library.Pipeline) { + c.Set(key, b) +} diff --git a/router/middleware/pipeline/context_test.go b/router/middleware/pipeline/context_test.go new file mode 100644 index 000000000..ddc5b8bbd --- /dev/null +++ b/router/middleware/pipeline/context_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "reflect" + "testing" + + "github.com/gin-gonic/gin" + "github.com/go-vela/types/library" +) + +func TestPipeline_FromContext(t *testing.T) { + // setup types + _pipeline := new(library.Pipeline) + + gin.SetMode(gin.TestMode) + _context, _ := gin.CreateTestContext(nil) + _context.Set(key, _pipeline) + + _emptyContext, _ := gin.CreateTestContext(nil) + + _nilContext, _ := gin.CreateTestContext(nil) + _nilContext.Set(key, nil) + + _typeContext, _ := gin.CreateTestContext(nil) + _typeContext.Set(key, 1) + + // setup tests + tests := []struct { + name string + context *gin.Context + want *library.Pipeline + }{ + { + name: "context", + context: _context, + want: _pipeline, + }, + { + name: "context with no value", + context: _emptyContext, + want: nil, + }, + { + name: "context with nil value", + context: _nilContext, + want: nil, + }, + { + name: "context with wrong value type", + context: _typeContext, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := FromContext(test.context) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("FromContext for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +func TestPipeline_ToContext(t *testing.T) { + // setup types + _pipeline := new(library.Pipeline) + + gin.SetMode(gin.TestMode) + _context, _ := gin.CreateTestContext(nil) + + // setup tests + tests := []struct { + name string + context *gin.Context + want *library.Pipeline + }{ + { + name: "context", + context: _context, + want: _pipeline, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ToContext(test.context, test.want) + + got := test.context.Value(key) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ToContext for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/router/middleware/pipeline/doc.go b/router/middleware/pipeline/doc.go new file mode 100644 index 000000000..e6f19504d --- /dev/null +++ b/router/middleware/pipeline/doc.go @@ -0,0 +1,12 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// Package pipeline provides the ability for inserting +// Vela pipeline resources into or extracting Vela pipeline +// resources from the middleware chain for the API. +// +// Usage: +// +// import "github.com/go-vela/server/router/middleware/pipeline" +package pipeline diff --git a/router/middleware/pipeline/pipeline.go b/router/middleware/pipeline/pipeline.go new file mode 100644 index 000000000..b3f2e57af --- /dev/null +++ b/router/middleware/pipeline/pipeline.go @@ -0,0 +1,99 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/compiler" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/util" + "github.com/go-vela/types" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// Retrieve gets the pipeline in the given context. +func Retrieve(c *gin.Context) *library.Pipeline { + return FromContext(c) +} + +// Establish sets the pipeline in the given context. +func Establish() gin.HandlerFunc { + return func(c *gin.Context) { + o := org.Retrieve(c) + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + if r == nil { + retErr := fmt.Errorf("repo %s/%s not found", util.PathParameter(c, "org"), util.PathParameter(c, "repo")) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + p := util.PathParameter(c, "pipeline") + if len(p) == 0 { + retErr := fmt.Errorf("no pipeline parameter provided") + + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + entry := fmt.Sprintf("%s/%s", r.GetFullName(), p) + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": o, + "pipeline": p, + "repo": r.GetName(), + "user": u.GetName(), + }).Debugf("reading pipeline %s", entry) + + pipeline, err := database.FromContext(c).GetPipelineForRepo(ctx, p, r) + if err != nil { // assume the pipeline doesn't exist in the database yet (before pipeline support was added) + // send API call to capture the pipeline configuration file + config, err := scm.FromContext(c).ConfigBackoff(u, r, p) + if err != nil { + retErr := fmt.Errorf("unable to get pipeline configuration for %s: %w", entry, err) + + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + // parse and compile the pipeline configuration file + _, pipeline, err = compiler.FromContext(c). + Duplicate(). + WithCommit(p). + WithMetadata(c.MustGet("metadata").(*types.Metadata)). + WithRepo(r). + WithUser(u). + Compile(config) + if err != nil { + retErr := fmt.Errorf("unable to compile pipeline configuration for %s: %w", entry, err) + + util.HandleError(c, http.StatusInternalServerError, retErr) + + return + } + } + + ToContext(c, pipeline) + + c.Next() + } +} diff --git a/router/middleware/pipeline/pipeline_test.go b/router/middleware/pipeline/pipeline_test.go new file mode 100644 index 000000000..0fc1e8b29 --- /dev/null +++ b/router/middleware/pipeline/pipeline_test.go @@ -0,0 +1,343 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package pipeline + +import ( + "context" + "flag" + "fmt" + "net/http" + "net/http/httptest" + "reflect" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/compiler" + "github.com/go-vela/server/compiler/native" + "github.com/go-vela/server/database" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/claims" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/scm" + "github.com/go-vela/server/scm/github" + "github.com/go-vela/types" + "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" + "github.com/golang-jwt/jwt/v5" + "github.com/urfave/cli/v2" +) + +func TestPipeline_Retrieve(t *testing.T) { + // setup types + _pipeline := new(library.Pipeline) + + gin.SetMode(gin.TestMode) + _context, _ := gin.CreateTestContext(nil) + + // setup tests + tests := []struct { + name string + context *gin.Context + want *library.Pipeline + }{ + { + name: "context", + context: _context, + want: _pipeline, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ToContext(test.context, test.want) + + got := Retrieve(test.context) + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("Retrieve for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +func TestPipeline_Establish(t *testing.T) { + // setup types + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + want := new(library.Pipeline) + want.SetID(1) + want.SetRepoID(1) + want.SetCommit("48afb5bdc41ad69bf22588491333f7cf71135163") + want.SetFlavor("") + want.SetPlatform("") + want.SetRef("refs/heads/master") + want.SetType("yaml") + want.SetVersion("1") + want.SetExternalSecrets(false) + want.SetInternalSecrets(false) + want.SetServices(false) + want.SetStages(false) + want.SetSteps(false) + want.SetTemplates(false) + want.SetData([]byte{}) + + got := new(library.Pipeline) + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeletePipeline(context.TODO(), want) + db.DeleteRepo(context.TODO(), r) + db.Close() + }() + + _, _ = db.CreateRepo(context.TODO(), r) + _, _ = db.CreatePipeline(context.TODO(), want) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/pipelines/foo/bar/48afb5bdc41ad69bf22588491333f7cf71135163", nil) + + // setup mock server + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(Establish()) + engine.GET("/pipelines/:org/:repo/:pipeline", func(c *gin.Context) { + got = Retrieve(c) + + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusOK) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("Establish is %v, want %v", got, want) + } +} + +func TestPipeline_Establish_NoRepo(t *testing.T) { + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/pipelines/foo/bar/48afb5bdc41ad69bf22588491333f7cf71135163", nil) + + // setup mock server + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(Establish()) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusNotFound { + t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusNotFound) + } +} + +func TestPipeline_Establish_NoPipelineParameter(t *testing.T) { + // setup types + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteRepo(context.TODO(), r) + db.Close() + }() + + _, _ = db.CreateRepo(context.TODO(), r) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/pipelines/foo/bar", nil) + + // setup mock server + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(Establish()) + engine.GET("/pipelines/:org/:repo", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusBadRequest { + t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusBadRequest) + } +} + +func TestPipeline_Establish_NoPipeline(t *testing.T) { + // setup types + secret := "superSecret" + + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + r := new(library.Repo) + r.SetID(1) + r.SetUserID(1) + r.SetHash("baz") + r.SetOrg("foo") + r.SetName("bar") + r.SetFullName("foo/bar") + r.SetVisibility("public") + + u := new(library.User) + u.SetID(1) + u.SetName("foo") + u.SetToken("bar") + u.SetHash("baz") + u.SetAdmin(true) + + m := &types.Metadata{ + Database: &types.Database{ + Driver: "foo", + Host: "foo", + }, + Queue: &types.Queue{ + Channel: "foo", + Driver: "foo", + Host: "foo", + }, + Source: &types.Source{ + Driver: "foo", + Host: "foo", + }, + Vela: &types.Vela{ + Address: "foo", + WebAddress: "foo", + }, + } + + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + at, err := tm.MintToken(mto) + if err != nil { + t.Errorf("unable to mint user access token: %v", err) + } + + set := flag.NewFlagSet("test", 0) + set.String("clone-image", "target/vela-git:latest", "doc") + + comp, err := native.New(cli.NewContext(nil, set, nil)) + if err != nil { + t.Errorf("unable to create compiler: %v", err) + } + + // setup database + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + + defer func() { + db.DeleteRepo(context.TODO(), r) + db.DeleteUser(u) + db.Close() + }() + + _, _ = db.CreateRepo(context.TODO(), r) + _ = db.CreateUser(u) + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/pipelines/foo/bar/148afb5bdc41ad69bf22588491333f7cf71135163", nil) + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", at)) + + // setup github mock server + engine.GET("/api/v3/repos/:org/:repo/contents/:path", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/yml.json") + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup client + client, _ := github.NewTest(s.URL) + + // setup vela mock server + engine.Use(func(c *gin.Context) { c.Set("metadata", m) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { compiler.WithGinContext(c, comp) }) + engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) + engine.Use(org.Establish()) + engine.Use(repo.Establish()) + engine.Use(user.Establish()) + engine.Use(Establish()) + engine.GET("/pipelines/:org/:repo/:pipeline", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusOK) + } +} diff --git a/router/middleware/pipeline/testdata/yml.json b/router/middleware/pipeline/testdata/yml.json new file mode 100644 index 000000000..458ecaa44 --- /dev/null +++ b/router/middleware/pipeline/testdata/yml.json @@ -0,0 +1,18 @@ +{ + "type": "file", + "encoding": "base64", + "size": 5362, + "name": ".vela.yml", + "path": ".vela.yml", + "content": "LS0tCnZlcnNpb246ICIxIgoKbWV0YWRhdGE6CiAgb3M6IGxpbnV4CgpzdGVwczoKICAtIG5hbWU6IGJ1aWxkCiAgICBpbWFnZTogb3BlbmpkazpsYXRlc3QKICAgIHB1bGw6IHRydWUKICAgIGVudmlyb25tZW50OgogICAgICBHUkFETEVfVVNFUl9IT01FOiAuZ3JhZGxlCiAgICAgIEdSQURMRV9PUFRTOiAtRG9yZy5ncmFkbGUuZGFlbW9uPWZhbHNlIC1Eb3JnLmdyYWRsZS53b3JrZXJzLm1heD0xIC1Eb3JnLmdyYWRsZS5wYXJhbGxlbD1mYWxzZQogICAgY29tbWFuZHM6CiAgICAgIC0gLi9ncmFkbGV3IGJ1aWxkIGRpc3RUYXIK", + "sha": "3d21ec53a331a6f037a91c368710b99387d012c1", + "url": "https://api.github.com/repos/octokit/octokit.rb/contents/.vela.yml", + "git_url": "https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1", + "html_url": "https://github.com/octokit/octokit.rb/blob/master/.vela.yml", + "download_url": "https://raw.githubusercontent.com/octokit/octokit.rb/master/.vela.yml", + "_links": { + "git": "https://api.github.com/repos/octokit/octokit.rb/git/blobs/3d21ec53a331a6f037a91c368710b99387d012c1", + "self": "https://api.github.com/repos/octokit/octokit.rb/contents/.vela.yml", + "html": "https://github.com/octokit/octokit.rb/blob/master/.vela.yml" + } +} \ No newline at end of file diff --git a/router/middleware/queue_test.go b/router/middleware/queue_test.go index cfbd94010..80c22ce07 100644 --- a/router/middleware/queue_test.go +++ b/router/middleware/queue_test.go @@ -20,7 +20,8 @@ func TestMiddleware_Queue(t *testing.T) { // setup types var got queue.Service - want, _ := redis.NewTest() + // signing keys are irrelevant here + want, _ := redis.NewTest("", "") // setup context gin.SetMode(gin.TestMode) diff --git a/router/middleware/repo/doc.go b/router/middleware/repo/doc.go index 6b6a33c97..028ebe70b 100644 --- a/router/middleware/repo/doc.go +++ b/router/middleware/repo/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/router/middleware/repo" +// import "github.com/go-vela/server/router/middleware/repo" package repo diff --git a/router/middleware/repo/repo.go b/router/middleware/repo/repo.go index 8afa16813..fcc898f9e 100644 --- a/router/middleware/repo/repo.go +++ b/router/middleware/repo/repo.go @@ -5,18 +5,16 @@ package repo import ( - "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/router/middleware/user" - "github.com/go-vela/types/library" - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database" - "github.com/go-vela/server/util" - "fmt" "net/http" "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" ) // Retrieve gets the repo in the given context. @@ -29,11 +27,13 @@ func Establish() gin.HandlerFunc { return func(c *gin.Context) { o := org.Retrieve(c) u := user.Retrieve(c) + ctx := c.Request.Context() - rParam := c.Param("repo") + rParam := util.PathParameter(c, "repo") if len(rParam) == 0 { retErr := fmt.Errorf("no repo parameter provided") util.HandleError(c, http.StatusBadRequest, retErr) + return } @@ -46,10 +46,11 @@ func Establish() gin.HandlerFunc { "user": u.GetName(), }).Debugf("reading repo %s/%s", o, rParam) - r, err := database.FromContext(c).GetRepo(o, rParam) + r, err := database.FromContext(c).GetRepoForOrg(ctx, o, rParam) if err != nil { - retErr := fmt.Errorf("unable to read repo %s/%s: %v", o, rParam, err) + retErr := fmt.Errorf("unable to read repo %s/%s: %w", o, rParam, err) util.HandleError(c, http.StatusNotFound, retErr) + return } diff --git a/router/middleware/repo/repo_test.go b/router/middleware/repo/repo_test.go index 63411640c..0274db0ec 100644 --- a/router/middleware/repo/repo_test.go +++ b/router/middleware/repo/repo_test.go @@ -5,6 +5,7 @@ package repo import ( + "context" "net/http" "net/http/httptest" "reflect" @@ -14,7 +15,6 @@ import ( "github.com/gin-gonic/gin" "github.com/go-vela/server/database" - "github.com/go-vela/server/database/sqlite" "github.com/go-vela/types/library" ) @@ -48,6 +48,7 @@ func TestRepo_Establish(t *testing.T) { want.SetLink("") want.SetClone("") want.SetBranch("") + want.SetTopics([]string{}) want.SetBuildLimit(0) want.SetTimeout(0) want.SetCounter(0) @@ -66,15 +67,17 @@ func TestRepo_Establish(t *testing.T) { got := new(library.Repo) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(context.TODO(), want) + db.Close() }() - _ = db.CreateRepo(want) + _, _ = db.CreateRepo(context.TODO(), want) // setup context gin.SetMode(gin.TestMode) @@ -107,8 +110,11 @@ func TestRepo_Establish(t *testing.T) { func TestRepo_Establish_NoOrgParameter(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // setup context gin.SetMode(gin.TestMode) @@ -134,8 +140,11 @@ func TestRepo_Establish_NoOrgParameter(t *testing.T) { func TestRepo_Establish_NoRepoParameter(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // setup context gin.SetMode(gin.TestMode) @@ -161,8 +170,11 @@ func TestRepo_Establish_NoRepoParameter(t *testing.T) { func TestRepo_Establish_NoRepo(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // setup context gin.SetMode(gin.TestMode) diff --git a/router/middleware/schedule/context.go b/router/middleware/schedule/context.go new file mode 100644 index 000000000..7ce62871c --- /dev/null +++ b/router/middleware/schedule/context.go @@ -0,0 +1,39 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "context" + + "github.com/go-vela/types/library" +) + +const key = "schedule" + +// Setter defines a context that enables setting values. +type Setter interface { + Set(string, interface{}) +} + +// FromContext returns the Schedule associated with this context. +func FromContext(c context.Context) *library.Schedule { + value := c.Value(key) + if value == nil { + return nil + } + + s, ok := value.(*library.Schedule) + if !ok { + return nil + } + + return s +} + +// ToContext adds the Schedule to this context if it supports +// the Setter interface. +func ToContext(c Setter, s *library.Schedule) { + c.Set(key, s) +} diff --git a/router/middleware/schedule/context_test.go b/router/middleware/schedule/context_test.go new file mode 100644 index 000000000..fb73d0b32 --- /dev/null +++ b/router/middleware/schedule/context_test.go @@ -0,0 +1,89 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "testing" + + "github.com/gin-gonic/gin" + "github.com/go-vela/types/library" +) + +func TestSchedule_FromContext(t *testing.T) { + // setup types + num := int64(1) + want := &library.Schedule{ID: &num} + + // setup context + gin.SetMode(gin.TestMode) + context, _ := gin.CreateTestContext(nil) + context.Set(key, want) + + // run test + got := FromContext(context) + + if got != want { + t.Errorf("FromContext is %v, want %v", got, want) + } +} + +func TestSchedule_FromContext_Bad(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + context, _ := gin.CreateTestContext(nil) + context.Set(key, nil) + + // run test + got := FromContext(context) + + if got != nil { + t.Errorf("FromContext is %v, want nil", got) + } +} + +func TestSchedule_FromContext_WrongType(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + context, _ := gin.CreateTestContext(nil) + context.Set(key, 1) + + // run test + got := FromContext(context) + + if got != nil { + t.Errorf("FromContext is %v, want nil", got) + } +} + +func TestSchedule_FromContext_Empty(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + context, _ := gin.CreateTestContext(nil) + + // run test + got := FromContext(context) + + if got != nil { + t.Errorf("FromContext is %v, want nil", got) + } +} + +func TestSchedule_ToContext(t *testing.T) { + // setup types + num := int64(1) + want := &library.Schedule{ID: &num} + + // setup context + gin.SetMode(gin.TestMode) + context, _ := gin.CreateTestContext(nil) + ToContext(context, want) + + // run test + got := context.Value(key) + + if got != want { + t.Errorf("ToContext is %v, want %v", got, want) + } +} diff --git a/router/middleware/schedule/schedule.go b/router/middleware/schedule/schedule.go new file mode 100644 index 000000000..65f78c4e0 --- /dev/null +++ b/router/middleware/schedule/schedule.go @@ -0,0 +1,60 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package schedule + +import ( + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// Retrieve gets the schedule in the given context. +func Retrieve(c *gin.Context) *library.Schedule { + return FromContext(c) +} + +// Establish sets the schedule in the given context. +func Establish() gin.HandlerFunc { + return func(c *gin.Context) { + r := repo.Retrieve(c) + u := user.Retrieve(c) + ctx := c.Request.Context() + + sParam := util.PathParameter(c, "schedule") + if len(sParam) == 0 { + retErr := fmt.Errorf("no schedule parameter provided") + util.HandleError(c, http.StatusBadRequest, retErr) + + return + } + + // update engine logger with API metadata + // + // https://pkg.go.dev/github.com/sirupsen/logrus?tab=doc#Entry.WithFields + logrus.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + "user": u.GetName(), + }).Debugf("reading schedule %s for repo %s", sParam, r.GetFullName()) + + s, err := database.FromContext(c).GetScheduleForRepo(ctx, r, sParam) + if err != nil { + retErr := fmt.Errorf("unable to read schedule %s for repo %s: %w", sParam, r.GetFullName(), err) + util.HandleError(c, http.StatusNotFound, retErr) + + return + } + + ToContext(c, s) + c.Next() + } +} diff --git a/router/middleware/schedule_frequency.go b/router/middleware/schedule_frequency.go new file mode 100644 index 000000000..243c2ad06 --- /dev/null +++ b/router/middleware/schedule_frequency.go @@ -0,0 +1,20 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package middleware + +import ( + "time" + + "github.com/gin-gonic/gin" +) + +// ScheduleFrequency is a middleware function that attaches the scheduleminimumfrequency used +// to limit the frequency which schedules can be run within the system. +func ScheduleFrequency(scheduleFrequency time.Duration) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("scheduleminimumfrequency", scheduleFrequency) + c.Next() + } +} diff --git a/router/middleware/schedule_frequency_test.go b/router/middleware/schedule_frequency_test.go new file mode 100644 index 000000000..171f96ad3 --- /dev/null +++ b/router/middleware/schedule_frequency_test.go @@ -0,0 +1,47 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package middleware + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" + "time" + + "github.com/gin-gonic/gin" +) + +func TestMiddleware_ScheduleFrequency(t *testing.T) { + // setup types + var got time.Duration + want := 30 * time.Minute + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/health", nil) + + // setup mock server + engine.Use(ScheduleFrequency(want)) + engine.GET("/health", func(c *gin.Context) { + got = c.Value("scheduleminimumfrequency").(time.Duration) + + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("ScheduleFrequency returned %v, want %v", resp.Code, http.StatusOK) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("ScheduleFrequency is %v, want %v", got, want) + } +} diff --git a/router/middleware/secret.go b/router/middleware/secret.go index 90f497492..66f28af78 100644 --- a/router/middleware/secret.go +++ b/router/middleware/secret.go @@ -26,6 +26,7 @@ func Secrets(secrets map[string]secret.Service) gin.HandlerFunc { for k, v := range secrets { secret.ToContext(c, k, v) } + c.Next() } } diff --git a/router/middleware/secret_test.go b/router/middleware/secret_test.go index ae6cb623b..9ca21fd24 100644 --- a/router/middleware/secret_test.go +++ b/router/middleware/secret_test.go @@ -10,7 +10,7 @@ import ( "reflect" "testing" - "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/database" "github.com/go-vela/server/secret" "github.com/go-vela/server/secret/native" @@ -51,13 +51,16 @@ func TestMiddleware_Secret(t *testing.T) { func TestMiddleware_Secrets(t *testing.T) { // setup types - d, _ := sqlite.NewTest() - defer func() { _sql, _ := d.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() var got secret.Service want, _ := native.New( - native.WithDatabase(d), + native.WithDatabase(db), ) s := map[string]secret.Service{"native": want} diff --git a/router/middleware/secure_cookie_test.go b/router/middleware/secure_cookie_test.go index 4681cb3ca..60fa73eb4 100644 --- a/router/middleware/secure_cookie_test.go +++ b/router/middleware/secure_cookie_test.go @@ -18,6 +18,7 @@ func TestCookie_SecureCookie(t *testing.T) { type args struct { secure bool } + tests := []struct { name string args args @@ -38,6 +39,7 @@ func TestCookie_SecureCookie(t *testing.T) { want: true, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // setup context diff --git a/router/middleware/service/doc.go b/router/middleware/service/doc.go index 2d185d23e..698d41aaa 100644 --- a/router/middleware/service/doc.go +++ b/router/middleware/service/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/router/middleware/service" +// import "github.com/go-vela/server/router/middleware/service" package service diff --git a/router/middleware/service/service.go b/router/middleware/service/service.go index 4e557345f..30abf92d0 100644 --- a/router/middleware/service/service.go +++ b/router/middleware/service/service.go @@ -9,16 +9,14 @@ import ( "net/http" "strconv" - "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/router/middleware/user" - + "github.com/gin-gonic/gin" "github.com/go-vela/server/database" "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" "github.com/go-vela/server/util" "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) @@ -37,21 +35,24 @@ func Establish() gin.HandlerFunc { u := user.Retrieve(c) if r == nil { - retErr := fmt.Errorf("repo %s/%s not found", o, c.Param("repo")) + retErr := fmt.Errorf("repo %s/%s not found", o, util.PathParameter(c, "repo")) util.HandleError(c, http.StatusNotFound, retErr) + return } if b == nil { - retErr := fmt.Errorf("build %s not found for repo %s", c.Param("build"), r.GetFullName()) + retErr := fmt.Errorf("build %s not found for repo %s", util.PathParameter(c, "build"), r.GetFullName()) util.HandleError(c, http.StatusNotFound, retErr) + return } - sParam := c.Param("service") + sParam := util.PathParameter(c, "service") if len(sParam) == 0 { retErr := fmt.Errorf("no service parameter provided") util.HandleError(c, http.StatusBadRequest, retErr) + return } @@ -59,6 +60,7 @@ func Establish() gin.HandlerFunc { if err != nil { retErr := fmt.Errorf("malformed service parameter provided: %s", sParam) util.HandleError(c, http.StatusBadRequest, retErr) + return } @@ -73,11 +75,11 @@ func Establish() gin.HandlerFunc { "user": u.GetName(), }).Debugf("reading service %s/%d/%d", r.GetFullName(), b.GetNumber(), number) - s, err := database.FromContext(c).GetService(number, b) + s, err := database.FromContext(c).GetServiceForBuild(b, number) if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to read service %s/%d/%d: %v", r.GetFullName(), b.GetNumber(), number, err) + retErr := fmt.Errorf("unable to read service %s/%d/%d: %w", r.GetFullName(), b.GetNumber(), number, err) util.HandleError(c, http.StatusNotFound, retErr) + return } diff --git a/router/middleware/service/service_test.go b/router/middleware/service/service_test.go index 3b324d9c4..70a8024c3 100644 --- a/router/middleware/service/service_test.go +++ b/router/middleware/service/service_test.go @@ -5,17 +5,16 @@ package service import ( + "context" "net/http" "net/http/httptest" "reflect" "testing" - "github.com/go-vela/server/router/middleware/org" - "github.com/gin-gonic/gin" "github.com/go-vela/server/database" - "github.com/go-vela/server/database/sqlite" "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" "github.com/go-vela/types/library" ) @@ -74,19 +73,21 @@ func TestService_Establish(t *testing.T) { got := new(library.Service) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from builds;") - db.Sqlite.Exec("delete from services;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteBuild(context.TODO(), b) + db.DeleteRepo(context.TODO(), r) + db.DeleteService(want) + db.Close() }() - _ = db.CreateRepo(r) - _ = db.CreateBuild(b) - _ = db.CreateService(want) + _, _ = db.CreateRepo(context.TODO(), r) + _, _ = db.CreateBuild(context.TODO(), b) + _, _ = db.CreateService(want) // setup context gin.SetMode(gin.TestMode) @@ -121,8 +122,11 @@ func TestService_Establish(t *testing.T) { func TestService_Establish_NoRepo(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // setup context gin.SetMode(gin.TestMode) @@ -158,15 +162,17 @@ func TestService_Establish_NoBuild(t *testing.T) { r.SetVisibility("public") // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(context.TODO(), r) // setup context gin.SetMode(gin.TestMode) @@ -209,17 +215,19 @@ func TestService_Establish_NoServiceParameter(t *testing.T) { b.SetNumber(1) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from builds;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteBuild(context.TODO(), b) + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) - _ = db.CreateBuild(b) + _, _ = db.CreateRepo(context.TODO(), r) + _, _ = db.CreateBuild(context.TODO(), b) // setup context gin.SetMode(gin.TestMode) @@ -263,17 +271,19 @@ func TestService_Establish_InvalidServiceParameter(t *testing.T) { b.SetNumber(1) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from builds;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteBuild(context.TODO(), b) + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) - _ = db.CreateBuild(b) + _, _ = db.CreateRepo(context.TODO(), r) + _, _ = db.CreateBuild(context.TODO(), b) // setup context gin.SetMode(gin.TestMode) @@ -317,17 +327,19 @@ func TestService_Establish_NoService(t *testing.T) { b.SetNumber(1) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from builds;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteBuild(context.TODO(), b) + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) - _ = db.CreateBuild(b) + _, _ = db.CreateRepo(context.TODO(), r) + _, _ = db.CreateBuild(context.TODO(), b) // setup context gin.SetMode(gin.TestMode) diff --git a/router/middleware/signing.go b/router/middleware/signing.go new file mode 100644 index 000000000..05c63b8e5 --- /dev/null +++ b/router/middleware/signing.go @@ -0,0 +1,18 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package middleware + +import ( + "github.com/gin-gonic/gin" +) + +// QueueSigningPrivateKey is a middleware function that attaches the private key used +// to sign items that are pushed to the queue. +func QueueSigningPrivateKey(key string) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("queue.private-key", key) + c.Next() + } +} diff --git a/router/middleware/step/doc.go b/router/middleware/step/doc.go index e1e0b26ba..45ddd7e61 100644 --- a/router/middleware/step/doc.go +++ b/router/middleware/step/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/router/middleware/step" +// import "github.com/go-vela/server/router/middleware/step" package step diff --git a/router/middleware/step/step.go b/router/middleware/step/step.go index 3ae6409ee..27d517efe 100644 --- a/router/middleware/step/step.go +++ b/router/middleware/step/step.go @@ -9,16 +9,14 @@ import ( "net/http" "strconv" - "github.com/go-vela/server/router/middleware/org" - "github.com/go-vela/server/router/middleware/user" - + "github.com/gin-gonic/gin" "github.com/go-vela/server/database" "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" + "github.com/go-vela/server/router/middleware/user" "github.com/go-vela/server/util" "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) @@ -37,21 +35,24 @@ func Establish() gin.HandlerFunc { u := user.Retrieve(c) if r == nil { - retErr := fmt.Errorf("repo %s/%s not found", o, c.Param("repo")) + retErr := fmt.Errorf("repo %s/%s not found", o, util.PathParameter(c, "repo")) util.HandleError(c, http.StatusNotFound, retErr) + return } if b == nil { - retErr := fmt.Errorf("build %s not found for repo %s", c.Param("build"), r.GetFullName()) + retErr := fmt.Errorf("build %s not found for repo %s", util.PathParameter(c, "build"), r.GetFullName()) util.HandleError(c, http.StatusNotFound, retErr) + return } - sParam := c.Param("step") + sParam := util.PathParameter(c, "step") if len(sParam) == 0 { retErr := fmt.Errorf("no step parameter provided") util.HandleError(c, http.StatusBadRequest, retErr) + return } @@ -59,6 +60,7 @@ func Establish() gin.HandlerFunc { if err != nil { retErr := fmt.Errorf("malformed step parameter provided: %s", sParam) util.HandleError(c, http.StatusBadRequest, retErr) + return } @@ -73,11 +75,11 @@ func Establish() gin.HandlerFunc { "user": u.GetName(), }).Debugf("reading step %s/%d/%d", r.GetFullName(), b.GetNumber(), number) - s, err := database.FromContext(c).GetStep(number, b) + s, err := database.FromContext(c).GetStepForBuild(b, number) if err != nil { - // nolint: lll // ignore long line length due to error message - retErr := fmt.Errorf("unable to read step %s/%d/%d: %v", r.GetFullName(), b.GetNumber(), number, err) + retErr := fmt.Errorf("unable to read step %s/%d/%d: %w", r.GetFullName(), b.GetNumber(), number, err) util.HandleError(c, http.StatusNotFound, retErr) + return } diff --git a/router/middleware/step/step_test.go b/router/middleware/step/step_test.go index e0367c887..9ca2c8faf 100644 --- a/router/middleware/step/step_test.go +++ b/router/middleware/step/step_test.go @@ -5,17 +5,16 @@ package step import ( + "context" "net/http" "net/http/httptest" "reflect" "testing" - "github.com/go-vela/server/router/middleware/org" - "github.com/gin-gonic/gin" "github.com/go-vela/server/database" - "github.com/go-vela/server/database/sqlite" "github.com/go-vela/server/router/middleware/build" + "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" "github.com/go-vela/types/library" ) @@ -76,19 +75,21 @@ func TestStep_Establish(t *testing.T) { got := new(library.Step) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from builds;") - db.Sqlite.Exec("delete from steps;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteBuild(context.TODO(), b) + db.DeleteRepo(context.TODO(), r) + db.DeleteStep(want) + db.Close() }() - _ = db.CreateRepo(r) - _ = db.CreateBuild(b) - _ = db.CreateStep(want) + _, _ = db.CreateRepo(context.TODO(), r) + _, _ = db.CreateBuild(context.TODO(), b) + _, _ = db.CreateStep(want) // setup context gin.SetMode(gin.TestMode) @@ -123,8 +124,11 @@ func TestStep_Establish(t *testing.T) { func TestStep_Establish_NoRepo(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // setup context gin.SetMode(gin.TestMode) @@ -160,15 +164,17 @@ func TestStep_Establish_NoBuild(t *testing.T) { r.SetVisibility("public") // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) + _, _ = db.CreateRepo(context.TODO(), r) // setup context gin.SetMode(gin.TestMode) @@ -211,17 +217,19 @@ func TestStep_Establish_NoStepParameter(t *testing.T) { b.SetNumber(1) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from builds;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteBuild(context.TODO(), b) + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) - _ = db.CreateBuild(b) + _, _ = db.CreateRepo(context.TODO(), r) + _, _ = db.CreateBuild(context.TODO(), b) // setup context gin.SetMode(gin.TestMode) @@ -265,17 +273,19 @@ func TestStep_Establish_InvalidStepParameter(t *testing.T) { b.SetNumber(1) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from builds;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteBuild(context.TODO(), b) + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) - _ = db.CreateBuild(b) + _, _ = db.CreateRepo(context.TODO(), r) + _, _ = db.CreateBuild(context.TODO(), b) // setup context gin.SetMode(gin.TestMode) @@ -319,17 +329,19 @@ func TestStep_Establish_NoStep(t *testing.T) { b.SetNumber(1) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from repos;") - db.Sqlite.Exec("delete from builds;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteBuild(context.TODO(), b) + db.DeleteRepo(context.TODO(), r) + db.Close() }() - _ = db.CreateRepo(r) - _ = db.CreateBuild(b) + _, _ = db.CreateRepo(context.TODO(), r) + _, _ = db.CreateBuild(context.TODO(), b) // setup context gin.SetMode(gin.TestMode) diff --git a/router/middleware/token/token.go b/router/middleware/token/token.go deleted file mode 100644 index 51e9a2e07..000000000 --- a/router/middleware/token/token.go +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package token - -import ( - "errors" - "fmt" - "net/http" - "net/url" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-vela/server/database" - "github.com/go-vela/types" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - - "github.com/golang-jwt/jwt/v4" - "github.com/golang-jwt/jwt/v4/request" - "github.com/sirupsen/logrus" -) - -type Claims struct { - IsAdmin bool `json:"is_admin"` - IsActive bool `json:"is_active"` - jwt.StandardClaims -} - -// Compose generates an refresh and access token pair unique -// to the provided user and sets a secure cookie. -// It uses a secret hash, which is unique for every user. -// The hash signs the token to guarantee the signature is unique -// per token. The refresh token is returned to store with the user -// in the database. -// nolint:lll // reference links cause long lines -func Compose(c *gin.Context, u *library.User) (string, string, error) { - // grab the metadata from the context to pull in provided - // cookie duration information - m := c.MustGet("metadata").(*types.Metadata) - - // create a refresh with the provided duration - refreshToken, refreshExpiry, err := CreateRefreshToken(u, m.Vela.RefreshTokenDuration) - if err != nil { - return "", "", err - } - - // create an access token with the provided duration - accessToken, err := CreateAccessToken(u, m.Vela.AccessTokenDuration) - if err != nil { - return "", "", err - } - - // parse the address for the backend server - // so we can set it for the cookie domain - addr, err := url.Parse(m.Vela.Address) - if err != nil { - return "", "", err - } - - // set the SameSite value for the cookie - // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#samesite-attribute - // We set to Lax because we will have links from source provider web UI. - // Setting this to Strict would force a login when navigating via source provider web UI links. - c.SetSameSite(http.SameSiteLaxMode) - // set the cookie with the refresh token as a HttpOnly, Secure cookie - // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#httponly-attribute - // https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#secure-attribute - c.SetCookie(constants.RefreshTokenName, refreshToken, refreshExpiry, "/", addr.Hostname(), c.Value("securecookie").(bool), true) - - // return the refresh and access tokens - return refreshToken, accessToken, nil -} - -// Parse scans the signed JWT token as a string and extracts -// the user login from the claims to be looked up in the database. -// This function will return an error for a few different reasons: -// -// * the token signature doesn't match what is expected -// * the token signing method doesn't match what is expected -// * the token is invalid (potentially expired or improper). -func Parse(t string, db database.Service) (*library.User, error) { - u := new(library.User) - - // create a new JWT parser - p := &jwt.Parser{ - // explicitly only allow these signing methods - ValidMethods: []string{jwt.SigningMethodHS256.Name}, - } - - // parse the signed JWT token string - // parse also validates the claims and token by default. - _, err := p.ParseWithClaims(t, &Claims{}, func(token *jwt.Token) (interface{}, error) { - var err error - - // extract the claims from the token - claims := token.Claims.(*Claims) - name := claims.Subject - - // check if subject has a value in claims; - // we can save a db lookup attempt - if len(name) == 0 { - return nil, errors.New("no subject defined") - } - - // ParseWithClaims will skip expiration check - // if expiration has default value; - // forcing a check and exiting if not set - if claims.ExpiresAt == 0 { - return nil, errors.New("token has no expiration") - } - - // lookup the user in the database - logrus.WithField("user", name).Debugf("reading user %s", name) - u, err = db.GetUserName(name) - return []byte(u.GetHash()), err - }) - - // there will be an error if we're not able to parse - // the token, eg. due to expiration, invalid signature, etc - if err != nil { - return nil, fmt.Errorf("invalid token provided for %s: %w", u.GetName(), err) - } - - return u, nil -} - -// RetrieveAccessToken gets the passed in access token from the header in the request. -func RetrieveAccessToken(r *http.Request) (accessToken string, err error) { - accessToken, err = request.AuthorizationHeaderExtractor.ExtractToken(r) - - return -} - -// RetrieveRefreshToken gets the refresh token sent along with the request as a cookie. -func RetrieveRefreshToken(r *http.Request) (string, error) { - refreshToken, err := r.Cookie(constants.RefreshTokenName) - - if refreshToken == nil || len(refreshToken.Value) == 0 { - // cookie will not be sent if it has expired - return "", fmt.Errorf("refresh token expired or not provided") - } - - return refreshToken.Value, err -} - -// CreateAccessToken creates a new access token for the given user and duration. -func CreateAccessToken(u *library.User, d time.Duration) (string, error) { - now := time.Now() - exp := now.Add(d) - - claims := &Claims{ - IsActive: u.GetActive(), - IsAdmin: u.GetAdmin(), - StandardClaims: jwt.StandardClaims{ - Subject: u.GetName(), - IssuedAt: now.Unix(), - ExpiresAt: exp.Unix(), - }, - } - - t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - token, err := t.SignedString([]byte(u.GetHash())) - if err != nil { - return "", err - } - - return token, nil -} - -// CreateCreateRefreshToken creates a new refresh token for the given user and duration. -func CreateRefreshToken(u *library.User, d time.Duration) (string, int, error) { - exp := time.Now().Add(d) - - claims := jwt.StandardClaims{} - claims.Subject = u.GetName() - claims.ExpiresAt = exp.Unix() - - t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - refreshToken, err := t.SignedString([]byte(u.GetHash())) - if err != nil { - return "", 0, err - } - - return refreshToken, int(d.Seconds()), nil -} - -// Refresh returns a new access token, if the provided refreshToken is valid. -func Refresh(c *gin.Context, refreshToken string) (string, error) { - // get the metadata - m := c.MustGet("metadata").(*types.Metadata) - // get a reference to the database - db := database.FromContext(c) - - // parse (which also validates) the token - u, err := Parse(refreshToken, db) - if err != nil { - return "", err - } - - // create a new access token - at, err := CreateAccessToken(u, m.Vela.AccessTokenDuration) - if err != nil { - return "", err - } - - return at, nil -} diff --git a/router/middleware/token/token_test.go b/router/middleware/token/token_test.go deleted file mode 100644 index 6e22c8111..000000000 --- a/router/middleware/token/token_test.go +++ /dev/null @@ -1,539 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package token - -import ( - "fmt" - "net/http" - "net/http/httptest" - "reflect" - "strings" - "testing" - "time" - - "github.com/gin-gonic/gin" - "github.com/go-vela/server/database/sqlite" - "github.com/go-vela/types" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" - - jwt "github.com/golang-jwt/jwt/v4" -) - -func TestToken_Compose(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - - d := time.Minute * 5 - now := time.Now() - exp := now.Add(d) - - claims := &Claims{ - IsActive: u.GetActive(), - IsAdmin: u.GetAdmin(), - StandardClaims: jwt.StandardClaims{ - Subject: u.GetName(), - IssuedAt: now.Unix(), - ExpiresAt: exp.Unix(), - }, - } - - tkn := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - want, err := tkn.SignedString([]byte(u.GetHash())) - if err != nil { - t.Errorf("Unable to create test token: %v", err) - } - - m := &types.Metadata{ - Vela: &types.Vela{ - AccessTokenDuration: d, - }, - } - - gin.SetMode(gin.TestMode) - - resp := httptest.NewRecorder() - context, _ := gin.CreateTestContext(resp) - context.Set("metadata", m) - context.Set("securecookie", false) - - // run test - _, got, err := Compose(context, u) - if err != nil { - t.Errorf("Compose returned err: %v", err) - } - - if !strings.EqualFold(got, want) { - t.Errorf("Compose is %v, want %v", got, want) - } -} - -func TestToken_Parse(t *testing.T) { - // setup types - want := new(library.User) - want.SetID(1) - want.SetName("foo") - want.SetRefreshToken("fresh") - want.SetToken("bar") - want.SetHash("baz") - want.SetActive(false) - want.SetAdmin(false) - want.SetFavorites([]string{}) - - m := &types.Metadata{ - Vela: &types.Vela{ - AccessTokenDuration: time.Minute * 5, - }, - } - - gin.SetMode(gin.TestMode) - - resp := httptest.NewRecorder() - context, _ := gin.CreateTestContext(resp) - context.Set("metadata", m) - - tkn, err := CreateAccessToken(want, time.Minute*5) - if err != nil { - t.Errorf("Unable to create token: %v", err) - } - - // setup database - db, _ := sqlite.NewTest() - - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() - - _ = db.CreateUser(want) - - // run test - got, err := Parse(tkn, db) - if err != nil { - t.Errorf("Parse returned err: %v", err) - } - - if !reflect.DeepEqual(got, want) { - t.Errorf("Parse is %v, want %v", got, want) - } -} - -func TestToken_Parse_Error_NoParse(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - - // setup database - db, _ := sqlite.NewTest() - - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() - - _ = db.CreateUser(u) - - // run test - got, err := Parse("!@#$%^&*()", db) - if err == nil { - t.Errorf("Parse should have returned err") - } - - if got != nil { - t.Errorf("Parse is %v, want nil", got) - } -} - -func TestToken_Parse_Error_InvalidSignature(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - - claims := &Claims{ - IsActive: u.GetActive(), - IsAdmin: u.GetAdmin(), - StandardClaims: jwt.StandardClaims{ - Subject: u.GetName(), - }, - } - tkn := jwt.NewWithClaims(jwt.SigningMethodHS512, claims) - - token, err := tkn.SignedString([]byte(u.GetHash())) - if err != nil { - t.Errorf("Unable to create test token: %v", err) - } - - // setup database - db, _ := sqlite.NewTest() - - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() - - _ = db.CreateUser(u) - - // run test - got, err := Parse(token, db) - if err == nil { - t.Errorf("Parse should have returned err") - } - - if got != nil { - t.Errorf("Parse is %v, want nil", got) - } -} - -func TestToken_Parse_AccessToken_Expired(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - - tkn, err := CreateAccessToken(u, time.Minute*-1) - if err != nil { - t.Errorf("Unable to create token: %v", err) - } - - // setup database - db, _ := sqlite.NewTest() - - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() - - _ = db.CreateUser(u) - - // run test - _, err = Parse(tkn, db) - if err == nil { - t.Errorf("Parse should return error due to expiration") - } -} - -func TestToken_Parse_AccessToken_NoSubject(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - - claims := &Claims{ - IsActive: u.GetActive(), - IsAdmin: u.GetAdmin(), - StandardClaims: jwt.StandardClaims{ - ExpiresAt: 42, - }, - } - tkn := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - token, err := tkn.SignedString([]byte(u.GetHash())) - if err != nil { - t.Errorf("Unable to create test token: %v", err) - } - - // setup database - db, _ := sqlite.NewTest() - - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() - - _ = db.CreateUser(u) - - // run test - got, err := Parse(token, db) - if err == nil { - t.Errorf("Parse should have returned err") - } - - if got != nil { - t.Errorf("Parse is %v, want nil", got) - } -} - -func TestToken_Parse_AccessToken_NoExpiration(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - - claims := &Claims{ - IsActive: u.GetActive(), - IsAdmin: u.GetAdmin(), - StandardClaims: jwt.StandardClaims{ - Subject: u.GetName(), - }, - } - tkn := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - token, err := tkn.SignedString([]byte(u.GetHash())) - if err != nil { - t.Errorf("Unable to create test token: %v", err) - } - - // setup database - db, _ := sqlite.NewTest() - - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() - - _ = db.CreateUser(u) - - // run test - got, err := Parse(token, db) - if err == nil { - t.Errorf("Parse should have returned err") - } - - if got != nil { - t.Errorf("Parse is %v, want nil", got) - } -} - -func TestToken_Refresh(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - - d := time.Minute * 5 - - m := &types.Metadata{ - Vela: &types.Vela{ - AccessTokenDuration: d, - }, - } - - rt, _, err := CreateRefreshToken(u, d) - if err != nil { - t.Errorf("unable to create refresh token") - } - - u.SetRefreshToken(rt) - - // setup database - db, _ := sqlite.NewTest() - - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() - - _ = db.CreateUser(u) - - // set up context - gin.SetMode(gin.TestMode) - - resp := httptest.NewRecorder() - context, _ := gin.CreateTestContext(resp) - context.Set("metadata", m) - context.Set("database", db) - - // run tests - got, err := Refresh(context, rt) - if err != nil { - t.Error("Refresh should not error") - } - - if len(got) == 0 { - t.Errorf("Refresh should have returned an access token") - } -} - -func TestToken_Refresh_Expired(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - - d := time.Minute * -1 - - m := &types.Metadata{ - Vela: &types.Vela{ - AccessTokenDuration: d, - }, - } - - rt, _, err := CreateRefreshToken(u, d) - if err != nil { - t.Errorf("unable to create refresh token") - } - - u.SetRefreshToken(rt) - - // setup database - db, _ := sqlite.NewTest() - - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() - - _ = db.CreateUser(u) - - // set up context - gin.SetMode(gin.TestMode) - - resp := httptest.NewRecorder() - context, _ := gin.CreateTestContext(resp) - context.Set("metadata", m) - context.Set("database", db) - - // run tests - _, err = Refresh(context, rt) - if err == nil { - t.Error("Refresh with expired token should error") - } -} - -func TestToken_Refresh_TokenMissing(t *testing.T) { - // setup types - u := new(library.User) - u.SetID(1) - u.SetName("foo") - u.SetToken("bar") - u.SetHash("baz") - - d := time.Minute * -1 - - m := &types.Metadata{ - Vela: &types.Vela{ - AccessTokenDuration: d, - }, - } - - rt, _, err := CreateRefreshToken(u, d) - if err != nil { - t.Errorf("unable to create refresh token") - } - - // setup database - db, _ := sqlite.NewTest() - - defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() - }() - - _ = db.CreateUser(u) - - // set up context - gin.SetMode(gin.TestMode) - - resp := httptest.NewRecorder() - context, _ := gin.CreateTestContext(resp) - context.Set("metadata", m) - context.Set("database", db) - - // run tests - _, err = Refresh(context, rt) - if err == nil { - t.Error("Refresh with token that doesn't exist in database should error") - } -} - -func TestToken_Retrieve_Refresh(t *testing.T) { - // setup types - want := "fresh" - - request, _ := http.NewRequest(http.MethodGet, "/test", nil) - request.AddCookie(&http.Cookie{ - Name: constants.RefreshTokenName, - Value: want, - }) - - // run test - got, err := RetrieveRefreshToken(request) - if err != nil { - t.Errorf("Retrieve returned err: %v", err) - } - - if !strings.EqualFold(got, want) { - t.Errorf("Retrieve is %v, want %v", got, want) - } -} - -func TestToken_Retrieve_Access(t *testing.T) { - // setup types - want := "foobar" - - header := fmt.Sprintf("Bearer %s", want) - request, _ := http.NewRequest(http.MethodGet, "/test", nil) - request.Header.Set("Authorization", header) - - // run test - got, err := RetrieveAccessToken(request) - if err != nil { - t.Errorf("Retrieve returned err: %v", err) - } - - if !strings.EqualFold(got, want) { - t.Errorf("Retrieve is %v, want %v", got, want) - } -} - -func TestToken_Retrieve_Access_Error(t *testing.T) { - // setup types - request, _ := http.NewRequest(http.MethodGet, "/test", nil) - - // run test - got, err := RetrieveAccessToken(request) - if err == nil { - t.Errorf("Retrieve should have returned err") - } - - if len(got) > 0 { - t.Errorf("Retrieve is %v, want \"\"", got) - } -} - -func TestToken_Retrieve_Refresh_Error(t *testing.T) { - // setup types - request, _ := http.NewRequest(http.MethodGet, "/test", nil) - - // run test - got, err := RetrieveRefreshToken(request) - if err == nil { - t.Errorf("Retrieve should have returned err") - } - - if len(got) > 0 { - t.Errorf("Retrieve is %v, want \"\"", got) - } -} diff --git a/router/middleware/token_manager.go b/router/middleware/token_manager.go new file mode 100644 index 000000000..0d8d78108 --- /dev/null +++ b/router/middleware/token_manager.go @@ -0,0 +1,20 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package middleware + +import ( + "github.com/gin-gonic/gin" + + "github.com/go-vela/server/internal/token" +) + +// TokenManager is a middleware function that attaches the token manager +// to the context of every http.Request. +func TokenManager(m *token.Manager) gin.HandlerFunc { + return func(c *gin.Context) { + c.Set("token-manager", m) + c.Next() + } +} diff --git a/router/middleware/token_manager_test.go b/router/middleware/token_manager_test.go new file mode 100644 index 000000000..2ba6e23f2 --- /dev/null +++ b/router/middleware/token_manager_test.go @@ -0,0 +1,53 @@ +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package middleware + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/go-vela/server/internal/token" + + "github.com/gin-gonic/gin" +) + +func TestMiddleware_TokenManager(t *testing.T) { + // setup types + s := httptest.NewServer(http.NotFoundHandler()) + defer s.Close() + + var got *token.Manager + + want := new(token.Manager) + want.PrivateKey = "123abc" + + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + context, engine := gin.CreateTestContext(resp) + context.Request, _ = http.NewRequest(http.MethodGet, "/health", nil) + + // setup mock server + engine.Use(TokenManager(want)) + engine.GET("/health", func(c *gin.Context) { + got = c.MustGet("token-manager").(*token.Manager) + + c.Status(http.StatusOK) + }) + + // run test + engine.ServeHTTP(context.Writer, context.Request) + + if resp.Code != http.StatusOK { + t.Errorf("TokenManager returned %v, want %v", resp.Code, http.StatusOK) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("TokenManager is %v, want %v", got, want) + } +} diff --git a/router/middleware/user/doc.go b/router/middleware/user/doc.go index d86024512..42181f07d 100644 --- a/router/middleware/user/doc.go +++ b/router/middleware/user/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/router/middleware/user" +// import "github.com/go-vela/server/router/middleware/user" package user diff --git a/router/middleware/user/user.go b/router/middleware/user/user.go index 58d2cab87..ad94a5e0d 100644 --- a/router/middleware/user/user.go +++ b/router/middleware/user/user.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -9,9 +9,10 @@ import ( "strings" "github.com/go-vela/server/database" - "github.com/go-vela/server/router/middleware/token" + "github.com/go-vela/server/router/middleware/claims" "github.com/go-vela/server/util" + "github.com/go-vela/types/constants" "github.com/go-vela/types/library" "github.com/gin-gonic/gin" @@ -26,20 +27,11 @@ func Retrieve(c *gin.Context) *library.User { // Establish sets the user in the given context. func Establish() gin.HandlerFunc { return func(c *gin.Context) { - // get the access token from the request - at, err := token.RetrieveAccessToken(c.Request) - if err != nil { - util.HandleError(c, http.StatusUnauthorized, err) - return - } + cl := claims.Retrieve(c) - // special handling for workers - secret := c.MustGet("secret").(string) - if strings.EqualFold(at, secret) { + // if token is not a user token or claims were not retrieved, establish empty user to better handle nil checks + if cl == nil || !strings.EqualFold(cl.TokenType, constants.UserAccessTokenType) { u := new(library.User) - u.SetName("vela-worker") - u.SetActive(true) - u.SetAdmin(true) ToContext(c, u) c.Next() @@ -49,8 +41,8 @@ func Establish() gin.HandlerFunc { logrus.Debugf("parsing user access token") - // parse and validate the token and return the associated the user - u, err := token.Parse(at, database.FromContext(c)) + // lookup user in claims subject in the database + u, err := database.FromContext(c).GetUserForName(cl.Subject) if err != nil { util.HandleError(c, http.StatusUnauthorized, err) return diff --git a/router/middleware/user/user_test.go b/router/middleware/user/user_test.go index 6aefc9e8a..0ea18dfcc 100644 --- a/router/middleware/user/user_test.go +++ b/router/middleware/user/user_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. @@ -12,16 +12,15 @@ import ( "testing" "time" + "github.com/gin-gonic/gin" "github.com/go-vela/server/database" - "github.com/go-vela/server/database/sqlite" - "github.com/go-vela/server/router/middleware/token" + "github.com/go-vela/server/internal/token" + "github.com/go-vela/server/router/middleware/claims" "github.com/go-vela/server/scm" "github.com/go-vela/server/scm/github" - "github.com/go-vela/types/constants" "github.com/go-vela/types/library" - - "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" ) func TestUser_Retrieve(t *testing.T) { @@ -47,6 +46,13 @@ func TestUser_Establish(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + want := new(library.User) want.SetID(1) want.SetName("foo") @@ -65,7 +71,13 @@ func TestUser_Establish(t *testing.T) { context, engine := gin.CreateTestContext(resp) context.Request, _ = http.NewRequest(http.MethodGet, "/users/foo", nil) - at, _ := token.CreateAccessToken(want, time.Minute*5) + mto := &token.MintTokenOpts{ + User: want, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } + + at, _ := tm.MintToken(mto) context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", at)) context.Request.AddCookie(&http.Cookie{ @@ -74,12 +86,14 @@ func TestUser_Establish(t *testing.T) { }) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from users;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteUser(want) + db.Close() }() _ = db.CreateUser(want) @@ -100,8 +114,10 @@ func TestUser_Establish(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(claims.Establish()) engine.Use(Establish()) engine.GET("/users/:user", func(c *gin.Context) { got = Retrieve(c) @@ -125,9 +141,20 @@ func TestUser_Establish(t *testing.T) { } func TestUser_Establish_NoToken(t *testing.T) { + // setup types + secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // setup context gin.SetMode(gin.TestMode) @@ -137,7 +164,10 @@ func TestUser_Establish_NoToken(t *testing.T) { context.Request, _ = http.NewRequest(http.MethodGet, "/users/foo", nil) // setup mock server + engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(claims.Establish()) engine.Use(Establish()) // run test @@ -148,14 +178,18 @@ func TestUser_Establish_NoToken(t *testing.T) { } } -func TestUser_Establish_SecretValid(t *testing.T) { +func TestUser_Establish_DiffTokenType(t *testing.T) { // setup types secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + want := new(library.User) - want.SetName("vela-worker") - want.SetActive(true) - want.SetAdmin(true) got := new(library.User) @@ -169,6 +203,8 @@ func TestUser_Establish_SecretValid(t *testing.T) { // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(claims.Establish()) engine.Use(Establish()) engine.GET("/users/:user", func(c *gin.Context) { got = Retrieve(c) @@ -195,9 +231,19 @@ func TestUser_Establish_NoAuthorizeUser(t *testing.T) { // setup database secret := "superSecret" + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // setup context gin.SetMode(gin.TestMode) @@ -213,6 +259,8 @@ func TestUser_Establish_NoAuthorizeUser(t *testing.T) { engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(claims.Establish()) engine.Use(Establish()) // run test @@ -225,12 +273,26 @@ func TestUser_Establish_NoAuthorizeUser(t *testing.T) { func TestUser_Establish_NoUser(t *testing.T) { // setup types + tm := &token.Manager{ + PrivateKey: "123abc", + SignMethod: jwt.SigningMethodHS256, + UserAccessTokenDuration: time.Minute * 5, + UserRefreshTokenDuration: time.Minute * 30, + } + + u := new(library.User) + u.SetID(1) + u.SetName("foo") + + // setup database secret := "superSecret" - got := new(library.User) // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // setup context gin.SetMode(gin.TestMode) @@ -239,30 +301,30 @@ func TestUser_Establish_NoUser(t *testing.T) { context, engine := gin.CreateTestContext(resp) context.Request, _ = http.NewRequest(http.MethodGet, "/users/foo?access_token=bar", nil) - // setup github mock server - engine.GET("/api/v3/user", func(c *gin.Context) { - c.String(http.StatusOK, userPayload) - }) + mto := &token.MintTokenOpts{ + User: u, + TokenDuration: tm.UserAccessTokenDuration, + TokenType: constants.UserAccessTokenType, + } - s := httptest.NewServer(engine) - defer s.Close() + at, _ := tm.MintToken(mto) + + context.Request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", at)) + context.Request.AddCookie(&http.Cookie{ + Name: constants.RefreshTokenName, + Value: "fresh", + }) // setup client - client, _ := github.NewTest(s.URL) + client, _ := github.NewTest("") // setup vela mock server engine.Use(func(c *gin.Context) { c.Set("secret", secret) }) engine.Use(func(c *gin.Context) { database.ToContext(c, db) }) engine.Use(func(c *gin.Context) { scm.ToContext(c, client) }) + engine.Use(func(c *gin.Context) { c.Set("token-manager", tm) }) + engine.Use(claims.Establish()) engine.Use(Establish()) - engine.GET("/users/:user", func(c *gin.Context) { - got = Retrieve(c) - - c.Status(http.StatusOK) - }) - - s1 := httptest.NewServer(engine) - defer s1.Close() // run test engine.ServeHTTP(context.Writer, context.Request) @@ -270,10 +332,6 @@ func TestUser_Establish_NoUser(t *testing.T) { if resp.Code != http.StatusUnauthorized { t.Errorf("Establish returned %v, want %v", resp.Code, http.StatusUnauthorized) } - - if got.GetID() != 0 { - t.Errorf("Establish is %v, want 0", got) - } } const userPayload = ` diff --git a/router/middleware/webhook_validation_test.go b/router/middleware/webhook_validation_test.go index e7596ba9f..cb0f432fe 100644 --- a/router/middleware/webhook_validation_test.go +++ b/router/middleware/webhook_validation_test.go @@ -18,6 +18,7 @@ func TestWebhook_WebhookValidation(t *testing.T) { type args struct { validate bool } + tests := []struct { name string args args @@ -38,6 +39,7 @@ func TestWebhook_WebhookValidation(t *testing.T) { want: true, }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // setup context diff --git a/router/middleware/worker/doc.go b/router/middleware/worker/doc.go index 1070cbd2b..44eaaa675 100644 --- a/router/middleware/worker/doc.go +++ b/router/middleware/worker/doc.go @@ -8,5 +8,5 @@ // // Usage: // -// import "github.com/go-vela/server/router/middleware/worker" +// import "github.com/go-vela/server/router/middleware/worker" package worker diff --git a/router/middleware/worker/worker.go b/router/middleware/worker/worker.go index c2eed2880..5afc8b3bf 100644 --- a/router/middleware/worker/worker.go +++ b/router/middleware/worker/worker.go @@ -5,15 +5,13 @@ package worker import ( - "github.com/go-vela/server/database" - "github.com/go-vela/types/library" - - "github.com/go-vela/server/util" - "fmt" "net/http" "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/util" + "github.com/go-vela/types/library" "github.com/sirupsen/logrus" ) @@ -25,18 +23,21 @@ func Retrieve(c *gin.Context) *library.Worker { // Establish sets the worker in the given context. func Establish() gin.HandlerFunc { return func(c *gin.Context) { - wParam := c.Param("worker") + wParam := util.PathParameter(c, "worker") if len(wParam) == 0 { retErr := fmt.Errorf("no worker parameter provided") util.HandleError(c, http.StatusBadRequest, retErr) + return } logrus.Debugf("Reading worker %s", wParam) - w, err := database.FromContext(c).GetWorker(wParam) + + w, err := database.FromContext(c).GetWorkerForHostname(wParam) if err != nil { - retErr := fmt.Errorf("unable to read worker %s: %v", wParam, err) + retErr := fmt.Errorf("unable to read worker %s: %w", wParam, err) util.HandleError(c, http.StatusNotFound, retErr) + return } diff --git a/router/middleware/worker/worker_test.go b/router/middleware/worker/worker_test.go index dd0b92ffa..58d090825 100644 --- a/router/middleware/worker/worker_test.go +++ b/router/middleware/worker/worker_test.go @@ -12,7 +12,6 @@ import ( "github.com/gin-gonic/gin" "github.com/go-vela/server/database" - "github.com/go-vela/server/database/sqlite" "github.com/go-vela/types/library" ) @@ -42,18 +41,25 @@ func TestWorker_Establish(t *testing.T) { want.SetAddress("localhost") want.SetRoutes([]string{"foo", "bar", "baz"}) want.SetActive(true) + want.SetStatus("available") + want.SetLastStatusUpdateAt(12345) + want.SetRunningBuildIDs([]string{}) + want.SetLastBuildStartedAt(12345) + want.SetLastBuildFinishedAt(12345) want.SetLastCheckedIn(12345) want.SetBuildLimit(0) got := new(library.Worker) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from workers;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteWorker(want) + db.Close() }() _ = db.CreateWorker(want) @@ -88,8 +94,11 @@ func TestWorker_Establish(t *testing.T) { func TestWorker_Establish_NoWorkerParameter(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // setup context gin.SetMode(gin.TestMode) diff --git a/router/middleware/worker_test.go b/router/middleware/worker_test.go index 2e1a941e4..5c141bdbb 100644 --- a/router/middleware/worker_test.go +++ b/router/middleware/worker_test.go @@ -17,6 +17,7 @@ import ( func TestMiddleware_Worker(t *testing.T) { // setup types var got time.Duration + want := 5 * time.Minute // setup context diff --git a/router/pipeline.go b/router/pipeline.go index 9f2668b0e..b523f9495 100644 --- a/router/pipeline.go +++ b/router/pipeline.go @@ -6,27 +6,41 @@ package router import ( "github.com/gin-gonic/gin" - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/pipeline" "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/perm" + pmiddleware "github.com/go-vela/server/router/middleware/pipeline" "github.com/go-vela/server/router/middleware/repo" ) // PipelineHandlers is a function that extends the provided base router group // with the API handlers for pipeline functionality. // -// GET /api/v1/pipelines/:org/:repo -// GET /api/v1/pipelines/:org/:repo/templates -// POST /api/v1/pipelines/:org/:repo/expand -// POST /api/v1/pipelines/:org/:repo/compile -// POST /api/v1/pipelines/:org/:repo/validate . +// POST /api/v1/pipelines/:org/:repo +// GET /api/v1/pipelines/:org/:repo +// GET /api/v1/pipelines/:org/:repo/:pipeline +// PUT /api/v1/pipelines/:org/:repo/:pipeline +// DELETE /api/v1/pipelines/:org/:repo/:pipeline +// GET /api/v1/pipelines/:org/:repo/:pipeline/templates +// POST /api/v1/pipelines/:org/:repo/:pipeline/expand +// POST /api/v1/pipelines/:org/:repo/:pipeline/compile +// POST /api/v1/pipelines/:org/:repo/:pipeline/validate . func PipelineHandlers(base *gin.RouterGroup) { // Pipelines endpoints - pipelines := base.Group("pipelines/:org/:repo", org.Establish(), repo.Establish()) + _pipelines := base.Group("pipelines/:org/:repo", org.Establish(), repo.Establish()) { - pipelines.GET("", api.GetPipeline) - pipelines.GET("/templates", api.GetTemplates) - pipelines.POST("/expand", api.ExpandPipeline) - pipelines.POST("/validate", api.ValidatePipeline) - pipelines.POST("/compile", api.CompilePipeline) + _pipelines.POST("", perm.MustAdmin(), pipeline.CreatePipeline) + _pipelines.GET("", perm.MustRead(), pipeline.ListPipelines) + + _pipeline := _pipelines.Group("/:pipeline", pmiddleware.Establish()) + { + _pipeline.GET("", perm.MustRead(), pipeline.GetPipeline) + _pipeline.PUT("", perm.MustWrite(), pipeline.UpdatePipeline) + _pipeline.DELETE("", perm.MustPlatformAdmin(), pipeline.DeletePipeline) + _pipeline.GET("/templates", perm.MustRead(), pipeline.GetTemplates) + _pipeline.POST("/compile", perm.MustRead(), pipeline.CompilePipeline) + _pipeline.POST("/expand", perm.MustRead(), pipeline.ExpandPipeline) + _pipeline.POST("/validate", perm.MustRead(), pipeline.ValidatePipeline) + } // end of pipeline endpoints } // end of pipelines endpoints } diff --git a/router/repo.go b/router/repo.go index 0ebbb3522..7b7859f6a 100644 --- a/router/repo.go +++ b/router/repo.go @@ -6,11 +6,12 @@ package router import ( "github.com/gin-gonic/gin" - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/build" + "github.com/go-vela/server/api/repo" "github.com/go-vela/server/router/middleware" "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/perm" - "github.com/go-vela/server/router/middleware/repo" + rmiddleware "github.com/go-vela/server/router/middleware/repo" ) // RepoHandlers is a function that extends the provided base router group @@ -31,7 +32,9 @@ import ( // GET /api/v1/repos/:org/:repo/builds/:build // PUT /api/v1/repos/:org/:repo/builds/:build // DELETE /api/v1/repos/:org/:repo/builds/:build +// DELETE /api/v1/repos/:org/:repo/builds/:build/cancel // GET /api/v1/repos/:org/:repo/builds/:build/logs +// GET /api/v1/repos/:org/:repo/builds/:build/token // POST /api/v1/repos/:org/:repo/builds/:build/services // GET /api/v1/repos/:org/:repo/builds/:build/services // GET /api/v1/repos/:org/:repo/builds/:build/services/:service @@ -52,32 +55,32 @@ import ( // DELETE /api/v1/repos/:org/:repo/builds/:build/steps/:step/logs . func RepoHandlers(base *gin.RouterGroup) { // Repos endpoints - repos := base.Group("/repos") + _repos := base.Group("/repos") { - repos.POST("", middleware.Payload(), api.CreateRepo) - repos.GET("", api.GetRepos) + _repos.POST("", middleware.Payload(), repo.CreateRepo) + _repos.GET("", repo.ListRepos) // Org endpoints - org := repos.Group("/:org", org.Establish()) + org := _repos.Group("/:org", org.Establish()) { - org.GET("", api.GetOrgRepos) - org.GET("/builds", api.GetOrgBuilds) + org.GET("", repo.ListReposForOrg) + org.GET("/builds", build.ListBuildsForOrg) // Repo endpoints - repo := org.Group("/:repo", repo.Establish()) + _repo := org.Group("/:repo", rmiddleware.Establish()) { - repo.GET("", perm.MustRead(), api.GetRepo) - repo.PUT("", perm.MustAdmin(), middleware.Payload(), api.UpdateRepo) - repo.DELETE("", perm.MustAdmin(), api.DeleteRepo) - repo.PATCH("/repair", perm.MustAdmin(), api.RepairRepo) - repo.PATCH("/chown", perm.MustAdmin(), api.ChownRepo) + _repo.GET("", perm.MustRead(), repo.GetRepo) + _repo.PUT("", perm.MustAdmin(), middleware.Payload(), repo.UpdateRepo) + _repo.DELETE("", perm.MustAdmin(), repo.DeleteRepo) + _repo.PATCH("/repair", perm.MustAdmin(), repo.RepairRepo) + _repo.PATCH("/chown", perm.MustAdmin(), repo.ChownRepo) // Build endpoints // * Service endpoints // * Log endpoints // * Step endpoints // * Log endpoints - BuildHandlers(repo) + BuildHandlers(_repo) } // end of repo endpoints } // end of org endpoints } // end of repos endpoints diff --git a/router/router.go b/router/router.go index 43870bb5d..f95c6bf4b 100644 --- a/router/router.go +++ b/router/router.go @@ -6,39 +6,41 @@ // // API for the Vela server // -// Version: 0.0.0-dev -// Schemes: http, https -// Host: localhost +// Version: 0.0.0-dev +// Schemes: http, https +// Host: localhost // -// Consumes: -// - application/json +// Consumes: +// - application/json // -// Produces: -// - application/json +// Produces: +// - application/json // -// SecurityDefinitions: -// ApiKeyAuth: -// description: Bearer token -// type: apiKey -// in: header -// name: Authorization -// CookieAuth: -// description: Refresh token sent as cookie (swagger 2.0 doesn't support cookie auth) -// type: apiKey -// in: header -// name: vela_refresh_token +// SecurityDefinitions: +// ApiKeyAuth: +// description: Bearer token +// type: apiKey +// in: header +// name: Authorization +// CookieAuth: +// description: Refresh token sent as cookie (swagger 2.0 doesn't support cookie auth) +// type: apiKey +// in: header +// name: vela_refresh_token // // swagger:meta package router import ( + "github.com/gin-gonic/gin" "github.com/go-vela/server/api" + "github.com/go-vela/server/api/auth" + "github.com/go-vela/server/api/webhook" "github.com/go-vela/server/router/middleware" + "github.com/go-vela/server/router/middleware/claims" "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" "github.com/go-vela/server/router/middleware/user" - - "github.com/gin-gonic/gin" ) const ( @@ -65,34 +67,37 @@ func Load(options ...gin.HandlerFunc) *gin.Engine { r.GET("/health", api.Health) // Login endpoint - r.GET("/login", api.Login) + r.GET("/login", auth.Login) // Logout endpoint - r.GET("/logout", user.Establish(), api.Logout) + r.GET("/logout", claims.Establish(), user.Establish(), auth.Logout) // Refresh Access Token endpoint - r.GET("/token-refresh", api.RefreshAccessToken) + r.GET("/token-refresh", auth.RefreshAccessToken) // Metric endpoint r.GET("/metrics", api.CustomMetrics, gin.WrapH(api.BaseMetrics())) + // Validate Server Token endpoint + r.GET("/validate-token", claims.Establish(), auth.ValidateServerToken) + // Version endpoint r.GET("/version", api.Version) // Webhook endpoint - r.POST("/webhook", api.PostWebhook) + r.POST("/webhook", webhook.PostWebhook) // Authentication endpoints authenticate := r.Group("/authenticate") { - authenticate.GET("", api.Authenticate) - authenticate.GET("/:type", api.AuthenticateType) - authenticate.GET("/:type/:port", api.AuthenticateType) - authenticate.POST("/token", api.AuthenticateToken) + authenticate.GET("", auth.GetAuthToken) + authenticate.GET("/:type", auth.GetAuthRedirect) + authenticate.GET("/:type/:port", auth.GetAuthRedirect) + authenticate.POST("/token", auth.PostAuthToken) } // API endpoints - baseAPI := r.Group(base, user.Establish()) + baseAPI := r.Group(base, claims.Establish(), user.Establish()) { // Admin endpoints AdminHandlers(baseAPI) @@ -111,9 +116,15 @@ func Load(options ...gin.HandlerFunc) *gin.Engine { // * Log endpoints RepoHandlers(baseAPI) + // Schedule endpoints + ScheduleHandler(baseAPI) + // Source code management endpoints ScmHandlers(baseAPI) + // Search endpoints + SearchHandlers(baseAPI) + // Secret endpoints SecretHandlers(baseAPI) @@ -125,6 +136,7 @@ func Load(options ...gin.HandlerFunc) *gin.Engine { // Pipeline endpoints PipelineHandlers(baseAPI) + } // end of api return r diff --git a/router/schedule.go b/router/schedule.go new file mode 100644 index 000000000..7c73e30c2 --- /dev/null +++ b/router/schedule.go @@ -0,0 +1,39 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package router + +import ( + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api/schedule" + "github.com/go-vela/server/router/middleware" + "github.com/go-vela/server/router/middleware/org" + "github.com/go-vela/server/router/middleware/perm" + "github.com/go-vela/server/router/middleware/repo" + sMiddleware "github.com/go-vela/server/router/middleware/schedule" +) + +// ScheduleHandler is a function that extends the provided base router group +// with the API handlers for schedule functionality. +// +// POST /api/v1/schedules/:org/:repo +// GET /api/v1/schedules/:org/:repo +// GET /api/v1/schedules/:org/:repo/:schedule +// PUT /api/v1/schedules/:org/:repo/:schedule +// DELETE /api/v1/schedules/:org/:repo/:schedule . +func ScheduleHandler(base *gin.RouterGroup) { + // Schedules endpoints + _schedules := base.Group("/schedules/:org/:repo", org.Establish(), repo.Establish()) + { + _schedules.POST("", perm.MustAdmin(), middleware.Payload(), schedule.CreateSchedule) + _schedules.GET("", perm.MustRead(), schedule.ListSchedules) + + s := _schedules.Group("/:schedule", sMiddleware.Establish()) + { + s.GET("", perm.MustRead(), schedule.GetSchedule) + s.PUT("", perm.MustAdmin(), middleware.Payload(), schedule.UpdateSchedule) + s.DELETE("", perm.MustAdmin(), schedule.DeleteSchedule) + } + } // end of schedules endpoints +} diff --git a/router/scm.go b/router/scm.go index 85b2a0d9f..40d745087 100644 --- a/router/scm.go +++ b/router/scm.go @@ -6,7 +6,7 @@ package router import ( "github.com/gin-gonic/gin" - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/scm" "github.com/go-vela/server/router/middleware/org" "github.com/go-vela/server/router/middleware/repo" ) @@ -23,7 +23,7 @@ func ScmHandlers(base *gin.RouterGroup) { // SCM org endpoints org := orgs.Group("/:org", org.Establish()) { - org.GET("/sync", api.SyncRepos) + org.GET("/sync", scm.SyncReposForOrg) } // end of SCM org endpoints } // end of SCM orgs endpoints @@ -33,7 +33,7 @@ func ScmHandlers(base *gin.RouterGroup) { // SCM repo endpoints repo := repos.Group("/:org/:repo", org.Establish(), repo.Establish()) { - repo.GET("/sync", api.SyncRepo) + repo.GET("/sync", scm.SyncRepo) } // end of SCM repo endpoints } // end of SCM repos endpoints } diff --git a/router/search.go b/router/search.go new file mode 100644 index 000000000..62e3542db --- /dev/null +++ b/router/search.go @@ -0,0 +1,26 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package router + +import ( + "github.com/gin-gonic/gin" + "github.com/go-vela/server/api/build" +) + +// SearchHandlers is a function that extends the provided base router group +// with the API handlers for resource search functionality. +// +// GET /api/v1/search/builds/:id . +func SearchHandlers(base *gin.RouterGroup) { + // Search endpoints + search := base.Group("/search") + { + // Build endpoint + b := search.Group("/builds") + { + b.GET("/:id", build.GetBuildByID) + } + } // end of search endpoints +} diff --git a/router/secret.go b/router/secret.go index dbc18e6b6..0ce94d982 100644 --- a/router/secret.go +++ b/router/secret.go @@ -5,7 +5,7 @@ package router import ( - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/secret" "github.com/go-vela/server/router/middleware/perm" "github.com/gin-gonic/gin" @@ -23,10 +23,10 @@ func SecretHandlers(base *gin.RouterGroup) { // Secrets endpoints secrets := base.Group("/secrets/:engine/:type/:org/:name", perm.MustSecretAdmin()) { - secrets.POST("", api.CreateSecret) - secrets.GET("", api.GetSecrets) - secrets.GET("/*secret", api.GetSecret) - secrets.PUT("/*secret", api.UpdateSecret) - secrets.DELETE("/*secret", api.DeleteSecret) + secrets.POST("", secret.CreateSecret) + secrets.GET("", secret.ListSecrets) + secrets.GET("/*secret", secret.GetSecret) + secrets.PUT("/*secret", secret.UpdateSecret) + secrets.DELETE("/*secret", secret.DeleteSecret) } // end of secrets endpoints } diff --git a/router/service.go b/router/service.go index 647d301e1..7b81e7af7 100644 --- a/router/service.go +++ b/router/service.go @@ -1,16 +1,16 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. -// nolint: dupl // ignore similar code with step +//nolint:dupl // ignore similar code with step package router import ( "github.com/gin-gonic/gin" - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/service" "github.com/go-vela/server/router/middleware" "github.com/go-vela/server/router/middleware/perm" - "github.com/go-vela/server/router/middleware/service" + smiddleware "github.com/go-vela/server/router/middleware/service" ) // ServiceHandlers is a function that extends the provided base router group @@ -24,26 +24,23 @@ import ( // POST /api/v1/repos/:org/:repo/builds/:build/services/:service/logs // GET /api/v1/repos/:org/:repo/builds/:build/services/:service/logs // PUT /api/v1/repos/:org/:repo/builds/:build/services/:service/logs -// DELETE /api/v1/repos/:org/:repo/builds/:build/services/:service/logs -// POST /api/v1/repos/:org/:repo/builds/:build/services/:service/stream . +// DELETE /api/v1/repos/:org/:repo/builds/:build/services/:service/logs . func ServiceHandlers(base *gin.RouterGroup) { // Services endpoints services := base.Group("/services") { - services.POST("", perm.MustPlatformAdmin(), middleware.Payload(), api.CreateService) - services.GET("", perm.MustRead(), api.GetServices) + services.POST("", perm.MustPlatformAdmin(), middleware.Payload(), service.CreateService) + services.GET("", perm.MustRead(), service.ListServices) // Service endpoints - service := services.Group("/:service", service.Establish()) + s := services.Group("/:service", smiddleware.Establish()) { - service.GET("", perm.MustRead(), api.GetService) - service.PUT("", perm.MustPlatformAdmin(), middleware.Payload(), api.UpdateService) - service.DELETE("", perm.MustPlatformAdmin(), api.DeleteService) - - service.POST("/stream", perm.MustPlatformAdmin(), api.PostServiceStream) + s.GET("", perm.MustRead(), service.GetService) + s.PUT("", perm.MustBuildAccess(), middleware.Payload(), service.UpdateService) + s.DELETE("", perm.MustPlatformAdmin(), service.DeleteService) // Log endpoints - LogServiceHandlers(service) + LogServiceHandlers(s) } // end of service endpoints } // end of services endpoints } diff --git a/router/step.go b/router/step.go index ac73ca238..9a53e4e97 100644 --- a/router/step.go +++ b/router/step.go @@ -1,16 +1,16 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// Copyright (c) 2023 Target Brands, Inc. All rights reserved. // // Use of this source code is governed by the LICENSE file in this repository. -// nolint: dupl // ignore similar code with service +//nolint:dupl // ignore similar code with service package router import ( "github.com/gin-gonic/gin" - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/step" "github.com/go-vela/server/router/middleware" "github.com/go-vela/server/router/middleware/perm" - "github.com/go-vela/server/router/middleware/step" + smiddleware "github.com/go-vela/server/router/middleware/step" ) // StepHandlers is a function that extends the provided base router group @@ -24,26 +24,23 @@ import ( // POST /api/v1/repos/:org/:repo/builds/:build/steps/:step/logs // GET /api/v1/repos/:org/:repo/builds/:build/steps/:step/logs // PUT /api/v1/repos/:org/:repo/builds/:build/steps/:step/logs -// DELETE /api/v1/repos/:org/:repo/builds/:build/steps/:step/logs -// POST /api/v1/repos/:org/:repo/builds/:build/steps/:step/stream . +// DELETE /api/v1/repos/:org/:repo/builds/:build/steps/:step/logs . func StepHandlers(base *gin.RouterGroup) { // Steps endpoints steps := base.Group("/steps") { - steps.POST("", perm.MustPlatformAdmin(), middleware.Payload(), api.CreateStep) - steps.GET("", perm.MustRead(), api.GetSteps) + steps.POST("", perm.MustPlatformAdmin(), middleware.Payload(), step.CreateStep) + steps.GET("", perm.MustRead(), step.ListSteps) // Step endpoints - step := steps.Group("/:step", step.Establish()) + s := steps.Group("/:step", smiddleware.Establish()) { - step.GET("", perm.MustRead(), api.GetStep) - step.PUT("", perm.MustPlatformAdmin(), middleware.Payload(), api.UpdateStep) - step.DELETE("", perm.MustPlatformAdmin(), api.DeleteStep) - - step.POST("/stream", perm.MustPlatformAdmin(), api.PostStepStream) + s.GET("", perm.MustRead(), step.GetStep) + s.PUT("", perm.MustBuildAccess(), middleware.Payload(), step.UpdateStep) + s.DELETE("", perm.MustPlatformAdmin(), step.DeleteStep) // Log endpoints - LogStepHandlers(step) + LogStepHandlers(s) } // end of step endpoints } // end of steps endpoints } diff --git a/router/user.go b/router/user.go index 1b91836b1..31abe2d92 100644 --- a/router/user.go +++ b/router/user.go @@ -6,7 +6,7 @@ package router import ( "github.com/gin-gonic/gin" - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/user" "github.com/go-vela/server/router/middleware/perm" ) @@ -18,9 +18,6 @@ import ( // GET /api/v1/users/:user // PUT /api/v1/users/:user // DELETE /api/v1/users/:user -// GET /api/v1/users/:user/source/repos -// POST /api/v1/users/:user/token -// DELETE /api/v1/users/:user/token // GET /api/v1/user // PUT /api/v1/user // GET /api/v1/user/source/repos @@ -28,22 +25,22 @@ import ( // DELETE /api/v1/user/token . func UserHandlers(base *gin.RouterGroup) { // Users endpoints - users := base.Group("/users") + _users := base.Group("/users") { - users.POST("", perm.MustPlatformAdmin(), api.CreateUser) - users.GET("", api.GetUsers) - users.GET("/:user", perm.MustPlatformAdmin(), api.GetUser) - users.PUT("/:user", perm.MustPlatformAdmin(), api.UpdateUser) - users.DELETE("/:user", perm.MustPlatformAdmin(), api.DeleteUser) + _users.POST("", perm.MustPlatformAdmin(), user.CreateUser) + _users.GET("", user.ListUsers) + _users.GET("/:user", perm.MustPlatformAdmin(), user.GetUser) + _users.PUT("/:user", perm.MustPlatformAdmin(), user.UpdateUser) + _users.DELETE("/:user", perm.MustPlatformAdmin(), user.DeleteUser) } // end of users endpoints // User endpoints - user := base.Group("/user") + _user := base.Group("/user") { - user.GET("", api.GetCurrentUser) - user.PUT("", api.UpdateCurrentUser) - user.GET("/source/repos", api.GetUserSourceRepos) - user.POST("/token", api.CreateToken) - user.DELETE("/token", api.DeleteToken) + _user.GET("", user.GetCurrentUser) + _user.PUT("", user.UpdateCurrentUser) + _user.GET("/source/repos", user.GetSourceRepos) + _user.POST("/token", user.CreateToken) + _user.DELETE("/token", user.DeleteToken) } // end of user endpoints } diff --git a/router/worker.go b/router/worker.go index 7f7df3a3b..f85a84c45 100644 --- a/router/worker.go +++ b/router/worker.go @@ -6,10 +6,10 @@ package router import ( "github.com/gin-gonic/gin" - "github.com/go-vela/server/api" + "github.com/go-vela/server/api/worker" "github.com/go-vela/server/router/middleware" "github.com/go-vela/server/router/middleware/perm" - "github.com/go-vela/server/router/middleware/worker" + wmiddleware "github.com/go-vela/server/router/middleware/worker" ) // WorkerHandlers is a function that extends the provided base router group @@ -19,20 +19,22 @@ import ( // GET /api/v1/workers // GET /api/v1/workers/:worker // PUT /api/v1/workers/:worker +// POST /api/v1/workers/:worker/refresh // DELETE /api/v1/workers/:worker . func WorkerHandlers(base *gin.RouterGroup) { // Workers endpoints - workers := base.Group("/workers") + _workers := base.Group("/workers") { - workers.POST("", perm.MustPlatformAdmin(), middleware.Payload(), api.CreateWorker) - workers.GET("", api.GetWorkers) + _workers.POST("", perm.MustWorkerRegisterToken(), middleware.Payload(), worker.CreateWorker) + _workers.GET("", worker.ListWorkers) // Worker endpoints - w := workers.Group("/:worker") + _worker := _workers.Group("/:worker") { - w.GET("", worker.Establish(), api.GetWorker) - w.PUT("", perm.MustPlatformAdmin(), worker.Establish(), api.UpdateWorker) - w.DELETE("", perm.MustPlatformAdmin(), worker.Establish(), api.DeleteWorker) + _worker.GET("", wmiddleware.Establish(), worker.GetWorker) + _worker.PUT("", perm.MustWorkerAuthToken(), wmiddleware.Establish(), worker.UpdateWorker) + _worker.POST("/refresh", perm.MustWorkerAuthToken(), wmiddleware.Establish(), worker.Refresh) + _worker.DELETE("", perm.MustPlatformAdmin(), wmiddleware.Establish(), worker.DeleteWorker) } // end of worker endpoints } // end of workers endpoints } diff --git a/scm/doc.go b/scm/doc.go index 1401e5a14..e30a7bcee 100644 --- a/scm/doc.go +++ b/scm/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/scm" +// import "github.com/go-vela/server/scm" package scm diff --git a/scm/flags.go b/scm/flags.go index 9de700f67..3dc36d139 100644 --- a/scm/flags.go +++ b/scm/flags.go @@ -15,7 +15,6 @@ import ( // https://pkg.go.dev/github.com/urfave/cli?tab=doc#Flag // // TODO: in a future release remove the "source" vars in favor of the "scm" ones. -// nolint:lll // these errors will go away when the TODO is completed var Flags = []cli.Flag{ // SCM Flags diff --git a/scm/github/access.go b/scm/github/access.go index d8d7a8afd..ee47cf3d1 100644 --- a/scm/github/access.go +++ b/scm/github/access.go @@ -10,7 +10,7 @@ import ( "github.com/sirupsen/logrus" "github.com/go-vela/types/library" - "github.com/google/go-github/v42/github" + "github.com/google/go-github/v53/github" ) // OrgAccess captures the user's access level for an org. @@ -20,9 +20,14 @@ func (c *client) OrgAccess(u *library.User, org string) (string, error) { "user": u.GetName(), }).Tracef("capturing %s access level to org %s", u.GetName(), org) - // if user is accessing personal org - if strings.EqualFold(org, *u.Name) { - // nolint: goconst // ignore making constant + // check if user is accessing personal org + if strings.EqualFold(org, u.GetName()) { + c.Logger.WithFields(logrus.Fields{ + "org": org, + "user": u.GetName(), + }).Debugf("skipping access level check for user %s with org %s", u.GetName(), org) + + //nolint:goconst // ignore making constant return "admin", nil } @@ -51,6 +56,17 @@ func (c *client) RepoAccess(u *library.User, token, org, repo string) (string, e "user": u.GetName(), }).Tracef("capturing %s access level to repo %s/%s", u.GetName(), org, repo) + // check if user is accessing repo in personal org + if strings.EqualFold(org, u.GetName()) { + c.Logger.WithFields(logrus.Fields{ + "org": org, + "repo": repo, + "user": u.GetName(), + }).Debugf("skipping access level check for user %s with repo %s/%s", u.GetName(), org, repo) + + return "admin", nil + } + // create github oauth client with the given token client := c.newClientToken(token) @@ -71,6 +87,17 @@ func (c *client) TeamAccess(u *library.User, org, team string) (string, error) { "user": u.GetName(), }).Tracef("capturing %s access level to team %s/%s", u.GetName(), org, team) + // check if user is accessing team in personal org + if strings.EqualFold(org, u.GetName()) { + c.Logger.WithFields(logrus.Fields{ + "org": org, + "team": team, + "user": u.GetName(), + }).Debugf("skipping access level check for user %s with team %s/%s", u.GetName(), org, team) + + return "admin", nil + } + // create GitHub OAuth client with user's token client := c.newClientToken(u.GetToken()) teams := []*github.Team{} diff --git a/scm/github/authentication.go b/scm/github/authentication.go index c2d27b14c..3b4ccf14b 100644 --- a/scm/github/authentication.go +++ b/scm/github/authentication.go @@ -14,7 +14,7 @@ import ( "github.com/go-vela/server/random" "github.com/go-vela/types/library" - "github.com/google/go-github/v42/github" + "github.com/google/go-github/v53/github" ) // Authorize uses the given access token to authorize the user. @@ -38,8 +38,6 @@ func (c *client) Login(w http.ResponseWriter, r *http.Request) (string, error) { c.Logger.Trace("processing login request") // generate a random string for creating the OAuth state - // - // nolint: gomnd // ignore magic number oAuthState, err := random.GenerateRandomString(32) if err != nil { return "", err @@ -59,8 +57,6 @@ func (c *client) Login(w http.ResponseWriter, r *http.Request) (string, error) { // Authenticate completes the authentication workflow for the session // and returns the remote user details. -// -// nolint: lll // ignore long line length due to variable names func (c *client) Authenticate(w http.ResponseWriter, r *http.Request, oAuthState string) (*library.User, error) { c.Logger.Trace("authenticating user") @@ -136,7 +132,8 @@ func (c *client) AuthenticateToken(r *http.Request) (*library.User, error) { // check if the provided token was created by Vela _, resp, err := client.Authorizations.Check(context.Background(), c.config.ClientID, token) // check if the error is of type ErrorResponse - if gerr, ok := err.(*github.ErrorResponse); ok { + var gerr *github.ErrorResponse + if errors.As(err, &gerr) { // check the status code switch gerr.Response.StatusCode { // 404 is expected when non vela token is used diff --git a/scm/github/authentication_test.go b/scm/github/authentication_test.go index 3e1c50942..d1b369a1f 100644 --- a/scm/github/authentication_test.go +++ b/scm/github/authentication_test.go @@ -411,6 +411,7 @@ func TestGithub_AuthenticateToken_Vela_OAuth(t *testing.T) { // run test _, err := client.AuthenticateToken(context.Request) + if resp.Code != http.StatusOK { t.Errorf("AuthenticateToken returned %v, want %v", resp.Code, http.StatusOK) } diff --git a/scm/github/changeset.go b/scm/github/changeset.go index 6a47edce0..da8ca221a 100644 --- a/scm/github/changeset.go +++ b/scm/github/changeset.go @@ -10,7 +10,7 @@ import ( "github.com/sirupsen/logrus" "github.com/go-vela/types/library" - "github.com/google/go-github/v42/github" + "github.com/google/go-github/v53/github" ) // Changeset captures the list of files changed for a commit. @@ -31,7 +31,7 @@ func (c *client) Changeset(u *library.User, r *library.Repo, sha string) ([]stri // send API call to capture the commit commit, _, err := client.Repositories.GetCommit(ctx, r.GetOrg(), r.GetName(), sha, &opts) if err != nil { - return nil, fmt.Errorf("Repositories.GetCommit returned error: %v", err) + return nil, fmt.Errorf("Repositories.GetCommit returned error: %w", err) } // iterate through each file in the commit @@ -62,7 +62,7 @@ func (c *client) ChangesetPR(u *library.User, r *library.Repo, number int) ([]st // send API call to capture the files from the pull request files, resp, err := client.PullRequests.ListFiles(ctx, r.GetOrg(), r.GetName(), number, &opts) if err != nil { - return nil, fmt.Errorf("PullRequests.ListFiles returned error: %v", err) + return nil, fmt.Errorf("PullRequests.ListFiles returned error: %w", err) } f = append(f, files...) diff --git a/scm/github/deployment.go b/scm/github/deployment.go index 799948a74..fea3f8d03 100644 --- a/scm/github/deployment.go +++ b/scm/github/deployment.go @@ -11,12 +11,10 @@ import ( "github.com/go-vela/types/library" "github.com/go-vela/types/raw" - "github.com/google/go-github/v42/github" + "github.com/google/go-github/v53/github" ) // GetDeployment gets a deployment from the GitHub repo. -// -// nolint: lll // ignore long line length due to variable names func (c *client) GetDeployment(u *library.User, r *library.Repo, id int64) (*library.Deployment, error) { c.Logger.WithFields(logrus.Fields{ "org": r.GetOrg(), @@ -34,6 +32,7 @@ func (c *client) GetDeployment(u *library.User, r *library.Repo, id int64) (*lib } var payload *raw.StringSliceMap + err = json.Unmarshal(deployment.Payload, &payload) if err != nil { c.Logger.Tracef("Unable to unmarshal payload for deployment id %v", deployment.ID) @@ -96,8 +95,6 @@ func (c *client) GetDeploymentCount(u *library.User, r *library.Repo) (int64, er } // GetDeploymentList gets a list of deployments from the GitHub repo. -// -// nolint: lll // ignore long line length due to variable names func (c *client) GetDeploymentList(u *library.User, r *library.Repo, page, perPage int) ([]*library.Deployment, error) { c.Logger.WithFields(logrus.Fields{ "org": r.GetOrg(), @@ -128,6 +125,7 @@ func (c *client) GetDeploymentList(u *library.User, r *library.Repo, page, perPa // iterate through all API results for _, deployment := range d { var payload *raw.StringSliceMap + err := json.Unmarshal(deployment.Payload, &payload) if err != nil { c.Logger.Tracef("Unable to unmarshal payload for deployment id %v", deployment.ID) diff --git a/scm/github/doc.go b/scm/github/doc.go index 804a33b12..e47f97555 100644 --- a/scm/github/doc.go +++ b/scm/github/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/scm/github" +// import "github.com/go-vela/server/scm/github" package github diff --git a/scm/github/github.go b/scm/github/github.go index f3d8e5665..ebae7ec76 100644 --- a/scm/github/github.go +++ b/scm/github/github.go @@ -9,7 +9,7 @@ import ( "fmt" "net/url" - "github.com/google/go-github/v42/github" + "github.com/google/go-github/v53/github" "github.com/sirupsen/logrus" "golang.org/x/oauth2" @@ -25,6 +25,7 @@ const ( eventDeployment = "deployment" eventIssueComment = "issue_comment" eventRepository = "repository" + eventInitialize = "initialize" ) var ctx = context.Background() @@ -61,7 +62,7 @@ type client struct { // New returns a SCM implementation that integrates with // a GitHub or a GitHub Enterprise instance. // -// nolint: revive // ignore returning unexported client +//nolint:revive // ignore returning unexported client func New(opts ...ClientOpt) (*client, error) { // create new GitHub client c := new(client) @@ -120,7 +121,7 @@ func New(opts ...ClientOpt) (*client, error) { // // This function is intended for running tests only. // -// nolint: revive // ignore returning unexported client +//nolint:revive // ignore returning unexported client func NewTest(urls ...string) (*client, error) { address := urls[0] server := address diff --git a/scm/github/github_test.go b/scm/github/github_test.go index 059b6392d..1c10db60a 100644 --- a/scm/github/github_test.go +++ b/scm/github/github_test.go @@ -12,7 +12,7 @@ import ( "reflect" "testing" - "github.com/google/go-github/v42/github" + "github.com/google/go-github/v53/github" "golang.org/x/oauth2" ) @@ -76,12 +76,12 @@ func TestGithub_newClientToken(t *testing.T) { // run test got := client.newClientToken("foobar") - // nolint: staticcheck // ignore false positive + //nolint:staticcheck // ignore false positive if got == nil { t.Errorf("newClientToken is nil, want %v", want) } - // nolint: staticcheck // ignore false positive + //nolint:staticcheck // ignore false positive if !reflect.DeepEqual(got.BaseURL, want.BaseURL) { t.Errorf("newClientToken BaseURL is %v, want %v", got.BaseURL, want.BaseURL) } diff --git a/scm/github/org.go b/scm/github/org.go new file mode 100644 index 000000000..ef9e43ca4 --- /dev/null +++ b/scm/github/org.go @@ -0,0 +1,43 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package github + +import ( + "net/http" + + "github.com/sirupsen/logrus" + + "github.com/go-vela/types/library" +) + +// GetOrgName gets org name from Github. +func (c *client) GetOrgName(u *library.User, o string) (string, error) { + c.Logger.WithFields(logrus.Fields{ + "org": o, + "user": u.GetName(), + }).Tracef("retrieving org information for %s", o) + + // create GitHub OAuth client with user's token + client := c.newClientToken(u.GetToken()) + + // send an API call to get the org info + orgInfo, resp, err := client.Organizations.Get(ctx, o) + + orgName := orgInfo.GetLogin() + + // if org is not found, return the personal org + if resp.StatusCode == http.StatusNotFound { + user, _, err := client.Users.Get(ctx, "") + if err != nil { + return "", err + } + + orgName = user.GetLogin() + } else if err != nil { + return "", err + } + + return orgName, nil +} diff --git a/scm/github/org_test.go b/scm/github/org_test.go new file mode 100644 index 000000000..b0512f9b9 --- /dev/null +++ b/scm/github/org_test.go @@ -0,0 +1,131 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package github + +import ( + "net/http" + "net/http/httptest" + "reflect" + "testing" + + "github.com/gin-gonic/gin" + + "github.com/go-vela/types/library" +) + +func TestGithub_GetOrgName(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/orgs/:org", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/get_org.json") + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + u := new(library.User) + u.SetName("foo") + u.SetToken("bar") + + want := "github" + + client, _ := NewTest(s.URL) + + // run test + got, err := client.GetOrgName(u, "github") + + if resp.Code != http.StatusOK { + t.Errorf("GetOrgName returned %v, want %v", resp.Code, http.StatusOK) + } + + if err != nil { + t.Errorf("GetOrgName returned err: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("GetOrgName is %v, want %v", got, want) + } +} + +func TestGithub_GetOrgName_Personal(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/user", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/user.json") + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + u := new(library.User) + u.SetName("foo") + u.SetToken("bar") + + want := "octocat" + + client, _ := NewTest(s.URL) + + // run test + got, err := client.GetOrgName(u, "octocat") + + if resp.Code != http.StatusOK { + t.Errorf("GetOrgName returned %v, want %v", resp.Code, http.StatusOK) + } + + if err != nil { + t.Errorf("GetOrgName returned err: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("GetOrgName is %v, want %v", got, want) + } +} + +func TestGithub_GetOrgName_Fail(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/orgs/:org", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusNotFound) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + u := new(library.User) + u.SetName("foo") + u.SetToken("bar") + + client, _ := NewTest(s.URL) + + // run test + _, err := client.GetOrgName(u, "octocat") + + if err == nil { + t.Error("GetOrgName should return error") + } +} diff --git a/scm/github/repo.go b/scm/github/repo.go index 9f45bfc7f..e69c7220c 100644 --- a/scm/github/repo.go +++ b/scm/github/repo.go @@ -15,17 +15,17 @@ import ( "github.com/go-vela/types/constants" "github.com/go-vela/types/library" - "github.com/google/go-github/v42/github" + "github.com/google/go-github/v53/github" ) // ConfigBackoff is a wrapper for Config that will retry five times if the function // fails to retrieve the yaml/yml file. -// nolint: lll // ignore long line length due to input arguments func (c *client) ConfigBackoff(u *library.User, r *library.Repo, ref string) (data []byte, err error) { // number of times to retry retryLimit := 5 for i := 0; i < retryLimit; i++ { + logrus.Debugf("Fetching config file - Attempt %d", i+1) // attempt to fetch the config data, err = c.Config(u, r, ref) @@ -98,7 +98,7 @@ func (c *client) Disable(u *library.User, org, name string) error { "org": org, "repo": name, "user": u.GetName(), - }).Tracef("deleting repository webhook for %s/%s", org, name) + }).Tracef("deleting repository webhooks for %s/%s", org, name) // create GitHub OAuth client with user's token client := c.newClientToken(*u.Token) @@ -132,6 +132,12 @@ func (c *client) Disable(u *library.User, org, name string) error { // skip if we have no hook IDs if len(ids) == 0 { + c.Logger.WithFields(logrus.Fields{ + "org": org, + "repo": name, + "user": u.GetName(), + }).Warnf("no repository webhooks matching %s/webhook found for %s/%s", c.config.ServerWebhookAddress, org, name) + return nil } @@ -145,47 +151,120 @@ func (c *client) Disable(u *library.User, org, name string) error { } // Enable activates a repo by creating the webhook. -func (c *client) Enable(u *library.User, org, name, secret string) (string, error) { +func (c *client) Enable(u *library.User, r *library.Repo, h *library.Hook) (*library.Hook, string, error) { c.Logger.WithFields(logrus.Fields{ - "org": org, - "repo": name, + "org": r.GetOrg(), + "repo": r.GetName(), "user": u.GetName(), - }).Tracef("creating repository webhook for %s/%s", org, name) + }).Tracef("creating repository webhook for %s/%s", r.GetOrg(), r.GetName()) // create GitHub OAuth client with user's token client := c.newClientToken(*u.Token) + // always listen to repository events in case of repo name change + events := []string{eventRepository} + + if r.GetAllowComment() { + events = append(events, eventIssueComment) + } + + if r.GetAllowDeploy() { + events = append(events, eventDeployment) + } + + if r.GetAllowPull() { + events = append(events, eventPullRequest) + } + + if r.GetAllowPush() || r.GetAllowTag() { + events = append(events, eventPush) + } + // create the hook object to make the API call hook := &github.Hook{ - Events: []string{ - eventPush, - eventPullRequest, - eventDeployment, - eventIssueComment, - eventRepository, - }, + Events: events, Config: map[string]interface{}{ "url": fmt.Sprintf("%s/webhook", c.config.ServerWebhookAddress), "content_type": "form", - "secret": secret, + "secret": r.GetHash(), }, Active: github.Bool(true), } // send API call to create the webhook - _, resp, err := client.Repositories.CreateHook(ctx, org, name, hook) + hookInfo, resp, err := client.Repositories.CreateHook(ctx, r.GetOrg(), r.GetName(), hook) + + // create the first hook for the repo and record its ID from GitHub + webhook := new(library.Hook) + webhook.SetWebhookID(hookInfo.GetID()) + webhook.SetSourceID(r.GetName() + "-" + eventInitialize) + webhook.SetCreated(hookInfo.GetCreatedAt().Unix()) + webhook.SetEvent(eventInitialize) + webhook.SetNumber(h.GetNumber() + 1) + webhook.SetStatus(constants.StatusSuccess) switch resp.StatusCode { case http.StatusUnprocessableEntity: - return "", fmt.Errorf("repo already enabled") + return nil, "", fmt.Errorf("repo already enabled") case http.StatusNotFound: - return "", fmt.Errorf("repo not found") + return nil, "", fmt.Errorf("repo not found") } // create the URL for the repo - url := fmt.Sprintf("%s/%s/%s", c.config.Address, org, name) + url := fmt.Sprintf("%s/%s/%s", c.config.Address, r.GetOrg(), r.GetName()) + + return webhook, url, err +} + +// Update edits a repo webhook. +func (c *client) Update(u *library.User, r *library.Repo, hookID int64) error { + c.Logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + "user": u.GetName(), + }).Tracef("updating repository webhook for %s/%s", r.GetOrg(), r.GetName()) + + // create GitHub OAuth client with user's token + client := c.newClientToken(*u.Token) + + // always listen to repository events in case of repo name change + events := []string{eventRepository} + + if r.GetAllowComment() { + events = append(events, eventIssueComment) + } + + if r.GetAllowDeploy() { + events = append(events, eventDeployment) + } + + if r.GetAllowPull() { + events = append(events, eventPullRequest) + } + + if r.GetAllowPush() || r.GetAllowTag() { + events = append(events, eventPush) + } + + // create the hook object to make the API call + hook := &github.Hook{ + Events: events, + Config: map[string]interface{}{ + "url": fmt.Sprintf("%s/webhook", c.config.ServerWebhookAddress), + "content_type": "form", + "secret": r.GetHash(), + }, + Active: github.Bool(true), + } + + // send API call to update the webhook + _, _, err := client.Repositories.EditHook(ctx, r.GetOrg(), r.GetName(), hookID, hook) - return url, err + if err != nil { + return err + } + + return nil } // Status sends the commit status for the given SHA from the GitHub repo. @@ -218,7 +297,7 @@ func (c *client) Status(u *library.User, b *library.Build, org, name string) err state = "success" description = "the build was successful" case constants.StatusFailure: - // nolint: goconst // ignore making constant + //nolint:goconst // ignore making constant state = "failure" description = "the build has failed" case constants.StatusCanceled: @@ -310,6 +389,26 @@ func (c *client) GetRepo(u *library.User, r *library.Repo) (*library.Repo, error return toLibraryRepo(*repo), nil } +// GetOrgAndRepoName returns the name of the org and the repository in the SCM. +func (c *client) GetOrgAndRepoName(u *library.User, o string, r string) (string, string, error) { + c.Logger.WithFields(logrus.Fields{ + "org": o, + "repo": r, + "user": u.GetName(), + }).Tracef("retrieving repository information for %s/%s", o, r) + + // create GitHub OAuth client with user's token + client := c.newClientToken(u.GetToken()) + + // send an API call to get the repo info + repo, _, err := client.Repositories.Get(ctx, o, r) + if err != nil { + return "", "", err + } + + return repo.GetOwner().GetLogin(), repo.GetName(), nil +} + // ListUserRepos returns a list of all repos the user has access to. func (c *client) ListUserRepos(u *library.User) ([]*library.Repo, error) { c.Logger.WithFields(logrus.Fields{ @@ -332,7 +431,7 @@ func (c *client) ListUserRepos(u *library.User) ([]*library.Repo, error) { // send API call to capture the user's repos repos, resp, err := client.Repositories.List(ctx, "", opts) if err != nil { - return nil, fmt.Errorf("unable to list user repos: %v", err) + return nil, fmt.Errorf("unable to list user repos: %w", err) } r = append(r, repos...) @@ -347,6 +446,12 @@ func (c *client) ListUserRepos(u *library.User) ([]*library.Repo, error) { // iterate through each repo for the user for _, repo := range r { + // skip if the repo is void + // fixes an issue with GitHub replication being out of sync + if repo == nil { + continue + } + // skip if the repo is archived or disabled if repo.GetArchived() || repo.GetDisabled() { continue @@ -360,20 +465,29 @@ func (c *client) ListUserRepos(u *library.User) ([]*library.Repo, error) { // toLibraryRepo does a partial conversion of a github repo to a library repo. func toLibraryRepo(gr github.Repository) *library.Repo { + // setting the visbility to match the SCM visbility + var visibility string + if *gr.Private { + visibility = constants.VisibilityPrivate + } else { + visibility = constants.VisibilityPublic + } + return &library.Repo{ - Org: gr.GetOwner().Login, - Name: gr.Name, - FullName: gr.FullName, - Link: gr.HTMLURL, - Clone: gr.CloneURL, - Branch: gr.DefaultBranch, - Private: gr.Private, + Org: gr.GetOwner().Login, + Name: gr.Name, + FullName: gr.FullName, + Link: gr.HTMLURL, + Clone: gr.CloneURL, + Branch: gr.DefaultBranch, + Topics: &gr.Topics, + Private: gr.Private, + Visibility: &visibility, } } // GetPullRequest defines a function that retrieves // a pull request for a repo. -// nolint:lll // function signature is lengthy func (c *client) GetPullRequest(u *library.User, r *library.Repo, number int) (string, string, string, string, error) { c.Logger.WithFields(logrus.Fields{ "org": r.GetOrg(), @@ -422,6 +536,7 @@ func (c *client) GetHTMLURL(u *library.User, org, repo, name, ref string) (strin // data is not nil if the file exists if data != nil { URL := data.GetHTMLURL() + if err != nil { return "", err } @@ -431,3 +546,22 @@ func (c *client) GetHTMLURL(u *library.User, org, repo, name, ref string) (strin return "", fmt.Errorf("no valid repository contents found") } + +// GetBranch defines a function that retrieves a branch for a repo. +func (c *client) GetBranch(u *library.User, r *library.Repo, branch string) (string, string, error) { + c.Logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + "user": u.GetName(), + }).Tracef("retrieving branch %s for repo %s", branch, r.GetFullName()) + + // create GitHub OAuth client with user's token + client := c.newClientToken(u.GetToken()) + + data, _, err := client.Repositories.GetBranch(ctx, r.GetOrg(), r.GetName(), branch, true) + if err != nil { + return "", "", err + } + + return data.GetName(), data.GetCommit().GetSHA(), nil +} diff --git a/scm/github/repo_test.go b/scm/github/repo_test.go index 55c143fa6..858d60f18 100644 --- a/scm/github/repo_test.go +++ b/scm/github/repo_test.go @@ -6,9 +6,9 @@ package github import ( "fmt" - "io/ioutil" "net/http" "net/http/httptest" + "os" "reflect" "strings" "testing" @@ -41,7 +41,7 @@ func TestGithub_Config_YML(t *testing.T) { s := httptest.NewServer(engine) defer s.Close() - want, err := ioutil.ReadFile("testdata/pipeline.yml") + want, err := os.ReadFile("testdata/pipeline.yml") if err != nil { t.Errorf("Config reading file returned err: %v", err) } @@ -95,7 +95,7 @@ func TestGithub_ConfigBackoff_YML(t *testing.T) { s := httptest.NewServer(engine) defer s.Close() - want, err := ioutil.ReadFile("testdata/pipeline.yml") + want, err := os.ReadFile("testdata/pipeline.yml") if err != nil { t.Errorf("Config reading file returned err: %v", err) } @@ -155,6 +155,7 @@ func TestGithub_Config_YML_BadRequest(t *testing.T) { // run test got, err := client.Config(u, r, "") + if resp.Code != http.StatusOK { t.Errorf("Config returned %v, want %v", resp.Code, http.StatusOK) } @@ -190,7 +191,7 @@ func TestGithub_Config_YAML(t *testing.T) { s := httptest.NewServer(engine) defer s.Close() - want, err := ioutil.ReadFile("testdata/pipeline.yml") + want, err := os.ReadFile("testdata/pipeline.yml") if err != nil { t.Errorf("Config reading file returned err: %v", err) } @@ -244,7 +245,7 @@ func TestGithub_Config_Star(t *testing.T) { s := httptest.NewServer(engine) defer s.Close() - want, err := ioutil.ReadFile("testdata/pipeline.yml") + want, err := os.ReadFile("testdata/pipeline.yml") if err != nil { t.Errorf("Config reading file returned err: %v", err) } @@ -299,7 +300,7 @@ func TestGithub_Config_Py(t *testing.T) { s := httptest.NewServer(engine) defer s.Close() - want, err := ioutil.ReadFile("testdata/pipeline.yml") + want, err := os.ReadFile("testdata/pipeline.yml") if err != nil { t.Errorf("Config reading file returned err: %v", err) } @@ -601,10 +602,27 @@ func TestGithub_Enable(t *testing.T) { u.SetName("foo") u.SetToken("bar") + wantHook := new(library.Hook) + wantHook.SetWebhookID(1) + wantHook.SetSourceID("bar-initialize") + wantHook.SetCreated(1315329987) + wantHook.SetNumber(1) + wantHook.SetEvent("initialize") + wantHook.SetStatus("success") + + r := new(library.Repo) + r.SetID(1) + r.SetName("bar") + r.SetOrg("foo") + r.SetHash("secret") + r.SetAllowPush(true) + r.SetAllowPull(true) + r.SetAllowDeploy(true) + client, _ := NewTest(s.URL) // run test - _, err := client.Enable(u, "foo", "bar", "secret") + got, _, err := client.Enable(u, r, new(library.Hook)) if resp.Code != http.StatusOK { t.Errorf("Enable returned %v, want %v", resp.Code, http.StatusOK) @@ -613,6 +631,57 @@ func TestGithub_Enable(t *testing.T) { if err != nil { t.Errorf("Enable returned err: %v", err) } + + if !reflect.DeepEqual(wantHook, got) { + t.Errorf("Enable returned hook %v, want %v", got, wantHook) + } +} + +func TestGithub_Update(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.PATCH("/api/v3/repos/:org/:repo/hooks/:hook_id", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/hook.json") + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + u := new(library.User) + u.SetName("foo") + u.SetToken("bar") + + r := new(library.Repo) + r.SetID(1) + r.SetName("bar") + r.SetOrg("foo") + r.SetHash("secret") + r.SetAllowPush(true) + r.SetAllowPull(true) + r.SetAllowDeploy(true) + + hookID := int64(1) + + client, _ := NewTest(s.URL) + + // run test + err := client.Update(u, r, hookID) + + if resp.Code != http.StatusOK { + t.Errorf("Update returned %v, want %v", resp.Code, http.StatusOK) + } + + if err != nil { + t.Errorf("Update returned err: %v", err) + } } func TestGithub_Status_Deployment(t *testing.T) { @@ -958,6 +1027,8 @@ func TestGithub_GetRepo(t *testing.T) { want.SetClone("https://github.com/octocat/Hello-World.git") want.SetBranch("master") want.SetPrivate(false) + want.SetTopics([]string{"octocat", "atom", "electron", "api"}) + want.SetVisibility("public") client, _ := NewTest(s.URL) @@ -1012,6 +1083,84 @@ func TestGithub_GetRepo_Fail(t *testing.T) { } } +func TestGithub_GetOrgAndRepoName(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/repos/:owner/:repo", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/get_repo.json") + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + u := new(library.User) + u.SetName("foo") + u.SetToken("bar") + + wantOrg := "octocat" + wantRepo := "Hello-World" + + client, _ := NewTest(s.URL) + + // run test + gotOrg, gotRepo, err := client.GetOrgAndRepoName(u, "octocat", "Hello-World") + + if resp.Code != http.StatusOK { + t.Errorf("GetRepoName returned %v, want %v", resp.Code, http.StatusOK) + } + + if err != nil { + t.Errorf("GetRepoName returned err: %v", err) + } + + if !reflect.DeepEqual(gotOrg, wantOrg) { + t.Errorf("GetRepoName org is %v, want %v", gotOrg, wantOrg) + } + + if !reflect.DeepEqual(gotRepo, wantRepo) { + t.Errorf("GetRepoName repo is %v, want %v", gotRepo, wantRepo) + } +} + +func TestGithub_GetOrgAndRepoName_Fail(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/repos/:owner/:repo", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusNotFound) + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + u := new(library.User) + u.SetName("foo") + u.SetToken("bar") + + client, _ := NewTest(s.URL) + + // run test + _, _, err := client.GetOrgAndRepoName(u, "octocat", "Hello-World") + + if err == nil { + t.Error("GetRepoName should return error") + } +} + func TestGithub_ListUserRepos(t *testing.T) { // setup context gin.SetMode(gin.TestMode) @@ -1042,6 +1191,8 @@ func TestGithub_ListUserRepos(t *testing.T) { r.SetClone("https://github.com/octocat/Hello-World.git") r.SetBranch("master") r.SetPrivate(false) + r.SetTopics([]string{"octocat", "atom", "electron", "api"}) + r.SetVisibility("public") want := []*library.Repo{r} @@ -1153,3 +1304,52 @@ func TestGithub_GetPullRequest(t *testing.T) { t.Errorf("HeadRef is %v, want %v", gotHeadRef, wantHeadRef) } } + +func TestGithub_GetBranch(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.GET("/api/v3/repos/:owner/:repo/branches/:branch", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/branch.json") + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + u := new(library.User) + u.SetName("foo") + u.SetToken("bar") + + r := new(library.Repo) + r.SetOrg("octocat") + r.SetName("Hello-World") + r.SetFullName("octocat/Hello-World") + r.SetBranch("main") + + wantBranch := "main" + wantCommit := "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d" + + client, _ := NewTest(s.URL) + + // run test + gotBranch, gotCommit, err := client.GetBranch(u, r, "main") + + if err != nil { + t.Errorf("Status returned err: %v", err) + } + + if !strings.EqualFold(gotBranch, wantBranch) { + t.Errorf("Branch is %v, want %v", gotBranch, wantBranch) + } + + if !strings.EqualFold(gotCommit, wantCommit) { + t.Errorf("Commit is %v, want %v", gotCommit, wantCommit) + } +} diff --git a/scm/github/testdata/branch.json b/scm/github/testdata/branch.json new file mode 100644 index 000000000..b133e7b38 --- /dev/null +++ b/scm/github/testdata/branch.json @@ -0,0 +1,101 @@ +{ + "name": "main", + "commit": { + "sha": "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", + "node_id": "MDY6Q29tbWl0MTI5NjI2OTo3ZmQxYTYwYjAxZjkxYjMxNGY1OTk1NWE0ZTRkNGU4MGQ4ZWRmMTFk", + "commit": { + "author": { + "name": "The Octocat", + "email": "octocat@nowhere.com", + "date": "2012-03-06T23:06:50Z" + }, + "committer": { + "name": "The Octocat", + "email": "octocat@nowhere.com", + "date": "2012-03-06T23:06:50Z" + }, + "message": "Merge pull request #6 from Spaceghost/patch-1\n\nNew line at end of file.", + "tree": { + "sha": "b4eecafa9be2f2006ce1b709d6857b07069b4608", + "url": "https://api.github.com/repos/octocat/Hello-World/git/trees/b4eecafa9be2f2006ce1b709d6857b07069b4608" + }, + "url": "https://api.github.com/repos/octocat/Hello-World/git/commits/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", + "comment_count": 77, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null + } + }, + "url": "https://api.github.com/repos/octocat/Hello-World/commits/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", + "html_url": "https://github.com/octocat/Hello-World/commit/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/commits/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d/comments", + "author": { + "login": "octocat", + "id": 583231, + "node_id": "MDQ6VXNlcjU4MzIzMQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/583231?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "committer": { + "login": "octocat", + "id": 583231, + "node_id": "MDQ6VXNlcjU4MzIzMQ==", + "avatar_url": "https://avatars.githubusercontent.com/u/583231?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "parents": [ + { + "sha": "553c2077f0edc3d5dc5d17262f6aa498e69d6f8e", + "url": "https://api.github.com/repos/octocat/Hello-World/commits/553c2077f0edc3d5dc5d17262f6aa498e69d6f8e", + "html_url": "https://github.com/octocat/Hello-World/commit/553c2077f0edc3d5dc5d17262f6aa498e69d6f8e" + }, + { + "sha": "762941318ee16e59dabbacb1b4049eec22f0d303", + "url": "https://api.github.com/repos/octocat/Hello-World/commits/762941318ee16e59dabbacb1b4049eec22f0d303", + "html_url": "https://github.com/octocat/Hello-World/commit/762941318ee16e59dabbacb1b4049eec22f0d303" + } + ] + }, + "_links": { + "self": "https://api.github.com/repos/octocat/Hello-World/branches/main", + "html": "https://github.com/octocat/Hello-World/tree/main" + }, + "protected": false, + "protection": { + "enabled": false, + "required_status_checks": { + "enforcement_level": "off", + "contexts": [], + "checks": [] + } + }, + "protection_url": "https://api.github.com/repos/octocat/Hello-World/branches/main/protection" +} \ No newline at end of file diff --git a/scm/github/testdata/delivery_summaries.json b/scm/github/testdata/delivery_summaries.json new file mode 100644 index 000000000..12d8e3ae5 --- /dev/null +++ b/scm/github/testdata/delivery_summaries.json @@ -0,0 +1,72 @@ +[ + { + "id": 22948189250, + "guid": "b6552230-aee1-11ec-949c-91a5dc3f8159", + "delivered_at": "2022-03-28T21:54:59Z", + "redelivery": false, + "duration": 0.4, + "status": "Invalid HTTP Response: 404", + "status_code": 404, + "event": "pull_request", + "action": "edited", + "installation_id": null, + "repository_id": 219518422, + "url": "" + }, + { + "id": 22948188373, + "guid": "b595f0e0-aee1-11ec-86cf-9418381395c4", + "delivered_at": "2022-03-28T21:54:58Z", + "redelivery": false, + "duration": 0.31, + "status": "Invalid HTTP Response: 404", + "status_code": 404, + "event": "pull_request", + "action": "synchronize", + "installation_id": null, + "repository_id": 219518422, + "url": "" + }, + { + "id": 22948187625, + "guid": "b4f5b1a2-aee1-11ec-94e7-28efa21cef07", + "delivered_at": "2022-03-28T21:54:57Z", + "redelivery": false, + "duration": 0.28, + "status": "Invalid HTTP Response: 404", + "status_code": 404, + "event": "push", + "action": null, + "installation_id": null, + "repository_id": 219518422, + "url": "" + }, + { + "id": 22939666537, + "guid": "5275f5b0-aec7-11ec-9778-f09445731fb3", + "delivered_at": "2022-03-28T18:46:05Z", + "redelivery": false, + "duration": 0.29, + "status": "Invalid HTTP Response: 404", + "status_code": 404, + "event": "pull_request", + "action": "synchronize", + "installation_id": null, + "repository_id": 219518422, + "url": "" + }, + { + "id": 22939665716, + "guid": "51d8149e-aec7-11ec-8377-b51dc14c4d81", + "delivered_at": "2022-03-28T18:46:04Z", + "redelivery": false, + "duration": 0.3, + "status": "Invalid HTTP Response: 404", + "status_code": 404, + "event": "push", + "action": null, + "installation_id": null, + "repository_id": 219518422, + "url": "" + } +] \ No newline at end of file diff --git a/scm/github/testdata/get_org.json b/scm/github/testdata/get_org.json new file mode 100644 index 000000000..e43ba5655 --- /dev/null +++ b/scm/github/testdata/get_org.json @@ -0,0 +1,53 @@ +{ + "login": "github", + "id": 1, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE=", + "url": "https://api.github.com/orgs/github", + "repos_url": "https://api.github.com/orgs/github/repos", + "events_url": "https://api.github.com/orgs/github/events", + "hooks_url": "https://api.github.com/orgs/github/hooks", + "issues_url": "https://api.github.com/orgs/github/issues", + "members_url": "https://api.github.com/orgs/github/members{/member}", + "public_members_url": "https://api.github.com/orgs/github/public_members{/member}", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "description": "A great organization", + "name": "github", + "company": "GitHub", + "blog": "https://github.com/blog", + "location": "San Francisco", + "email": "octocat@github.com", + "twitter_username": "github", + "is_verified": true, + "has_organization_projects": true, + "has_repository_projects": true, + "public_repos": 2, + "public_gists": 1, + "followers": 20, + "following": 0, + "html_url": "https://github.com/octocat", + "created_at": "2008-01-14T04:33:35Z", + "updated_at": "2014-03-03T18:58:10Z", + "type": "Organization", + "total_private_repos": 100, + "owned_private_repos": 100, + "private_gists": 81, + "disk_usage": 10000, + "collaborators": 8, + "billing_email": "mona@github.com", + "plan": { + "name": "Medium", + "space": 400, + "private_repos": 20, + "filled_seats": 4, + "seats": 5 + }, + "default_repository_permission": "read", + "members_can_create_repositories": true, + "two_factor_requirement_enabled": true, + "members_allowed_repository_creation_type": "all", + "members_can_create_public_repositories": false, + "members_can_create_private_repositories": false, + "members_can_create_internal_repositories": false, + "members_can_create_pages": true, + "members_can_fork_private_repositories": false +} \ No newline at end of file diff --git a/scm/github/testdata/hooks/push.json b/scm/github/testdata/hooks/push.json index 9beb5bc86..7058efe6b 100644 --- a/scm/github/testdata/hooks/push.json +++ b/scm/github/testdata/hooks/push.json @@ -156,7 +156,11 @@ "watchers": 0, "default_branch": "master", "stargazers": 0, - "master_branch": "master" + "master_branch": "master", + "topics": [ + "go", + "vela" + ] }, "pusher": { "name": "Codertocat", diff --git a/scm/github/testdata/hooks/push_no_sender.json b/scm/github/testdata/hooks/push_no_sender.json index 79eac0888..f9bef26d3 100644 --- a/scm/github/testdata/hooks/push_no_sender.json +++ b/scm/github/testdata/hooks/push_no_sender.json @@ -156,7 +156,11 @@ "watchers": 0, "default_branch": "master", "stargazers": 0, - "master_branch": "master" + "master_branch": "master", + "topics": [ + "go", + "vela" + ] }, "pusher": { "name": "Codertocat", diff --git a/scm/github/testdata/hooks/repository_archived.json b/scm/github/testdata/hooks/repository_archived.json new file mode 100644 index 000000000..b9c0ff71b --- /dev/null +++ b/scm/github/testdata/hooks/repository_archived.json @@ -0,0 +1,133 @@ +{ + "action": "archived", + "repository": { + "id": 118, + "node_id": "MDEwOlJlcG9zaXRvcnkxMTg=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "login": "Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://octocoders.github.io/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World", + "forks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/forks", + "keys_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/events", + "assignees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/merges", + "archive_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/deployments", + "created_at": "2019-05-15T19:37:07Z", + "updated_at": "2019-05-15T19:38:25Z", + "pushed_at": "2019-05-15T19:38:23Z", + "git_url": "git://octocoders.github.io/Codertocat/Hello-World.git", + "ssh_url": "git@octocoders.github.io:Codertocat/Hello-World.git", + "clone_url": "https://octocoders.github.io/Codertocat/Hello-World.git", + "svn_url": "https://octocoders.github.io/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Ruby", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "archived": true, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" + }, + "enterprise": { + "id": 1, + "slug": "github", + "name": "GitHub", + "node_id": "MDg6QnVzaW5lc3Mx", + "avatar_url": "https://octocoders.github.io/avatars/b/1?", + "description": null, + "website_url": null, + "html_url": "https://octocoders.github.io/businesses/github", + "created_at": "2019-05-14T19:31:12Z", + "updated_at": "2019-05-14T19:31:12Z" + }, + "sender": { + "login": "Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "installation": { + "id": 5, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNQ==" + } +} \ No newline at end of file diff --git a/scm/github/testdata/hooks/repository_edited.json b/scm/github/testdata/hooks/repository_edited.json new file mode 100644 index 000000000..c9a41a1a1 --- /dev/null +++ b/scm/github/testdata/hooks/repository_edited.json @@ -0,0 +1,142 @@ +{ + "action": "edited", + "changes": { + "default_branch": { + "from": "not-main" + } + }, + "repository": { + "id": 118, + "node_id": "MDEwOlJlcG9zaXRvcnkxMTg=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "login": "Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://octocoders.github.io/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World", + "forks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/forks", + "keys_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/events", + "assignees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/merges", + "archive_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/deployments", + "created_at": "2019-05-15T19:37:07Z", + "updated_at": "2019-05-15T19:38:25Z", + "pushed_at": "2019-05-15T19:38:23Z", + "git_url": "git://octocoders.github.io/Codertocat/Hello-World.git", + "ssh_url": "git@octocoders.github.io:Codertocat/Hello-World.git", + "clone_url": "https://octocoders.github.io/Codertocat/Hello-World.git", + "svn_url": "https://octocoders.github.io/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Ruby", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "main", + "topics": [ + "cloud", + "security" + ] + }, + "enterprise": { + "id": 1, + "slug": "github", + "name": "GitHub", + "node_id": "MDg6QnVzaW5lc3Mx", + "avatar_url": "https://octocoders.github.io/avatars/b/1?", + "description": null, + "website_url": null, + "html_url": "https://octocoders.github.io/businesses/github", + "created_at": "2019-05-14T19:31:12Z", + "updated_at": "2019-05-14T19:31:12Z" + }, + "sender": { + "login": "Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "installation": { + "id": 5, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNQ==" + } +} \ No newline at end of file diff --git a/scm/github/testdata/hooks/repository_transferred.json b/scm/github/testdata/hooks/repository_transferred.json new file mode 100644 index 000000000..2fdea23f6 --- /dev/null +++ b/scm/github/testdata/hooks/repository_transferred.json @@ -0,0 +1,159 @@ +{ + "action": "transferred", + "changes": { + "owner": { + "from": { + "user": { + "login": "Old-Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + } + } + } + }, + "repository": { + "id": 118, + "node_id": "MDEwOlJlcG9zaXRvcnkxMTg=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "login": "Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://octocoders.github.io/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World", + "forks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/forks", + "keys_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/events", + "assignees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/merges", + "archive_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://octocoders.github.io/api/v3/repos/Codertocat/Hello-World/deployments", + "created_at": "2019-05-15T19:37:07Z", + "updated_at": "2019-05-15T19:38:25Z", + "pushed_at": "2019-05-15T19:38:23Z", + "git_url": "git://octocoders.github.io/Codertocat/Hello-World.git", + "ssh_url": "git@octocoders.github.io:Codertocat/Hello-World.git", + "clone_url": "https://octocoders.github.io/Codertocat/Hello-World.git", + "svn_url": "https://octocoders.github.io/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Ruby", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" + }, + "enterprise": { + "id": 1, + "slug": "github", + "name": "GitHub", + "node_id": "MDg6QnVzaW5lc3Mx", + "avatar_url": "https://octocoders.github.io/avatars/b/1?", + "description": null, + "website_url": null, + "html_url": "https://octocoders.github.io/businesses/github", + "created_at": "2019-05-14T19:31:12Z", + "updated_at": "2019-05-14T19:31:12Z" + }, + "sender": { + "login": "Codertocat", + "id": 4, + "node_id": "MDQ6VXNlcjQ=", + "avatar_url": "https://octocoders.github.io/avatars/u/4?", + "gravatar_id": "", + "url": "https://octocoders.github.io/api/v3/users/Codertocat", + "html_url": "https://octocoders.github.io/Codertocat", + "followers_url": "https://octocoders.github.io/api/v3/users/Codertocat/followers", + "following_url": "https://octocoders.github.io/api/v3/users/Codertocat/following{/other_user}", + "gists_url": "https://octocoders.github.io/api/v3/users/Codertocat/gists{/gist_id}", + "starred_url": "https://octocoders.github.io/api/v3/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://octocoders.github.io/api/v3/users/Codertocat/subscriptions", + "organizations_url": "https://octocoders.github.io/api/v3/users/Codertocat/orgs", + "repos_url": "https://octocoders.github.io/api/v3/users/Codertocat/repos", + "events_url": "https://octocoders.github.io/api/v3/users/Codertocat/events{/privacy}", + "received_events_url": "https://octocoders.github.io/api/v3/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "installation": { + "id": 5, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uNQ==" + } + } \ No newline at end of file diff --git a/scm/github/webhook.go b/scm/github/webhook.go index 7db25edf2..43615e8cb 100644 --- a/scm/github/webhook.go +++ b/scm/github/webhook.go @@ -5,9 +5,13 @@ package github import ( + "context" "encoding/json" + "errors" "fmt" + "mime" "net/http" + "strconv" "strings" "time" @@ -16,16 +20,25 @@ import ( "github.com/go-vela/types" "github.com/go-vela/types/constants" "github.com/go-vela/types/library" - "github.com/google/go-github/v42/github" + "github.com/google/go-github/v53/github" ) // ProcessWebhook parses the webhook from a repo. +// +//nolint:nilerr // ignore webhook returning nil func (c *client) ProcessWebhook(request *http.Request) (*types.Webhook, error) { c.Logger.Tracef("processing GitHub webhook") h := new(library.Hook) h.SetNumber(1) h.SetSourceID(request.Header.Get("X-GitHub-Delivery")) + + hookID, err := strconv.Atoi(request.Header.Get("X-GitHub-Hook-ID")) + if err != nil { + return nil, fmt.Errorf("unable to convert hook id to int64: %w", err) + } + + h.SetWebhookID(int64(hookID)) h.SetCreated(time.Now().UTC().Unix()) h.SetHost("github.com") h.SetEvent(request.Header.Get("X-GitHub-Event")) @@ -35,7 +48,13 @@ func (c *client) ProcessWebhook(request *http.Request) (*types.Webhook, error) { h.SetHost(request.Header.Get("X-GitHub-Enterprise-Host")) } - payload, err := github.ValidatePayload(request, nil) + // get content type + contentType, _, err := mime.ParseMediaType(request.Header.Get("Content-Type")) + if err != nil { + return nil, err + } + + payload, err := github.ValidatePayloadFromBody(contentType, request.Body, "", nil) if err != nil { return &types.Webhook{Hook: h}, nil } @@ -79,9 +98,36 @@ func (c *client) VerifyWebhook(request *http.Request, r *library.Repo) error { return nil } +// RedeliverWebhook redelivers webhooks from GitHub. +func (c *client) RedeliverWebhook(ctx context.Context, u *library.User, r *library.Repo, h *library.Hook) error { + // create GitHub OAuth client with user's token + //nolint:contextcheck // do not need to pass context in this instance + client := c.newClientToken(*u.Token) + + // capture the delivery ID of the hook using GitHub API + deliveryID, err := c.getDeliveryID(ctx, client, r, h) + if err != nil { + return err + } + + // redeliver the webhook + _, _, err = client.Repositories.RedeliverHookDelivery(ctx, r.GetOrg(), r.GetName(), h.GetWebhookID(), deliveryID) + + if err != nil { + var acceptedError *github.AcceptedError + // Persist if the status received is a 202 Accepted. This + // means the job was added to the queue for GitHub. + if errors.As(err, &acceptedError) { + return nil + } + + return err + } + + return nil +} + // processPushEvent is a helper function to process the push event. -// -// nolint: lll // ignore long line length due to variable names func (c *client) processPushEvent(h *library.Hook, payload *github.PushEvent) (*types.Webhook, error) { c.Logger.WithFields(logrus.Fields{ "org": payload.GetRepo().GetOwner().GetLogin(), @@ -99,6 +145,7 @@ func (c *client) processPushEvent(h *library.Hook, payload *github.PushEvent) (* r.SetClone(repo.GetCloneURL()) r.SetBranch(repo.GetDefaultBranch()) r.SetPrivate(repo.GetPrivate()) + r.SetTopics(repo.Topics) // convert payload to library build b := new(library.Build) @@ -159,8 +206,6 @@ func (c *client) processPushEvent(h *library.Hook, payload *github.PushEvent) (* } // processPREvent is a helper function to process the pull_request event. -// -// nolint: lll // ignore long line length due to variable names func (c *client) processPREvent(h *library.Hook, payload *github.PullRequestEvent) (*types.Webhook, error) { c.Logger.WithFields(logrus.Fields{ "org": payload.GetRepo().GetOwner().GetLogin(), @@ -179,7 +224,7 @@ func (c *client) processPREvent(h *library.Hook, payload *github.PullRequestEven return &types.Webhook{Hook: h}, nil } - // skip if the pull request action is not opened or synchronize + // skip if the pull request action is not opened, synchronize if !strings.EqualFold(payload.GetAction(), "opened") && !strings.EqualFold(payload.GetAction(), "synchronize") { return &types.Webhook{Hook: h}, nil @@ -197,10 +242,12 @@ func (c *client) processPREvent(h *library.Hook, payload *github.PullRequestEven r.SetClone(repo.GetCloneURL()) r.SetBranch(repo.GetDefaultBranch()) r.SetPrivate(repo.GetPrivate()) + r.SetTopics(repo.Topics) // convert payload to library build b := new(library.Build) b.SetEvent(constants.EventPull) + b.SetEventAction(payload.GetAction()) b.SetClone(repo.GetCloneURL()) b.SetSource(payload.GetPullRequest().GetHTMLURL()) b.SetTitle(fmt.Sprintf("%s received from %s", constants.EventPull, repo.GetHTMLURL())) @@ -244,8 +291,6 @@ func (c *client) processPREvent(h *library.Hook, payload *github.PullRequestEven } // processDeploymentEvent is a helper function to process the deployment event. -// -// nolint: lll // ignore long line length due to variable names func (c *client) processDeploymentEvent(h *library.Hook, payload *github.DeploymentEvent) (*types.Webhook, error) { c.Logger.WithFields(logrus.Fields{ "org": payload.GetRepo().GetOwner().GetLogin(), @@ -264,6 +309,7 @@ func (c *client) processDeploymentEvent(h *library.Hook, payload *github.Deploym r.SetClone(repo.GetCloneURL()) r.SetBranch(repo.GetDefaultBranch()) r.SetPrivate(repo.GetPrivate()) + r.SetTopics(repo.Topics) // convert payload to library build b := new(library.Build) @@ -287,8 +333,6 @@ func (c *client) processDeploymentEvent(h *library.Hook, payload *github.Deploym // // sending an API request to GitHub with no // payload provided yields a default of `{}`. - // - // nolint: gomnd // ignore magic number if len(payload.GetDeployment().Payload) > 2 { deployPayload := make(map[string]string) // unmarshal the payload into the expected map[string]string format @@ -334,8 +378,6 @@ func (c *client) processDeploymentEvent(h *library.Hook, payload *github.Deploym } // processIssueCommentEvent is a helper function to process the issue comment event. -// -// nolint: lll // ignore long line length due to variable names func (c *client) processIssueCommentEvent(h *library.Hook, payload *github.IssueCommentEvent) (*types.Webhook, error) { c.Logger.WithFields(logrus.Fields{ "org": payload.GetRepo().GetOwner().GetLogin(), @@ -369,10 +411,12 @@ func (c *client) processIssueCommentEvent(h *library.Hook, payload *github.Issue r.SetClone(repo.GetCloneURL()) r.SetBranch(repo.GetDefaultBranch()) r.SetPrivate(repo.GetPrivate()) + r.SetTopics(repo.Topics) // convert payload to library build b := new(library.Build) b.SetEvent(constants.EventComment) + b.SetEventAction(payload.GetAction()) b.SetClone(repo.GetCloneURL()) b.SetSource(payload.Issue.GetHTMLURL()) b.SetTitle(fmt.Sprintf("%s received from %s", constants.EventComment, repo.GetHTMLURL())) @@ -402,7 +446,7 @@ func (c *client) processIssueCommentEvent(h *library.Hook, payload *github.Issue } // processRepositoryEvent is a helper function to process the repository event. -// nolint: lll // ignore long line length due to error message + func (c *client) processRepositoryEvent(h *library.Hook, payload *github.RepositoryEvent) (*types.Webhook, error) { logrus.Tracef("processing repository event GitHub webhook for %s", payload.GetRepo().GetFullName()) @@ -417,16 +461,27 @@ func (c *client) processRepositoryEvent(h *library.Hook, payload *github.Reposit r.SetClone(repo.GetCloneURL()) r.SetBranch(repo.GetDefaultBranch()) r.SetPrivate(repo.GetPrivate()) + r.SetActive(!repo.GetArchived()) + r.SetTopics(repo.Topics) // if action is renamed, then get the previous name from payload - if payload.GetAction() == "renamed" { - r.SetPreviousName(payload.GetChanges().GetRepo().GetName().GetFrom()) - // update hook object event type - h.SetEvent(constants.EventRepositoryRename) - } else { - h.SetEvent(constants.EventRepository) + if payload.GetAction() == constants.ActionRenamed { + r.SetPreviousName(repo.GetOwner().GetLogin() + "/" + payload.GetChanges().GetRepo().GetName().GetFrom()) } + // if action is transferred, then get the previous owner from payload + // could be a user or an org, but both are User structs + if payload.GetAction() == constants.ActionTransferred { + org := payload.GetChanges().GetOwner().GetOwnerInfo().GetOrg() + if org == nil { + org = payload.GetChanges().GetOwner().GetOwnerInfo().GetUser() + } + + r.SetPreviousName(org.GetLogin() + "/" + repo.GetName()) + } + + h.SetEvent(constants.EventRepository) + h.SetEventAction(payload.GetAction()) h.SetBranch(r.GetBranch()) h.SetLink( fmt.Sprintf("https://%s/%s/settings/hooks", h.GetHost(), r.GetFullName()), @@ -438,3 +493,40 @@ func (c *client) processRepositoryEvent(h *library.Hook, payload *github.Reposit Repo: r, }, nil } + +// getDeliveryID gets the last 100 webhook deliveries for a repo and +// finds the matching delivery id with the source id in the hook. +func (c *client) getDeliveryID(ctx context.Context, ghClient *github.Client, r *library.Repo, h *library.Hook) (int64, error) { + c.Logger.WithFields(logrus.Fields{ + "org": r.GetOrg(), + "repo": r.GetName(), + }).Tracef("searching for delivery id for hook: %s", h.GetSourceID()) + + // set per page to 100 to retrieve last 100 hook summaries + opt := &github.ListCursorOptions{PerPage: 100} + + // send API call to capture delivery summaries that contain Delivery ID value + deliveries, resp, err := ghClient.Repositories.ListHookDeliveries(ctx, r.GetOrg(), r.GetName(), h.GetWebhookID(), opt) + + // version check: if GitHub API is older than version 3.2, this call will not work + if resp.StatusCode == 415 { + err = fmt.Errorf("requires GitHub version 3.2 or later") + return 0, err + } + + if err != nil { + return 0, err + } + + // cycle through delivery summaries and match Source ID/GUID. Capture Delivery ID + for _, delivery := range deliveries { + if delivery.GetGUID() == h.GetSourceID() { + return delivery.GetID(), nil + } + } + + // if not found, webhook was not recent enough for GitHub + err = fmt.Errorf("webhook no longer available to be redelivered") + + return 0, err +} diff --git a/scm/github/webhook_test.go b/scm/github/webhook_test.go index fff122e79..1bef4c0ca 100644 --- a/scm/github/webhook_test.go +++ b/scm/github/webhook_test.go @@ -5,6 +5,7 @@ package github import ( + "context" "fmt" "net/http" "net/http/httptest" @@ -13,6 +14,7 @@ import ( "testing" "time" + "github.com/gin-gonic/gin" "github.com/go-vela/types/raw" "github.com/google/go-cmp/cmp" @@ -34,10 +36,11 @@ func TestGithub_ProcessWebhook_Push(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Event", "push") // setup client @@ -47,6 +50,7 @@ func TestGithub_ProcessWebhook_Push(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") wantHook.SetEvent("push") @@ -62,6 +66,7 @@ func TestGithub_ProcessWebhook_Push(t *testing.T) { wantRepo.SetClone("https://github.com/Codertocat/Hello-World.git") wantRepo.SetBranch("master") wantRepo.SetPrivate(false) + wantRepo.SetTopics([]string{"go", "vela"}) wantBuild := new(library.Build) wantBuild.SetEvent("push") @@ -108,10 +113,11 @@ func TestGithub_ProcessWebhook_Push_NoSender(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "push") @@ -123,6 +129,7 @@ func TestGithub_ProcessWebhook_Push_NoSender(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") wantHook.SetEvent("push") @@ -138,6 +145,7 @@ func TestGithub_ProcessWebhook_Push_NoSender(t *testing.T) { wantRepo.SetClone("https://github.com/Codertocat/Hello-World.git") wantRepo.SetBranch("master") wantRepo.SetPrivate(false) + wantRepo.SetTopics([]string{"go", "vela"}) wantBuild := new(library.Build) wantBuild.SetEvent("push") @@ -184,10 +192,11 @@ func TestGithub_ProcessWebhook_PullRequest(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "pull_request") @@ -199,6 +208,7 @@ func TestGithub_ProcessWebhook_PullRequest(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") wantHook.SetEvent("pull_request") @@ -214,9 +224,11 @@ func TestGithub_ProcessWebhook_PullRequest(t *testing.T) { wantRepo.SetClone("https://github.com/Codertocat/Hello-World.git") wantRepo.SetBranch("master") wantRepo.SetPrivate(false) + wantRepo.SetTopics(nil) wantBuild := new(library.Build) wantBuild.SetEvent("pull_request") + wantBuild.SetEventAction("opened") wantBuild.SetClone("https://github.com/Codertocat/Hello-World.git") wantBuild.SetSource("https://github.com/Codertocat/Hello-World/pull/1") wantBuild.SetTitle("pull_request received from https://github.com/Codertocat/Hello-World") @@ -262,10 +274,11 @@ func TestGithub_ProcessWebhook_PullRequest_ClosedAction(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "pull_request") @@ -277,6 +290,7 @@ func TestGithub_ProcessWebhook_PullRequest_ClosedAction(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") wantHook.SetEvent("pull_request") @@ -315,10 +329,11 @@ func TestGithub_ProcessWebhook_PullRequest_ClosedState(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "pull_request") @@ -330,6 +345,7 @@ func TestGithub_ProcessWebhook_PullRequest_ClosedState(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") wantHook.SetEvent("pull_request") @@ -363,6 +379,7 @@ func TestGithub_ProcessWebhook_Deployment(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetBranch("master") wantHook.SetLink("https://github.com/Codertocat/Hello-World/settings/hooks") @@ -378,6 +395,7 @@ func TestGithub_ProcessWebhook_Deployment(t *testing.T) { wantRepo.SetClone("https://github.com/Codertocat/Hello-World.git") wantRepo.SetBranch("master") wantRepo.SetPrivate(false) + wantRepo.SetTopics(nil) wantBuild := new(library.Build) wantBuild.SetEvent("deployment") @@ -400,6 +418,7 @@ func TestGithub_ProcessWebhook_Deployment(t *testing.T) { build *library.Build deploymentPayload raw.StringSliceMap } + tests := []struct { name string args args @@ -409,6 +428,7 @@ func TestGithub_ProcessWebhook_Deployment(t *testing.T) { {"unexpected json payload", args{file: "deployment_unexpected_json_payload.json", deploymentPayload: raw.StringSliceMap{}}, true}, {"unexpected text payload", args{file: "deployment_unexpected_text_payload.json", deploymentPayload: raw.StringSliceMap{}}, true}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { body, err := os.Open(fmt.Sprintf("testdata/hooks/%s", tt.args.file)) @@ -418,10 +438,11 @@ func TestGithub_ProcessWebhook_Deployment(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "deployment") @@ -462,10 +483,11 @@ func TestGithub_ProcessWebhook_Deployment_Commit(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "deployment") @@ -477,6 +499,7 @@ func TestGithub_ProcessWebhook_Deployment_Commit(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetBranch("master") wantHook.SetLink("https://github.com/Codertocat/Hello-World/settings/hooks") @@ -492,6 +515,7 @@ func TestGithub_ProcessWebhook_Deployment_Commit(t *testing.T) { wantRepo.SetClone("https://github.com/Codertocat/Hello-World.git") wantRepo.SetBranch("master") wantRepo.SetPrivate(false) + wantRepo.SetTopics(nil) wantBuild := new(library.Build) wantBuild.SetEvent("deployment") @@ -538,10 +562,11 @@ func TestGithub_ProcessWebhook_BadGithubEvent(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "foobar") @@ -553,6 +578,7 @@ func TestGithub_ProcessWebhook_BadGithubEvent(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") wantHook.SetEvent("foobar") @@ -589,10 +615,11 @@ func TestGithub_ProcessWebhook_BadContentType(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "foobar") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "pull_request") @@ -604,6 +631,7 @@ func TestGithub_ProcessWebhook_BadContentType(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") wantHook.SetEvent("pull_request") @@ -640,10 +668,11 @@ func TestGithub_VerifyWebhook_EmptyRepo(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "deployment") @@ -680,10 +709,11 @@ func TestGithub_VerifyWebhook_NoSecret(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "push") @@ -711,10 +741,11 @@ func TestGithub_ProcessWebhook_IssueComment_PR(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "issue_comment") @@ -726,6 +757,7 @@ func TestGithub_ProcessWebhook_IssueComment_PR(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") wantHook.SetEvent("comment") @@ -740,9 +772,11 @@ func TestGithub_ProcessWebhook_IssueComment_PR(t *testing.T) { wantRepo.SetClone("https://github.com/Codertocat/Hello-World.git") wantRepo.SetBranch("master") wantRepo.SetPrivate(false) + wantRepo.SetTopics(nil) wantBuild := new(library.Build) wantBuild.SetEvent("comment") + wantBuild.SetEventAction("created") wantBuild.SetClone("https://github.com/Codertocat/Hello-World.git") wantBuild.SetSource("https://github.com/Codertocat/Hello-World/pull/1") wantBuild.SetTitle("comment received from https://github.com/Codertocat/Hello-World") @@ -784,10 +818,11 @@ func TestGithub_ProcessWebhook_IssueComment_Created(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "issue_comment") @@ -799,6 +834,7 @@ func TestGithub_ProcessWebhook_IssueComment_Created(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") wantHook.SetEvent("comment") @@ -813,9 +849,11 @@ func TestGithub_ProcessWebhook_IssueComment_Created(t *testing.T) { wantRepo.SetClone("https://github.com/Codertocat/Hello-World.git") wantRepo.SetBranch("master") wantRepo.SetPrivate(false) + wantRepo.SetTopics(nil) wantBuild := new(library.Build) wantBuild.SetEvent("comment") + wantBuild.SetEventAction("created") wantBuild.SetClone("https://github.com/Codertocat/Hello-World.git") wantBuild.SetSource("https://github.com/Codertocat/Hello-World/issues/1") wantBuild.SetTitle("comment received from https://github.com/Codertocat/Hello-World") @@ -857,10 +895,11 @@ func TestGithub_ProcessWebhook_IssueComment_Deleted(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Host", "github.com") request.Header.Set("X-GitHub-Version", "2.16.0") request.Header.Set("X-GitHub-Event", "issue_comment") @@ -872,6 +911,7 @@ func TestGithub_ProcessWebhook_IssueComment_Deleted(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") wantHook.SetEvent("comment") @@ -910,10 +950,141 @@ func TestGitHub_ProcessWebhook_RepositoryRename(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") + request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") + request.Header.Set("X-GitHub-Event", "repository") + + // setup client + client, _ := NewTest(s.URL) + + // run test + wantHook := new(library.Hook) + wantHook.SetNumber(1) + wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) + wantHook.SetCreated(time.Now().UTC().Unix()) + wantHook.SetHost("github.com") + wantHook.SetEvent(constants.EventRepository) + wantHook.SetEventAction(constants.ActionRenamed) + wantHook.SetBranch("master") + wantHook.SetStatus(constants.StatusSuccess) + wantHook.SetLink("https://github.com/Codertocat/Hello-World/settings/hooks") + + wantRepo := new(library.Repo) + wantRepo.SetActive(true) + wantRepo.SetOrg("Codertocat") + wantRepo.SetName("Hello-World") + wantRepo.SetFullName("Codertocat/Hello-World") + wantRepo.SetLink("https://octocoders.github.io/Codertocat/Hello-World") + wantRepo.SetClone("https://octocoders.github.io/Codertocat/Hello-World.git") + wantRepo.SetBranch("master") + wantRepo.SetPrivate(false) + wantRepo.SetPreviousName("Codertocat/Hello-Old-World") + wantRepo.SetTopics(nil) + + want := &types.Webhook{ + Comment: "", + Hook: wantHook, + Repo: wantRepo, + } + + got, err := client.ProcessWebhook(request) + + if err != nil { + t.Errorf("ProcessWebhook returned err: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("ProcessWebhook is %v, want %v", got, want) + } +} + +func TestGitHub_ProcessWebhook_RepositoryTransfer(t *testing.T) { + // setup router + s := httptest.NewServer(http.NotFoundHandler()) + defer s.Close() + + // setup request + body, err := os.Open("testdata/hooks/repository_transferred.json") + if err != nil { + t.Errorf("unable to open file: %v", err) + } + + defer body.Close() + + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") + request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") + request.Header.Set("X-GitHub-Event", "repository") + + // setup client + client, _ := NewTest(s.URL) + + // run test + wantHook := new(library.Hook) + wantHook.SetNumber(1) + wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) + wantHook.SetCreated(time.Now().UTC().Unix()) + wantHook.SetHost("github.com") + wantHook.SetEvent(constants.EventRepository) + wantHook.SetEventAction(constants.ActionTransferred) + wantHook.SetBranch("master") + wantHook.SetStatus(constants.StatusSuccess) + wantHook.SetLink("https://github.com/Codertocat/Hello-World/settings/hooks") + + wantRepo := new(library.Repo) + wantRepo.SetActive(true) + wantRepo.SetOrg("Codertocat") + wantRepo.SetName("Hello-World") + wantRepo.SetFullName("Codertocat/Hello-World") + wantRepo.SetLink("https://octocoders.github.io/Codertocat/Hello-World") + wantRepo.SetClone("https://octocoders.github.io/Codertocat/Hello-World.git") + wantRepo.SetBranch("master") + wantRepo.SetPrivate(false) + wantRepo.SetPreviousName("Old-Codertocat/Hello-World") + wantRepo.SetTopics(nil) + + want := &types.Webhook{ + Comment: "", + Hook: wantHook, + Repo: wantRepo, + } + + got, err := client.ProcessWebhook(request) + + if err != nil { + t.Errorf("ProcessWebhook returned err: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("ProcessWebhook is %v, want %v", got, want) + } +} + +func TestGitHub_ProcessWebhook_RepositoryArchived(t *testing.T) { + // setup router + s := httptest.NewServer(http.NotFoundHandler()) + defer s.Close() + + // setup request + body, err := os.Open("testdata/hooks/repository_archived.json") + if err != nil { + t.Errorf("unable to open file: %v", err) + } + + defer body.Close() + + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Event", "repository") // setup client @@ -923,14 +1094,17 @@ func TestGitHub_ProcessWebhook_RepositoryRename(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") - wantHook.SetEvent("repositoryRename") + wantHook.SetEvent(constants.EventRepository) + wantHook.SetEventAction("archived") wantHook.SetBranch("master") wantHook.SetStatus(constants.StatusSuccess) wantHook.SetLink("https://github.com/Codertocat/Hello-World/settings/hooks") wantRepo := new(library.Repo) + wantRepo.SetActive(false) wantRepo.SetOrg("Codertocat") wantRepo.SetName("Hello-World") wantRepo.SetFullName("Codertocat/Hello-World") @@ -938,7 +1112,71 @@ func TestGitHub_ProcessWebhook_RepositoryRename(t *testing.T) { wantRepo.SetClone("https://octocoders.github.io/Codertocat/Hello-World.git") wantRepo.SetBranch("master") wantRepo.SetPrivate(false) - wantRepo.SetPreviousName("Hello-Old-World") + wantRepo.SetTopics(nil) + + want := &types.Webhook{ + Comment: "", + Hook: wantHook, + Repo: wantRepo, + } + + got, err := client.ProcessWebhook(request) + + if err != nil { + t.Errorf("ProcessWebhook returned err: %v", err) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("ProcessWebhook is %v, want %v", got, want) + } +} + +func TestGitHub_ProcessWebhook_RepositoryEdited(t *testing.T) { + // setup router + s := httptest.NewServer(http.NotFoundHandler()) + defer s.Close() + + // setup request + body, err := os.Open("testdata/hooks/repository_edited.json") + if err != nil { + t.Errorf("unable to open file: %v", err) + } + + defer body.Close() + + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") + request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") + request.Header.Set("X-GitHub-Event", "repository") + + // setup client + client, _ := NewTest(s.URL) + + // run test + wantHook := new(library.Hook) + wantHook.SetNumber(1) + wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) + wantHook.SetCreated(time.Now().UTC().Unix()) + wantHook.SetHost("github.com") + wantHook.SetEvent(constants.EventRepository) + wantHook.SetEventAction(constants.ActionEdited) + wantHook.SetBranch("main") + wantHook.SetStatus(constants.StatusSuccess) + wantHook.SetLink("https://github.com/Codertocat/Hello-World/settings/hooks") + + wantRepo := new(library.Repo) + wantRepo.SetActive(true) + wantRepo.SetOrg("Codertocat") + wantRepo.SetName("Hello-World") + wantRepo.SetFullName("Codertocat/Hello-World") + wantRepo.SetLink("https://octocoders.github.io/Codertocat/Hello-World") + wantRepo.SetClone("https://octocoders.github.io/Codertocat/Hello-World.git") + wantRepo.SetBranch("main") + wantRepo.SetTopics([]string{"cloud", "security"}) + wantRepo.SetPrivate(false) want := &types.Webhook{ Comment: "", @@ -970,10 +1208,11 @@ func TestGitHub_ProcessWebhook_Repository(t *testing.T) { defer body.Close() - request, _ := http.NewRequest(http.MethodGet, "/test", body) + request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "/test", body) request.Header.Set("Content-Type", "application/json") request.Header.Set("User-Agent", "GitHub-Hookshot/a22606a") request.Header.Set("X-GitHub-Delivery", "7bd477e4-4415-11e9-9359-0d41fdf9567e") + request.Header.Set("X-GitHub-Hook-ID", "123456") request.Header.Set("X-GitHub-Event", "repository") // setup client @@ -983,14 +1222,17 @@ func TestGitHub_ProcessWebhook_Repository(t *testing.T) { wantHook := new(library.Hook) wantHook.SetNumber(1) wantHook.SetSourceID("7bd477e4-4415-11e9-9359-0d41fdf9567e") + wantHook.SetWebhookID(123456) wantHook.SetCreated(time.Now().UTC().Unix()) wantHook.SetHost("github.com") - wantHook.SetEvent("repository") + wantHook.SetEvent(constants.EventRepository) + wantHook.SetEventAction("publicized") wantHook.SetBranch("master") wantHook.SetStatus(constants.StatusSuccess) wantHook.SetLink("https://github.com/Codertocat/Hello-World/settings/hooks") wantRepo := new(library.Repo) + wantRepo.SetActive(true) wantRepo.SetOrg("Codertocat") wantRepo.SetName("Hello-World") wantRepo.SetFullName("Codertocat/Hello-World") @@ -998,6 +1240,7 @@ func TestGitHub_ProcessWebhook_Repository(t *testing.T) { wantRepo.SetClone("https://octocoders.github.io/Codertocat/Hello-World.git") wantRepo.SetBranch("master") wantRepo.SetPrivate(false) + wantRepo.SetTopics(nil) want := &types.Webhook{ Comment: "", @@ -1015,3 +1258,105 @@ func TestGitHub_ProcessWebhook_Repository(t *testing.T) { t.Errorf("ProcessWebhook is %v, want %v", got, want) } } + +func TestGithub_Redeliver_Webhook(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + // setup mock server + engine.POST("/api/v3/repos/:org/:repo/hooks/:repo_id/deliveries/:delivery_id/attempts", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/hooks/push.json") + }) + engine.GET("/api/v3/repos/:org/:repo/hooks/:hook_id/deliveries", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/delivery_summaries.json") + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + u := new(library.User) + u.SetName("octocat") + u.SetToken("foo") + + _hook := new(library.Hook) + _hook.SetSourceID("b595f0e0-aee1-11ec-86cf-9418381395c4") + _hook.SetID(1) + _hook.SetRepoID(1) + _hook.SetBuildID(1) + _hook.SetNumber(1) + _hook.SetWebhookID(1234) + + _repo := new(library.Repo) + _repo.SetID(1) + _repo.SetName("bar") + _repo.SetOrg("foo") + + client, _ := NewTest(s.URL, "https://foo.bar.com") + + // run test + err := client.RedeliverWebhook(ctx, u, _repo, _hook) + + if err != nil { + t.Errorf("RedeliverWebhook returned err: %v", err) + } +} + +func TestGithub_GetDeliveryID(t *testing.T) { + // setup context + gin.SetMode(gin.TestMode) + + resp := httptest.NewRecorder() + _, engine := gin.CreateTestContext(resp) + + engine.GET("/api/v3/repos/:org/:repo/hooks/:hook_id/deliveries", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/delivery_summaries.json") + }) + + s := httptest.NewServer(engine) + defer s.Close() + + // setup types + u := new(library.User) + u.SetName("octocat") + u.SetToken("foo") + + _hook := new(library.Hook) + _hook.SetSourceID("b595f0e0-aee1-11ec-86cf-9418381395c4") + _hook.SetID(1) + _hook.SetRepoID(1) + _hook.SetBuildID(1) + _hook.SetNumber(1) + _hook.SetWebhookID(1234) + + _repo := new(library.Repo) + _repo.SetID(1) + _repo.SetName("bar") + _repo.SetOrg("foo") + + want := int64(22948188373) + + client, _ := NewTest(s.URL, "https://foo.bar.com") + + ghClient := client.newClientToken(*u.Token) + + // run test + got, err := client.getDeliveryID(ctx, ghClient, _repo, _hook) + + if err != nil { + t.Errorf("RedeliverWebhook returned err: %v", err) + } + + if got != want { + t.Errorf("getDeliveryID returned: %v; want: %v", got, want) + } +} diff --git a/scm/scm.go b/scm/scm.go index faf2881a9..2bd5c8170 100644 --- a/scm/scm.go +++ b/scm/scm.go @@ -12,14 +12,13 @@ import ( "github.com/sirupsen/logrus" ) -// nolint: godot // top level comment ends in a list -// // New creates and returns a Vela service capable of // integrating with the configured scm provider. // // Currently the following scm providers are supported: // // * Github +// . func New(s *Setup) (Service, error) { // validate the setup being provided // diff --git a/scm/service.go b/scm/service.go index efcf6d8fb..4de42362f 100644 --- a/scm/service.go +++ b/scm/service.go @@ -5,6 +5,7 @@ package scm import ( + "context" "net/http" "github.com/go-vela/types" @@ -97,19 +98,31 @@ type Service interface { Disable(*library.User, string, string) error // Enable defines a function that activates // a repo by creating the webhook. - Enable(*library.User, string, string, string) (string, error) + Enable(*library.User, *library.Repo, *library.Hook) (*library.Hook, string, error) + // Update defines a function that updates + // a webhook for a specified repo. + Update(*library.User, *library.Repo, int64) error // Status defines a function that sends the // commit status for the given SHA from a repo. Status(*library.User, *library.Build, string, string) error // ListUserRepos defines a function that retrieves // all repos with admin rights for the user. ListUserRepos(*library.User) ([]*library.Repo, error) + // GetBranch defines a function that retrieves + // a branch for a repo. + GetBranch(*library.User, *library.Repo, string) (string, string, error) // GetPullRequest defines a function that retrieves // a pull request for a repo. GetPullRequest(*library.User, *library.Repo, int) (string, string, string, string, error) // GetRepo defines a function that retrieves // details for a repo. GetRepo(*library.User, *library.Repo) (*library.Repo, error) + // GetOrgAndRepoName defines a function that retrieves + // the name of the org and repo in the SCM. + GetOrgAndRepoName(*library.User, string, string) (string, string, error) + // GetOrg defines a function that retrieves + // the name for an org in the SCM. + GetOrgName(*library.User, string) (string, error) // GetHTMLURL defines a function that retrieves // a repository file's html_url. GetHTMLURL(*library.User, string, string, string, string) (string, error) @@ -122,6 +135,9 @@ type Service interface { // VerifyWebhook defines a function that // verifies the webhook from a repo. VerifyWebhook(*http.Request, *library.Repo) error + // RedeliverWebhook defines a function that + // redelivers the webhook from the SCM. + RedeliverWebhook(context.Context, *library.User, *library.Repo, *library.Hook) error // TODO: Add convert functions to interface? } diff --git a/secret/context_test.go b/secret/context_test.go index 803980664..87f6afe06 100644 --- a/secret/context_test.go +++ b/secret/context_test.go @@ -7,19 +7,21 @@ package secret import ( "testing" - "github.com/go-vela/server/database/sqlite" - "github.com/go-vela/server/secret/native" - "github.com/gin-gonic/gin" + "github.com/go-vela/server/database" + "github.com/go-vela/server/secret/native" ) func TestSecret_FromContext(t *testing.T) { // setup types - d, _ := sqlite.NewTest() - defer func() { _sql, _ := d.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() want, err := native.New( - native.WithDatabase(d), + native.WithDatabase(db), ) if err != nil { t.Errorf("New returned err: %v", err) @@ -81,11 +83,14 @@ func TestSecret_FromContext_Empty(t *testing.T) { func TestSecret_ToContext(t *testing.T) { // setup types - d, _ := sqlite.NewTest() - defer func() { _sql, _ := d.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() want, err := native.New( - native.WithDatabase(d), + native.WithDatabase(db), ) if err != nil { t.Errorf("New returned err: %v", err) diff --git a/secret/doc.go b/secret/doc.go index 555864d4e..b15a4b102 100644 --- a/secret/doc.go +++ b/secret/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/secret" +// import "github.com/go-vela/server/secret" package secret diff --git a/secret/native/count.go b/secret/native/count.go index d534cb268..2460d998a 100644 --- a/secret/native/count.go +++ b/secret/native/count.go @@ -5,38 +5,62 @@ package native import ( - "strings" + "fmt" "github.com/go-vela/types/constants" + "github.com/go-vela/types/library" "github.com/sirupsen/logrus" ) // Count counts a list of secrets. func (c *client) Count(sType, org, name string, teams []string) (int64, error) { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": org, - "repo": name, - "type": sType, - } + // handle the secret based off the type + switch sType { + case constants.SecretOrg: + c.Logger.WithFields(logrus.Fields{ + "org": org, + "type": sType, + }).Tracef("counting native %s secrets for %s", sType, org) - // check if secret is a shared secret - if strings.EqualFold(sType, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ + // capture the count of org secrets from the native service + return c.Database.CountSecretsForOrg(org, nil) + case constants.SecretRepo: + c.Logger.WithFields(logrus.Fields{ "org": org, - "team": name, + "repo": name, "type": sType, + }).Tracef("counting native %s secrets for %s/%s", sType, org, name) + + // create the repo with the information available + r := new(library.Repo) + r.SetOrg(org) + r.SetName(name) + r.SetFullName(fmt.Sprintf("%s/%s", org, name)) + + // capture the count of repo secrets from the native service + return c.Database.CountSecretsForRepo(r, nil) + case constants.SecretShared: + // check if we should capture secrets for multiple teams + if name == "*" { + c.Logger.WithFields(logrus.Fields{ + "org": org, + "teams": teams, + "type": sType, + }).Tracef("counting native %s secrets for teams %s in org %s", sType, teams, org) + + // capture the count of shared secrets for multiple teams from the native service + return c.Database.CountSecretsForTeams(org, teams, nil) } - } - c.Logger.WithFields(fields).Tracef("counting native %s secrets for %s/%s", sType, org, name) + c.Logger.WithFields(logrus.Fields{ + "org": org, + "team": name, + "type": sType, + }).Tracef("counting native %s secrets for %s/%s", sType, org, name) - // capture the count of secrets from the native service - s, err := c.Database.GetTypeSecretCount(sType, org, name, teams) - if err != nil { - return 0, err + // capture the count of shared secrets from the native service + return c.Database.CountSecretsForTeam(org, name, nil) + default: + return 0, fmt.Errorf("invalid secret type: %s", sType) } - - return s, nil } diff --git a/secret/native/count_test.go b/secret/native/count_test.go index d9e7e94ab..42c71cc9b 100644 --- a/secret/native/count_test.go +++ b/secret/native/count_test.go @@ -7,7 +7,7 @@ package native import ( "testing" - "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/database" "github.com/go-vela/types/library" ) @@ -22,19 +22,23 @@ func TestNative_Count(t *testing.T) { sec.SetType("repo") sec.SetImages([]string{"foo", "bar"}) sec.SetEvents([]string{"foo", "bar"}) + sec.SetCreatedAt(1) + sec.SetUpdatedAt(1) want := 1 // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from secrets;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteSecret(sec) + db.Close() }() - _ = db.CreateSecret(sec) + _, _ = db.CreateSecret(sec) // run test s, err := New( @@ -54,11 +58,13 @@ func TestNative_Count(t *testing.T) { } } -func TestNative_Count_Invalid(t *testing.T) { +func TestNative_Count_Empty(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - _sql, _ := db.Sqlite.DB() - _sql.Close() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // run test s, err := New( @@ -69,8 +75,8 @@ func TestNative_Count_Invalid(t *testing.T) { } got, err := s.Count("repo", "foo", "bar", []string{}) - if err == nil { - t.Errorf("Count should have returned err") + if err != nil { + t.Errorf("Count returned err: %v", err) } if got != 0 { diff --git a/secret/native/create.go b/secret/native/create.go index b4bad22b1..b93869602 100644 --- a/secret/native/create.go +++ b/secret/native/create.go @@ -6,47 +6,46 @@ package native import ( "fmt" - "strings" - - "github.com/sirupsen/logrus" "github.com/go-vela/types/constants" "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" ) // Create creates a new secret. -func (c *client) Create(sType, org, name string, s *library.Secret) error { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": org, - "repo": name, - "secret": s.GetName(), - "type": sType, - } - - // check if secret is a shared secret - if strings.EqualFold(sType, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ +func (c *client) Create(sType, org, name string, s *library.Secret) (*library.Secret, error) { + // handle the secret based off the type + switch sType { + case constants.SecretOrg: + c.Logger.WithFields(logrus.Fields{ "org": org, - "team": name, "secret": s.GetName(), "type": sType, - } - } + }).Tracef("creating native %s secret %s for %s", sType, s.GetName(), org) - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("creating native %s secret %s for %s/%s", sType, s.GetName(), org, name) - - // create the secret for the native service - switch sType { - case constants.SecretOrg: - fallthrough + // create the org secret in the native service + return c.Database.CreateSecret(s) case constants.SecretRepo: - fallthrough + c.Logger.WithFields(logrus.Fields{ + "org": org, + "repo": name, + "secret": s.GetName(), + "type": sType, + }).Tracef("creating native %s secret %s for %s/%s", sType, s.GetName(), org, name) + + // create the repo secret in the native service + return c.Database.CreateSecret(s) case constants.SecretShared: + c.Logger.WithFields(logrus.Fields{ + "org": org, + "secret": s.GetName(), + "team": name, + "type": sType, + }).Tracef("creating native %s secret %s for %s/%s", sType, s.GetName(), org, name) + + // create the shared secret in the native service return c.Database.CreateSecret(s) default: - return fmt.Errorf("invalid secret type: %v", sType) + return nil, fmt.Errorf("invalid secret type: %s", sType) } } diff --git a/secret/native/create_test.go b/secret/native/create_test.go index 08fd4b18b..93c6898a4 100644 --- a/secret/native/create_test.go +++ b/secret/native/create_test.go @@ -8,7 +8,7 @@ import ( "reflect" "testing" - "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/database" "github.com/go-vela/types/library" ) @@ -31,12 +31,14 @@ func TestNative_Create_Org(t *testing.T) { want.SetUpdatedBy("user2") // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from secrets;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteSecret(want) + db.Close() }() // run test @@ -47,13 +49,11 @@ func TestNative_Create_Org(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Create("org", "foo", "*", want) + got, err := s.Create("org", "foo", "*", want) if err != nil { t.Errorf("Create returned err: %v", err) } - got, _ := s.Get("org", "foo", "*", "bar") - if !reflect.DeepEqual(got, want) { t.Errorf("Create is %v, want %v", got, want) } @@ -78,12 +78,14 @@ func TestNative_Create_Repo(t *testing.T) { want.SetUpdatedBy("user2") // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from secrets;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteSecret(want) + db.Close() }() // run test @@ -94,13 +96,11 @@ func TestNative_Create_Repo(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Create("repo", "foo", "bar", want) + got, err := s.Create("repo", "foo", "bar", want) if err != nil { t.Errorf("Create returned err: %v", err) } - got, _ := s.Get("repo", "foo", "bar", "baz") - if !reflect.DeepEqual(got, want) { t.Errorf("Create is %v, want %v", got, want) } @@ -125,12 +125,14 @@ func TestNative_Create_Shared(t *testing.T) { want.SetUpdatedBy("user2") // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from secrets;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteSecret(want) + db.Close() }() // run test @@ -141,13 +143,11 @@ func TestNative_Create_Shared(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Create("shared", "foo", "bar", want) + got, err := s.Create("shared", "foo", "bar", want) if err != nil { t.Errorf("Create returned err: %v", err) } - got, _ := s.Get("shared", "foo", "bar", "baz") - if !reflect.DeepEqual(got, want) { t.Errorf("Create is %v, want %v", got, want) } @@ -172,12 +172,14 @@ func TestNative_Create_Invalid(t *testing.T) { sec.SetUpdatedBy("user2") // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from secrets;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteSecret(sec) + db.Close() }() // run test @@ -188,7 +190,7 @@ func TestNative_Create_Invalid(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Create("invalid", "foo", "bar", sec) + _, err = s.Create("invalid", "foo", "bar", sec) if err == nil { t.Errorf("Create should have returned err") } diff --git a/secret/native/delete.go b/secret/native/delete.go index 76cf3ef15..c414296ca 100644 --- a/secret/native/delete.go +++ b/secret/native/delete.go @@ -5,7 +5,7 @@ package native import ( - "strings" + "fmt" "github.com/go-vela/types/constants" "github.com/sirupsen/logrus" @@ -13,34 +13,44 @@ import ( // Delete deletes a secret. func (c *client) Delete(sType, org, name, path string) error { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": org, - "repo": name, - "secret": path, - "type": sType, + // capture the secret from the native service + s, err := c.Get(sType, org, name, path) + if err != nil { + return err } - // check if secret is a shared secret - if strings.EqualFold(sType, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ + // handle the secret based off the type + switch sType { + case constants.SecretOrg: + c.Logger.WithFields(logrus.Fields{ "org": org, - "team": name, "secret": path, "type": sType, - } - } + }).Tracef("deleting native %s secret %s for %s", sType, path, org) - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("deleting native %s secret %s for %s/%s", sType, path, org, name) + // delete the org secret from the native service + return c.Database.DeleteSecret(s) + case constants.SecretRepo: + c.Logger.WithFields(logrus.Fields{ + "org": org, + "repo": name, + "secret": path, + "type": sType, + }).Tracef("deleting native %s secret %s for %s/%s", sType, path, org, name) - // capture the secret from the native service - s, err := c.Database.GetSecret(sType, org, name, path) - if err != nil { - return err - } + // delete the repo secret from the native service + return c.Database.DeleteSecret(s) + case constants.SecretShared: + c.Logger.WithFields(logrus.Fields{ + "org": org, + "secret": path, + "team": name, + "type": sType, + }).Tracef("deleting native %s secret %s for %s/%s", sType, path, org, name) - // delete the secret from the native service - return c.Database.DeleteSecret(s.GetID()) + // delete the shared secret from the native service + return c.Database.DeleteSecret(s) + default: + return fmt.Errorf("invalid secret type: %s", sType) + } } diff --git a/secret/native/delete_test.go b/secret/native/delete_test.go index 34f9e4229..e2e21a5f7 100644 --- a/secret/native/delete_test.go +++ b/secret/native/delete_test.go @@ -7,7 +7,7 @@ package native import ( "testing" - "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/database" "github.com/go-vela/types/library" ) @@ -24,17 +24,21 @@ func TestNative_Delete(t *testing.T) { sec.SetImages([]string{"foo", "bar"}) sec.SetEvents([]string{"foo", "bar"}) sec.SetAllowCommand(false) + sec.SetCreatedAt(1) + sec.SetUpdatedAt(1) // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from secrets;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteSecret(sec) + db.Close() }() - _ = db.CreateSecret(sec) + _, _ = db.CreateSecret(sec) // run test s, err := New( @@ -52,8 +56,11 @@ func TestNative_Delete(t *testing.T) { func TestNative_Delete_Invalid(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // run test s, err := New( diff --git a/secret/native/doc.go b/secret/native/doc.go index ddf75d3de..7cd54caec 100644 --- a/secret/native/doc.go +++ b/secret/native/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/secret/native" +// import "github.com/go-vela/server/secret/native" package native diff --git a/secret/native/driver_test.go b/secret/native/driver_test.go index 89e152f27..ec0038df5 100644 --- a/secret/native/driver_test.go +++ b/secret/native/driver_test.go @@ -8,17 +8,17 @@ import ( "reflect" "testing" - "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/database" "github.com/go-vela/types/constants" ) func TestNative_Driver(t *testing.T) { // setup types - db, err := sqlite.NewTest() + db, err := database.NewTest() if err != nil { t.Errorf("unable to create database service: %v", err) } - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + defer db.Close() want := constants.DriverNative diff --git a/secret/native/get.go b/secret/native/get.go index 087679a0d..9ed322355 100644 --- a/secret/native/get.go +++ b/secret/native/get.go @@ -5,7 +5,7 @@ package native import ( - "strings" + "fmt" "github.com/go-vela/types/constants" "github.com/go-vela/types/library" @@ -14,32 +14,44 @@ import ( // Get captures a secret. func (c *client) Get(sType, org, name, path string) (*library.Secret, error) { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": org, - "repo": name, - "secret": path, - "type": sType, - } - - // check if secret is a shared secret - if strings.EqualFold(sType, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ + // handle the secret based off the type + switch sType { + case constants.SecretOrg: + c.Logger.WithFields(logrus.Fields{ "org": org, - "team": name, "secret": path, "type": sType, - } - } + }).Tracef("getting native %s secret %s for %s", sType, path, org) - c.Logger.WithFields(fields).Tracef("getting native %s secret %s for %s/%s", sType, path, org, name) + // capture the org secret from the native service + return c.Database.GetSecretForOrg(org, path) + case constants.SecretRepo: + c.Logger.WithFields(logrus.Fields{ + "org": org, + "repo": name, + "secret": path, + "type": sType, + }).Tracef("getting native %s secret %s for %s/%s", sType, path, org, name) + + // create the repo with the information available + r := new(library.Repo) + r.SetOrg(org) + r.SetName(name) + r.SetFullName(fmt.Sprintf("%s/%s", org, name)) + + // capture the repo secret from the native service + return c.Database.GetSecretForRepo(path, r) + case constants.SecretShared: + c.Logger.WithFields(logrus.Fields{ + "org": org, + "secret": path, + "team": name, + "type": sType, + }).Tracef("getting native %s secret %s for %s/%s", sType, path, org, name) - // capture the secret from the native service - s, err := c.Database.GetSecret(sType, org, name, path) - if err != nil { - return nil, err + // capture the shared secret from the native service + return c.Database.GetSecretForTeam(org, name, path) + default: + return nil, fmt.Errorf("invalid secret type: %s", sType) } - - return s, nil } diff --git a/secret/native/get_test.go b/secret/native/get_test.go index 44db6acaf..218236dc6 100644 --- a/secret/native/get_test.go +++ b/secret/native/get_test.go @@ -8,7 +8,7 @@ import ( "reflect" "testing" - "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/database" "github.com/go-vela/types/library" ) @@ -31,12 +31,15 @@ func TestNative_Get(t *testing.T) { want.SetUpdatedBy("user2") // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() defer func() { - db.Sqlite.Exec("delete from secrets;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteSecret(want) + db.Close() }() // run test @@ -47,7 +50,7 @@ func TestNative_Get(t *testing.T) { t.Errorf("New returned err: %v", err) } - _ = s.Create("repo", "foo", "bar", want) + _, _ = s.Create("repo", "foo", "bar", want) got, err := s.Get("repo", "foo", "bar", "baz") if err != nil { @@ -61,8 +64,11 @@ func TestNative_Get(t *testing.T) { func TestNative_Get_Invalid(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // run test s, err := New( diff --git a/secret/native/list.go b/secret/native/list.go index a2dc14a6e..0bd9ab303 100644 --- a/secret/native/list.go +++ b/secret/native/list.go @@ -5,7 +5,7 @@ package native import ( - "strings" + "fmt" "github.com/go-vela/types/constants" "github.com/go-vela/types/library" @@ -13,33 +13,74 @@ import ( ) // List captures a list of secrets. -// -// nolint: lll // ignore long line length func (c *client) List(sType, org, name string, page, perPage int, teams []string) ([]*library.Secret, error) { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": org, - "repo": name, - "type": sType, - } + // handle the secret based off the type + switch sType { + case constants.SecretOrg: + c.Logger.WithFields(logrus.Fields{ + "org": org, + "type": sType, + }).Tracef("listing native %s secrets for %s", sType, org) + + // capture the list of org secrets from the native service + secrets, _, err := c.Database.ListSecretsForOrg(org, nil, page, perPage) + if err != nil { + return nil, err + } - // check if secret is a shared secret - if strings.EqualFold(sType, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ + return secrets, nil + case constants.SecretRepo: + c.Logger.WithFields(logrus.Fields{ "org": org, - "team": name, + "repo": name, "type": sType, + }).Tracef("listing native %s secrets for %s/%s", sType, org, name) + + // create the repo with the information available + r := new(library.Repo) + r.SetOrg(org) + r.SetName(name) + r.SetFullName(fmt.Sprintf("%s/%s", org, name)) + + // capture the list of repo secrets from the native service + secrets, _, err := c.Database.ListSecretsForRepo(r, nil, page, perPage) + if err != nil { + return nil, err } - } - c.Logger.WithFields(fields).Tracef("listing native %s secrets for %s/%s", sType, org, name) + return secrets, nil + case constants.SecretShared: + // check if we should capture secrets for multiple teams + if name == "*" { + c.Logger.WithFields(logrus.Fields{ + "org": org, + "teams": teams, + "type": sType, + }).Tracef("listing native %s secrets for teams %s in org %s", sType, teams, org) - // capture the list of secrets from the native service - s, err := c.Database.GetTypeSecretList(sType, org, name, page, perPage, teams) - if err != nil { - return nil, err - } + // capture the list of shared secrets for multiple teams from the native service + secrets, _, err := c.Database.ListSecretsForTeams(org, teams, nil, page, perPage) + if err != nil { + return nil, err + } + + return secrets, nil + } + + c.Logger.WithFields(logrus.Fields{ + "org": org, + "team": name, + "type": sType, + }).Tracef("listing native %s secrets for %s/%s", sType, org, name) - return s, nil + // capture the list of shared secrets from the native service + secrets, _, err := c.Database.ListSecretsForTeam(org, name, nil, page, perPage) + if err != nil { + return nil, err + } + + return secrets, nil + default: + return nil, fmt.Errorf("invalid secret type: %s", sType) + } } diff --git a/secret/native/list_test.go b/secret/native/list_test.go index ac9fd1c67..2cec7b78e 100644 --- a/secret/native/list_test.go +++ b/secret/native/list_test.go @@ -8,7 +8,7 @@ import ( "reflect" "testing" - "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/database" "github.com/go-vela/types/library" ) @@ -49,12 +49,15 @@ func TestNative_List(t *testing.T) { want := []*library.Secret{sTwo, sOne} // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from secrets;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteSecret(sOne) + db.DeleteSecret(sTwo) + db.Close() }() // run test @@ -65,9 +68,9 @@ func TestNative_List(t *testing.T) { t.Errorf("New returned err: %v", err) } - _ = s.Create("repo", "foo", "bar", sOne) + _, _ = s.Create("repo", "foo", "bar", sOne) - _ = s.Create("repo", "foo", "bar", sTwo) + _, _ = s.Create("repo", "foo", "bar", sTwo) got, err := s.List("repo", "foo", "bar", 1, 10, []string{}) if err != nil { @@ -79,11 +82,13 @@ func TestNative_List(t *testing.T) { } } -func TestNative_List_Invalid(t *testing.T) { +func TestNative_List_Empty(t *testing.T) { // setup database - db, _ := sqlite.NewTest() - _sql, _ := db.Sqlite.DB() - _sql.Close() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // run test s, err := New( @@ -94,11 +99,11 @@ func TestNative_List_Invalid(t *testing.T) { } got, err := s.List("repo", "foo", "bar", 1, 10, []string{}) - if err == nil { - t.Errorf("List should have returned err") + if err != nil { + t.Errorf("List returned err: %v", err) } - if got != nil { - t.Errorf("List is %v, want nil", got) + if len(got) > 0 { + t.Errorf("List is %v, want []", got) } } diff --git a/secret/native/native.go b/secret/native/native.go index fbc53cb5b..99b015715 100644 --- a/secret/native/native.go +++ b/secret/native/native.go @@ -12,20 +12,20 @@ import ( // client represents a struct to hold native secret setup. type client struct { // client to interact with database for secret operations - Database database.Service + Database database.Interface // https://pkg.go.dev/github.com/sirupsen/logrus#Entry Logger *logrus.Entry } // New returns a Secret implementation that integrates with a Native secrets engine. // -// nolint: revive // ignore returning unexported client +//nolint:revive // ignore returning unexported client func New(opts ...ClientOpt) (*client, error) { // create new native client c := new(client) // create new fields - c.Database = *new(database.Service) + c.Database = *new(database.Interface) // create new logger for the client // diff --git a/secret/native/native_test.go b/secret/native/native_test.go index bc1ec1aa1..0a5e3ee49 100644 --- a/secret/native/native_test.go +++ b/secret/native/native_test.go @@ -8,22 +8,21 @@ import ( "testing" "github.com/go-vela/server/database" - "github.com/go-vela/server/database/sqlite" ) func TestNative_New(t *testing.T) { // setup types - db, err := sqlite.NewTest() + db, err := database.NewTest() if err != nil { - t.Errorf("unable to create database service: %v", err) + t.Errorf("unable to create test database engine: %v", err) } - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + defer db.Close() // setup tests tests := []struct { failure bool - database database.Service - want database.Service + database database.Interface + want database.Interface }{ { failure: false, diff --git a/secret/native/opts.go b/secret/native/opts.go index 160b554c6..0c4396348 100644 --- a/secret/native/opts.go +++ b/secret/native/opts.go @@ -14,7 +14,7 @@ import ( type ClientOpt func(*client) error // WithDatabase sets the Vela database service in the secret client for Native. -func WithDatabase(d database.Service) ClientOpt { +func WithDatabase(d database.Interface) ClientOpt { return func(c *client) error { c.Logger.Trace("configuring database service in native secret client") diff --git a/secret/native/opts_test.go b/secret/native/opts_test.go index 2199a9f4d..0d9d968df 100644 --- a/secret/native/opts_test.go +++ b/secret/native/opts_test.go @@ -9,22 +9,21 @@ import ( "testing" "github.com/go-vela/server/database" - "github.com/go-vela/server/database/sqlite" ) func TestNative_ClientOpt_WithDatabase(t *testing.T) { // setup types - db, err := sqlite.NewTest() + db, err := database.NewTest() if err != nil { - t.Errorf("unable to create database service: %v", err) + t.Errorf("unable to create test database engine: %v", err) } - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + defer db.Close() // setup tests tests := []struct { failure bool - database database.Service - want database.Service + database database.Interface + want database.Interface }{ { failure: false, diff --git a/secret/native/update.go b/secret/native/update.go index 6cefa3f94..045b340c2 100644 --- a/secret/native/update.go +++ b/secret/native/update.go @@ -5,7 +5,7 @@ package native import ( - "strings" + "fmt" "github.com/go-vela/types/constants" "github.com/go-vela/types/library" @@ -13,54 +13,71 @@ import ( ) // Update updates an existing secret. -func (c *client) Update(sType, org, name string, s *library.Secret) error { - // create log fields from secret metadata - fields := logrus.Fields{ - "org": org, - "repo": name, - "secret": s.GetName(), - "type": sType, - } - - // check if secret is a shared secret - if strings.EqualFold(sType, constants.SecretShared) { - // update log fields from secret metadata - fields = logrus.Fields{ - "org": org, - "team": name, - "secret": s.GetName(), - "type": sType, - } - } - - // nolint: lll // ignore long line length due to parameters - c.Logger.WithFields(fields).Tracef("updating native %s secret %s for %s/%s", sType, s.GetName(), org, name) - +func (c *client) Update(sType, org, name string, s *library.Secret) (*library.Secret, error) { // capture the secret from the native service - sec, err := c.Database.GetSecret(sType, org, name, s.GetName()) + secret, err := c.Get(sType, org, name, s.GetName()) if err != nil { - return err + return nil, err } // update the events if set if len(s.GetEvents()) > 0 { - sec.SetEvents(s.GetEvents()) + secret.SetEvents(s.GetEvents()) } // update the images if set if s.Images != nil { - sec.SetImages(s.GetImages()) + secret.SetImages(s.GetImages()) } // update the value if set if len(s.GetValue()) > 0 { - sec.SetValue(s.GetValue()) + secret.SetValue(s.GetValue()) } // update allow_command if set if s.AllowCommand != nil { - sec.SetAllowCommand(s.GetAllowCommand()) + secret.SetAllowCommand(s.GetAllowCommand()) } - return c.Database.UpdateSecret(sec) + // update updated_at if set + secret.SetUpdatedAt(s.GetUpdatedAt()) + + // update updated_by if set + secret.SetUpdatedBy(s.GetUpdatedBy()) + + // handle the secret based off the type + switch sType { + case constants.SecretOrg: + c.Logger.WithFields(logrus.Fields{ + "org": org, + "secret": s.GetName(), + "type": sType, + }).Tracef("updating native %s secret %s for %s", sType, s.GetName(), org) + + // update the org secret in the native service + return c.Database.UpdateSecret(secret) + case constants.SecretRepo: + c.Logger.WithFields(logrus.Fields{ + "org": org, + "repo": name, + "secret": s.GetName(), + "type": sType, + }).Tracef("updating native %s secret %s for %s/%s", sType, s.GetName(), org, name) + + // update the repo secret in the native service + return c.Database.UpdateSecret(secret) + case constants.SecretShared: + c.Logger.WithFields(logrus.Fields{ + "org": org, + "team": name, + "secret": s.GetName(), + "type": sType, + }).Tracef("updating native %s secret %s for %s/%s", sType, s.GetName(), org, name) + + // update the shared secret in the native service + return c.Database.UpdateSecret(secret) + default: + return nil, fmt.Errorf("invalid secret type: %s", sType) + } } diff --git a/secret/native/update_test.go b/secret/native/update_test.go index 941df171f..66e637ad3 100644 --- a/secret/native/update_test.go +++ b/secret/native/update_test.go @@ -9,12 +9,28 @@ import ( "testing" "time" - "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/database" "github.com/go-vela/types/library" ) func TestNative_Update(t *testing.T) { // setup types + original := new(library.Secret) + original.SetID(1) + original.SetOrg("foo") + original.SetRepo("bar") + original.SetTeam("") + original.SetName("baz") + original.SetValue("secretValue") + original.SetType("repo") + original.SetImages([]string{"foo", "baz"}) + original.SetEvents([]string{"foob", "bar"}) + original.SetAllowCommand(true) + original.SetCreatedAt(1) + original.SetCreatedBy("user") + original.SetUpdatedAt(time.Now().UTC().Unix()) + original.SetUpdatedBy("user") + want := new(library.Secret) want.SetID(1) want.SetOrg("foo") @@ -32,15 +48,17 @@ func TestNative_Update(t *testing.T) { want.SetUpdatedBy("user2") // setup database - db, _ := sqlite.NewTest() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } defer func() { - db.Sqlite.Exec("delete from secrets;") - _sql, _ := db.Sqlite.DB() - _sql.Close() + db.DeleteSecret(original) + db.Close() }() - _ = db.CreateSecret(want) + _, _ = db.CreateSecret(original) // run test s, err := New( @@ -50,13 +68,11 @@ func TestNative_Update(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Update("repo", "foo", "bar", want) + got, err := s.Update("repo", "foo", "bar", want) if err != nil { t.Errorf("Update returned err: %v", err) } - got, _ := s.Get("repo", "foo", "bar", "baz") - if !reflect.DeepEqual(got, want) { t.Errorf("Update is %v, want %v", got, want) } @@ -69,8 +85,11 @@ func TestNative_Update_Invalid(t *testing.T) { sec.SetValue("foob") // setup database - db, _ := sqlite.NewTest() - defer func() { _sql, _ := db.Sqlite.DB(); _sql.Close() }() + db, err := database.NewTest() + if err != nil { + t.Errorf("unable to create test database engine: %v", err) + } + defer db.Close() // run test s, err := New( @@ -80,7 +99,7 @@ func TestNative_Update_Invalid(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Update("repo", "foo", "bar", sec) + _, err = s.Update("repo", "foo", "bar", sec) if err == nil { t.Errorf("Update should have returned err") } diff --git a/secret/secret.go b/secret/secret.go index eb39b07b1..edeec6c93 100644 --- a/secret/secret.go +++ b/secret/secret.go @@ -12,8 +12,6 @@ import ( "github.com/sirupsen/logrus" ) -// nolint: godot // top level comment ends in a list -// // New creates and returns a Vela service capable of // integrating with the configured secret provider. // @@ -21,6 +19,7 @@ import ( // // * Native // * Vault +// . func New(s *Setup) (Service, error) { // validate the setup being provided // diff --git a/secret/secret_test.go b/secret/secret_test.go index c6f806871..21ab33ee4 100644 --- a/secret/secret_test.go +++ b/secret/secret_test.go @@ -7,16 +7,16 @@ package secret import ( "testing" - "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/database" ) func TestSecret_New(t *testing.T) { // setup types - _database, err := sqlite.NewTest() + db, err := database.NewTest() if err != nil { - t.Errorf("unable to create database service: %v", err) + t.Errorf("unable to create test database engine: %v", err) } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() + defer db.Close() // setup tests tests := []struct { @@ -27,7 +27,7 @@ func TestSecret_New(t *testing.T) { failure: false, setup: &Setup{ Driver: "native", - Database: _database, + Database: db, }, }, { diff --git a/secret/service.go b/secret/service.go index 855ae1863..aacb9a0d3 100644 --- a/secret/service.go +++ b/secret/service.go @@ -22,9 +22,9 @@ type Service interface { // Count defines a function that counts a list of secrets. Count(string, string, string, []string) (int64, error) // Create defines a function that creates a new secret. - Create(string, string, string, *library.Secret) error + Create(string, string, string, *library.Secret) (*library.Secret, error) // Update defines a function that updates an existing secret. - Update(string, string, string, *library.Secret) error + Update(string, string, string, *library.Secret) (*library.Secret, error) // Delete defines a function that deletes a secret. Delete(string, string, string, string) error diff --git a/secret/setup.go b/secret/setup.go index cdebe406b..e0a28a8a8 100644 --- a/secret/setup.go +++ b/secret/setup.go @@ -13,7 +13,6 @@ import ( "github.com/go-vela/server/secret/native" "github.com/go-vela/server/secret/vault" "github.com/go-vela/types/constants" - "github.com/sirupsen/logrus" ) @@ -27,7 +26,7 @@ type Setup struct { Driver string // specifies the database service to use for the secret client - Database database.Service + Database database.Interface // specifies the address to use for the secret client Address string diff --git a/secret/setup_test.go b/secret/setup_test.go index 904e19059..ef4492681 100644 --- a/secret/setup_test.go +++ b/secret/setup_test.go @@ -8,20 +8,20 @@ import ( "reflect" "testing" - "github.com/go-vela/server/database/sqlite" + "github.com/go-vela/server/database" ) func TestSecret_Setup_Native(t *testing.T) { // setup types - _database, err := sqlite.NewTest() + db, err := database.NewTest() if err != nil { - t.Errorf("unable to create database service: %v", err) + t.Errorf("unable to create test database engine: %v", err) } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() + defer db.Close() _setup := &Setup{ Driver: "native", - Database: _database, + Database: db, } _native, err := _setup.Native() @@ -125,11 +125,11 @@ func TestSecret_Setup_Vault(t *testing.T) { func TestSecret_Setup_Validate(t *testing.T) { // setup types - _database, err := sqlite.NewTest() + db, err := database.NewTest() if err != nil { - t.Errorf("unable to create database service: %v", err) + t.Errorf("unable to create test database engine: %v", err) } - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() + defer db.Close() // setup tests tests := []struct { @@ -140,7 +140,7 @@ func TestSecret_Setup_Validate(t *testing.T) { failure: false, setup: &Setup{ Driver: "native", - Database: _database, + Database: db, }, }, { diff --git a/secret/vault/count.go b/secret/vault/count.go index 21373d861..44dbf2335 100644 --- a/secret/vault/count.go +++ b/secret/vault/count.go @@ -35,7 +35,7 @@ func (c *client) Count(sType, org, name string, _ []string) (i int64, err error) c.Logger.WithFields(fields).Tracef("counting vault %s secrets for %s/%s", sType, org, name) - // nolint: staticcheck // ignore false positive + //nolint:staticcheck // ignore false positive vault := new(api.Secret) count := 0 diff --git a/secret/vault/count_test.go b/secret/vault/count_test.go index b57d4ea1a..f5219b3e0 100644 --- a/secret/vault/count_test.go +++ b/secret/vault/count_test.go @@ -63,6 +63,7 @@ func TestVault_Count_Org(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -71,6 +72,7 @@ func TestVault_Count_Org(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -154,6 +156,7 @@ func TestVault_Count_Repo(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -162,6 +165,7 @@ func TestVault_Count_Repo(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -245,6 +249,7 @@ func TestVault_Count_Shared(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -253,6 +258,7 @@ func TestVault_Count_Shared(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -294,6 +300,7 @@ func TestVault_Count_InvalidType(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -302,6 +309,7 @@ func TestVault_Count_InvalidType(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -338,6 +346,7 @@ func TestVault_Count_ClosedServer(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -346,6 +355,7 @@ func TestVault_Count_ClosedServer(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -406,6 +416,7 @@ func TestVault_Count_EmptyList(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -414,6 +425,7 @@ func TestVault_Count_EmptyList(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -479,6 +491,7 @@ func TestVault_Count_InvalidList(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -487,6 +500,7 @@ func TestVault_Count_InvalidList(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( diff --git a/secret/vault/create.go b/secret/vault/create.go index 552a6fa55..ba34883ef 100644 --- a/secret/vault/create.go +++ b/secret/vault/create.go @@ -16,7 +16,7 @@ import ( ) // Create creates a new secret. -func (c *client) Create(sType, org, name string, s *library.Secret) error { +func (c *client) Create(sType, org, name string, s *library.Secret) (*library.Secret, error) { // create log fields from secret metadata fields := logrus.Fields{ "org": org, @@ -36,13 +36,12 @@ func (c *client) Create(sType, org, name string, s *library.Secret) error { } } - // nolint: lll // ignore long line length due to parameters c.Logger.WithFields(fields).Tracef("creating vault %s secret %s for %s/%s", sType, s.GetName(), org, name) // validate the secret err := database.SecretFromLibrary(s).Validate() if err != nil { - return err + return nil, err } // convert our secret to a Vault secret @@ -57,31 +56,31 @@ func (c *client) Create(sType, org, name string, s *library.Secret) error { case constants.SecretShared: return c.createShared(org, name, s.GetName(), vault.Data) default: - return fmt.Errorf("invalid secret type: %v", sType) + return nil, fmt.Errorf("invalid secret type: %v", sType) } } // createOrg is a helper function to create // the org secret for the provided path. -func (c *client) createOrg(org, path string, data map[string]interface{}) error { +func (c *client) createOrg(org, path string, data map[string]interface{}) (*library.Secret, error) { return c.create(fmt.Sprintf("%s/org/%s/%s", c.config.Prefix, org, path), data) } // createRepo is a helper function to create // the repo secret for the provided path. -func (c *client) createRepo(org, repo, path string, data map[string]interface{}) error { +func (c *client) createRepo(org, repo, path string, data map[string]interface{}) (*library.Secret, error) { return c.create(fmt.Sprintf("%s/repo/%s/%s/%s", c.config.Prefix, org, repo, path), data) } // createShared is a helper function to create // the shared secret for the provided path. -func (c *client) createShared(org, team, path string, data map[string]interface{}) error { +func (c *client) createShared(org, team, path string, data map[string]interface{}) (*library.Secret, error) { return c.create(fmt.Sprintf("%s/shared/%s/%s/%s", c.config.Prefix, org, team, path), data) } // create is a helper function to create // the secret for the provided path. -func (c *client) create(path string, data map[string]interface{}) error { +func (c *client) create(path string, data map[string]interface{}) (*library.Secret, error) { if strings.HasPrefix("secret/data", c.config.Prefix) { data = map[string]interface{}{ "data": data, @@ -89,10 +88,10 @@ func (c *client) create(path string, data map[string]interface{}) error { } // send API call to create the secret - _, err := c.Vault.Logical().Write(path, data) + s, err := c.Vault.Logical().Write(path, data) if err != nil { - return err + return nil, err } - return nil + return secretFromVault(s), nil } diff --git a/secret/vault/create_test.go b/secret/vault/create_test.go index 8ed0471b0..3213925f1 100644 --- a/secret/vault/create_test.go +++ b/secret/vault/create_test.go @@ -7,6 +7,7 @@ package vault import ( "net/http" "net/http/httptest" + "reflect" "testing" "github.com/go-vela/types/library" @@ -23,15 +24,21 @@ func TestVault_Create_Org(t *testing.T) { // setup mock server engine.PUT("/v1/secret/org/foo/bar", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v1/org.json") }) engine.PUT("/v1/secret/data/org/foo/bar", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/org.json") }) engine.PUT("/v1/secret/data/prefix/org/foo/bar", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/org.json") }) fake := httptest.NewServer(engine) @@ -41,18 +48,17 @@ func TestVault_Create_Org(t *testing.T) { sec := new(library.Secret) sec.SetOrg("foo") sec.SetRepo("*") - sec.SetTeam("") sec.SetName("bar") sec.SetValue("baz") sec.SetType("org") sec.SetImages([]string{"foo", "bar"}) sec.SetEvents([]string{"foo", "bar"}) - sec.SetAllowCommand(false) type args struct { version string prefix string } + tests := []struct { name string args args @@ -61,6 +67,7 @@ func TestVault_Create_Org(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -75,7 +82,7 @@ func TestVault_Create_Org(t *testing.T) { if err != nil { t.Errorf("New returned err: %v", err) } - err = s.Create("org", "foo", "*", sec) + got, err := s.Create("org", "foo", "*", sec) if resp.Code != http.StatusOK { t.Errorf("Create returned %v, want %v", resp.Code, http.StatusOK) @@ -84,6 +91,10 @@ func TestVault_Create_Org(t *testing.T) { if err != nil { t.Errorf("Create returned err: %v", err) } + + if !reflect.DeepEqual(got, sec) { + t.Errorf("Create returned %s, want %s", got, sec) + } }) } } @@ -97,15 +108,21 @@ func TestVault_Create_Repo(t *testing.T) { // setup mock server engine.PUT("/v1/secret/repo/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v1/repo.json") }) engine.PUT("/v1/secret/data/repo/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/repo.json") }) engine.PUT("/v1/secret/data/prefix/repo/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/repo.json") }) fake := httptest.NewServer(engine) @@ -115,18 +132,17 @@ func TestVault_Create_Repo(t *testing.T) { sec := new(library.Secret) sec.SetOrg("foo") sec.SetRepo("bar") - sec.SetTeam("") sec.SetName("baz") sec.SetValue("foob") sec.SetType("repo") sec.SetImages([]string{"foo", "bar"}) sec.SetEvents([]string{"foo", "bar"}) - sec.SetAllowCommand(false) type args struct { version string prefix string } + tests := []struct { name string args args @@ -135,6 +151,7 @@ func TestVault_Create_Repo(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -149,7 +166,8 @@ func TestVault_Create_Repo(t *testing.T) { if err != nil { t.Errorf("New returned err: %v", err) } - err = s.Create("repo", "foo", "bar", sec) + + got, err := s.Create("repo", "foo", "bar", sec) if resp.Code != http.StatusOK { t.Errorf("Create returned %v, want %v", resp.Code, http.StatusOK) @@ -158,6 +176,10 @@ func TestVault_Create_Repo(t *testing.T) { if err != nil { t.Errorf("Create returned err: %v", err) } + + if !reflect.DeepEqual(got, sec) { + t.Errorf("Create returned %s, want %s", got, sec) + } }) } } @@ -171,13 +193,21 @@ func TestVault_Create_Shared(t *testing.T) { // setup mock server engine.PUT("/v1/secret/shared/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v1/shared.json") }) + engine.PUT("/v1/secret/data/shared/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/shared.json") }) + engine.PUT("/v1/secret/data/prefix/shared/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/shared.json") }) fake := httptest.NewServer(engine) @@ -186,19 +216,18 @@ func TestVault_Create_Shared(t *testing.T) { // setup types sec := new(library.Secret) sec.SetOrg("foo") - sec.SetRepo("") sec.SetTeam("bar") sec.SetName("baz") sec.SetValue("foob") sec.SetType("shared") sec.SetImages([]string{"foo", "bar"}) sec.SetEvents([]string{"foo", "bar"}) - sec.SetAllowCommand(false) type args struct { version string prefix string } + tests := []struct { name string args args @@ -207,6 +236,7 @@ func TestVault_Create_Shared(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -221,7 +251,8 @@ func TestVault_Create_Shared(t *testing.T) { if err != nil { t.Errorf("New returned err: %v", err) } - err = s.Create("shared", "foo", "bar", sec) + + got, err := s.Create("shared", "foo", "bar", sec) if resp.Code != http.StatusOK { t.Errorf("Create returned %v, want %v", resp.Code, http.StatusOK) @@ -230,6 +261,10 @@ func TestVault_Create_Shared(t *testing.T) { if err != nil { t.Errorf("Create returned err: %v", err) } + + if !reflect.DeepEqual(got, sec) { + t.Errorf("Create returned %s, want %s", got, sec) + } }) } } @@ -273,6 +308,7 @@ func TestVault_Create_InvalidSecret(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -281,6 +317,7 @@ func TestVault_Create_InvalidSecret(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -295,7 +332,8 @@ func TestVault_Create_InvalidSecret(t *testing.T) { if err != nil { t.Errorf("New returned err: %v", err) } - err = s.Create("repo", "foo", "bar", sec) + + _, err = s.Create("repo", "foo", "bar", sec) if resp.Code != http.StatusOK { t.Errorf("Create returned %v, want %v", resp.Code, http.StatusOK) @@ -329,6 +367,7 @@ func TestVault_Create_InvalidType(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -337,6 +376,7 @@ func TestVault_Create_InvalidType(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -352,7 +392,7 @@ func TestVault_Create_InvalidType(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Create("invalid", "foo", "bar", sec) + _, err = s.Create("invalid", "foo", "bar", sec) if err == nil { t.Errorf("Create should have returned err") } @@ -381,6 +421,7 @@ func TestVault_Create_ClosedServer(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -389,6 +430,7 @@ func TestVault_Create_ClosedServer(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -404,7 +446,7 @@ func TestVault_Create_ClosedServer(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Create("repo", "foo", "bar", sec) + _, err = s.Create("repo", "foo", "bar", sec) if err == nil { t.Errorf("Create should have returned err") } diff --git a/secret/vault/delete_test.go b/secret/vault/delete_test.go index bdccaf416..57338820c 100644 --- a/secret/vault/delete_test.go +++ b/secret/vault/delete_test.go @@ -33,12 +33,14 @@ func TestVault_Delete_Org(t *testing.T) { }) fake := httptest.NewServer(engine) + defer fake.Close() type args struct { version string prefix string } + tests := []struct { name string args args @@ -47,6 +49,7 @@ func TestVault_Delete_Org(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -102,6 +105,7 @@ func TestVault_Delete_Repo(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -110,6 +114,7 @@ func TestVault_Delete_Repo(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -165,6 +170,7 @@ func TestVault_Delete_Shared(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -173,6 +179,7 @@ func TestVault_Delete_Shared(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -210,6 +217,7 @@ func TestVault_Delete_InvalidType(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -218,6 +226,7 @@ func TestVault_Delete_InvalidType(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -250,6 +259,7 @@ func TestVault_Delete_ClosedServer(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -258,6 +268,7 @@ func TestVault_Delete_ClosedServer(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( diff --git a/secret/vault/doc.go b/secret/vault/doc.go index 1fe491a49..12b1d8332 100644 --- a/secret/vault/doc.go +++ b/secret/vault/doc.go @@ -7,5 +7,5 @@ // // Usage: // -// import "github.com/go-vela/server/secret/vault" +// import "github.com/go-vela/server/secret/vault" package vault diff --git a/secret/vault/driver_test.go b/secret/vault/driver_test.go index 43e83e321..9e4589fe8 100644 --- a/secret/vault/driver_test.go +++ b/secret/vault/driver_test.go @@ -25,6 +25,7 @@ func TestVault_Driver(t *testing.T) { version string prefix string } + tests := []struct { name string args args diff --git a/secret/vault/get.go b/secret/vault/get.go index 486dec6fc..3612176a5 100644 --- a/secret/vault/get.go +++ b/secret/vault/get.go @@ -38,7 +38,7 @@ func (c *client) Get(sType, org, name, path string) (s *library.Secret, err erro c.Logger.WithFields(fields).Tracef("getting vault %s secret %s for %s/%s", sType, path, org, name) - // nolint: ineffassign,staticcheck // ignore false positive + //nolint:ineffassign,staticcheck // ignore false positive vault := new(api.Secret) // capture the secret from the Vault service @@ -75,7 +75,6 @@ func (c *client) getRepo(org, repo, path string) (*api.Secret, error) { // getShared is a helper function to capture // the shared secret for the provided path. func (c *client) getShared(org, team, path string) (*api.Secret, error) { - // nolint: lll // ignore long line length due to parameters return c.get(fmt.Sprintf("%s/%s/%s/%s/%s", c.config.Prefix, constants.SecretShared, org, team, path)) } diff --git a/secret/vault/get_test.go b/secret/vault/get_test.go index 024644990..624cbb07a 100644 --- a/secret/vault/get_test.go +++ b/secret/vault/get_test.go @@ -58,6 +58,7 @@ func TestVault_Get_Org(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -66,6 +67,7 @@ func TestVault_Get_Org(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -140,6 +142,7 @@ func TestVault_Get_Repo(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -148,6 +151,7 @@ func TestVault_Get_Repo(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -222,6 +226,7 @@ func TestVault_Get_Shared(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -230,6 +235,7 @@ func TestVault_Get_Shared(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -270,6 +276,7 @@ func TestVault_Get_InvalidType(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -278,6 +285,7 @@ func TestVault_Get_InvalidType(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -313,6 +321,7 @@ func TestVault_Get_ClosedServer(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -321,6 +330,7 @@ func TestVault_Get_ClosedServer(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( diff --git a/secret/vault/list.go b/secret/vault/list.go index 45172f548..00b64db29 100644 --- a/secret/vault/list.go +++ b/secret/vault/list.go @@ -43,7 +43,7 @@ func (c *client) List(sType, org, name string, _, _ int, _ []string) ([]*library var err error s := []*library.Secret{} - // nolint: staticcheck // ignore false positive + //nolint:staticcheck // ignore false positive vault := new(api.Secret) // capture the list of secrets from the Vault service diff --git a/secret/vault/list_test.go b/secret/vault/list_test.go index 3323948f8..bb7f538a4 100644 --- a/secret/vault/list_test.go +++ b/secret/vault/list_test.go @@ -75,6 +75,7 @@ func TestVault_List_Org(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -83,6 +84,7 @@ func TestVault_List_Org(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -204,6 +206,7 @@ func TestVault_List_Repo(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -212,6 +215,7 @@ func TestVault_List_Repo(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -318,6 +322,7 @@ func TestVault_List_Shared(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -326,6 +331,7 @@ func TestVault_List_Shared(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -366,6 +372,7 @@ func TestVault_List_InvalidType(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -374,6 +381,7 @@ func TestVault_List_InvalidType(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -463,6 +471,7 @@ func TestVault_List_EmptyList(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -471,6 +480,7 @@ func TestVault_List_EmptyList(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -536,6 +546,7 @@ func TestVault_List_InvalidList(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -544,6 +555,7 @@ func TestVault_List_InvalidList(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -618,6 +630,7 @@ func TestVault_List_NoRead(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -626,6 +639,7 @@ func TestVault_List_NoRead(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( diff --git a/secret/vault/refresh.go b/secret/vault/refresh.go index 330255a78..461d49fe8 100644 --- a/secret/vault/refresh.go +++ b/secret/vault/refresh.go @@ -1,10 +1,14 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + package vault import ( "encoding/base64" "encoding/json" "fmt" - "io/ioutil" + "io" "time" "github.com/aws/aws-sdk-go/aws" @@ -64,6 +68,7 @@ func (c *client) getAwsToken() (string, time.Duration, error) { } c.Logger.Trace("getting AWS token from vault") + secret, err := c.Vault.Logical().Write("auth/aws/login", headers) if err != nil { return "", 0, err @@ -97,14 +102,14 @@ func (c *client) generateAwsAuthHeader() (map[string]interface{}, error) { } // read the STS request body - requestBody, err := ioutil.ReadAll(req.Body) + requestBody, err := io.ReadAll(req.Body) if err != nil { return nil, err } // construct the vault STS auth header // - // nolint: lll // ignore long line length due to variable names + loginData := map[string]interface{}{ "role": c.AWS.Role, "iam_http_request_method": req.HTTPRequest.Method, diff --git a/secret/vault/refresh_test.go b/secret/vault/refresh_test.go index 60b5b4dca..33b41fa35 100644 --- a/secret/vault/refresh_test.go +++ b/secret/vault/refresh_test.go @@ -1,11 +1,15 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + package vault import ( "fmt" - "io/ioutil" "net/http" "net/http/httptest" "net/url" + "os" "strings" "testing" "time" @@ -41,7 +45,7 @@ func Test_client_initialize(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - data, err := ioutil.ReadFile(fmt.Sprintf("testdata/refresh/%s", tt.responseFile)) + data, err := os.ReadFile(fmt.Sprintf("testdata/refresh/%s", tt.responseFile)) if err != nil { t.Error(err) } @@ -197,7 +201,7 @@ func Test_client_getAwsToken(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(*testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - data, err := ioutil.ReadFile(fmt.Sprintf("testdata/refresh/%s", tt.responseFile)) + data, err := os.ReadFile(fmt.Sprintf("testdata/refresh/%s", tt.responseFile)) if err != nil { t.Error(err) } diff --git a/secret/vault/update.go b/secret/vault/update.go index ce2d53b57..453d15e3e 100644 --- a/secret/vault/update.go +++ b/secret/vault/update.go @@ -16,7 +16,7 @@ import ( ) // Update updates a secret. -func (c *client) Update(sType, org, name string, s *library.Secret) error { +func (c *client) Update(sType, org, name string, s *library.Secret) (*library.Secret, error) { // create log fields from secret metadata fields := logrus.Fields{ "org": org, @@ -36,13 +36,12 @@ func (c *client) Update(sType, org, name string, s *library.Secret) error { } } - // nolint: lll // ignore long line length due to parameters c.Logger.WithFields(fields).Tracef("updating vault %s secret %s for %s/%s", sType, s.GetName(), org, name) // capture the secret from the Vault service sec, err := c.Get(sType, org, name, s.GetName()) if err != nil { - return err + return nil, err } // convert the Vault secret our secret @@ -66,7 +65,7 @@ func (c *client) Update(sType, org, name string, s *library.Secret) error { // validate the secret err = database.SecretFromLibrary(secretFromVault(vault)).Validate() if err != nil { - return err + return nil, err } // update the secret for the Vault service @@ -84,37 +83,35 @@ func (c *client) Update(sType, org, name string, s *library.Secret) error { // updateOrg is a helper function to update // the org secret for the provided path. -func (c *client) updateOrg(org, path string, data map[string]interface{}) error { +func (c *client) updateOrg(org, path string, data map[string]interface{}) (*library.Secret, error) { return c.update(fmt.Sprintf("%s/%s/%s/%s", c.config.Prefix, constants.SecretOrg, org, path), data) } // updateRepo is a helper function to update // the repo secret for the provided path. -func (c *client) updateRepo(org, repo, path string, data map[string]interface{}) error { - // nolint: lll // ignore long line length due to variable names +func (c *client) updateRepo(org, repo, path string, data map[string]interface{}) (*library.Secret, error) { return c.update(fmt.Sprintf("%s/%s/%s/%s/%s", c.config.Prefix, constants.SecretRepo, org, repo, path), data) } // updateShared is a helper function to update // the shared secret for the provided path. -func (c *client) updateShared(org, team, path string, data map[string]interface{}) error { - // nolint: lll // ignore long line length due to variable names +func (c *client) updateShared(org, team, path string, data map[string]interface{}) (*library.Secret, error) { return c.update(fmt.Sprintf("%s/%s/%s/%s/%s", c.config.Prefix, constants.SecretShared, org, team, path), data) } // update is a helper function to update // the secret for the provided path. -func (c *client) update(path string, data map[string]interface{}) error { +func (c *client) update(path string, data map[string]interface{}) (*library.Secret, error) { if strings.HasPrefix("secret/data", c.config.Prefix) { data = map[string]interface{}{ "data": data, } } - _, err := c.Vault.Logical().Write(path, data) + s, err := c.Vault.Logical().Write(path, data) if err != nil { - return err + return nil, err } - return nil + return secretFromVault(s), nil } diff --git a/secret/vault/update_test.go b/secret/vault/update_test.go index 785c87dd2..49e04a1b2 100644 --- a/secret/vault/update_test.go +++ b/secret/vault/update_test.go @@ -7,6 +7,7 @@ package vault import ( "net/http" "net/http/httptest" + "reflect" "testing" "github.com/go-vela/types/library" @@ -22,31 +23,37 @@ func TestVault_Update_Org(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/v1/secret/org/foo/bar", func(c *gin.Context) { + engine.PUT("/v1/secret/org/foo/bar", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v1/org.json") }) - engine.PUT("/v1/secret/org/foo/bar", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + engine.GET("/v1/secret/org/foo/bar", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v1/org.json") }) - engine.GET("/v1/secret/data/org/foo/bar", func(c *gin.Context) { + engine.PUT("/v1/secret/data/org/foo/bar", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v2/org.json") }) - engine.PUT("/v1/secret/data/org/foo/bar", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + engine.GET("/v1/secret/data/org/foo/bar", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/org.json") }) - engine.GET("/v1/secret/data/prefix/org/foo/bar", func(c *gin.Context) { + engine.PUT("/v1/secret/data/prefix/org/foo/bar", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v2/org.json") }) - engine.PUT("/v1/secret/data/prefix/org/foo/bar", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + engine.GET("/v1/secret/data/prefix/org/foo/bar", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/org.json") }) fake := httptest.NewServer(engine) @@ -56,18 +63,17 @@ func TestVault_Update_Org(t *testing.T) { sec := new(library.Secret) sec.SetOrg("foo") sec.SetRepo("*") - sec.SetTeam("") sec.SetName("bar") sec.SetValue("baz") sec.SetType("org") sec.SetImages([]string{"foo", "bar"}) sec.SetEvents([]string{"foo", "bar"}) - sec.SetAllowCommand(false) type args struct { version string prefix string } + tests := []struct { name string args args @@ -76,6 +82,7 @@ func TestVault_Update_Org(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -91,7 +98,7 @@ func TestVault_Update_Org(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Update("org", "foo", "*", sec) + got, err := s.Update("org", "foo", "*", sec) if resp.Code != http.StatusOK { t.Errorf("Update returned %v, want %v", resp.Code, http.StatusOK) @@ -100,6 +107,10 @@ func TestVault_Update_Org(t *testing.T) { if err != nil { t.Errorf("Update returned err: %v", err) } + + if !reflect.DeepEqual(got, sec) { + t.Errorf("Update returned %s, want %s", got, sec) + } }) } } @@ -112,31 +123,37 @@ func TestVault_Update_Repo(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/v1/secret/repo/foo/bar/baz", func(c *gin.Context) { + engine.PUT("/v1/secret/repo/foo/bar/baz", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v1/repo.json") }) - engine.PUT("/v1/secret/repo/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + engine.GET("/v1/secret/repo/foo/bar/baz", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v1/repo.json") }) - engine.GET("/v1/secret/data/repo/foo/bar/baz", func(c *gin.Context) { + engine.PUT("/v1/secret/data/repo/foo/bar/baz", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v2/repo.json") }) - engine.PUT("/v1/secret/data/repo/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + engine.GET("/v1/secret/data/repo/foo/bar/baz", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/repo.json") }) - engine.GET("/v1/secret/data/prefix/repo/foo/bar/baz", func(c *gin.Context) { + engine.PUT("/v1/secret/data/prefix/repo/foo/bar/baz", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v2/repo.json") }) - engine.PUT("/v1/secret/data/prefix/repo/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + engine.GET("/v1/secret/data/prefix/repo/foo/bar/baz", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/repo.json") }) fake := httptest.NewServer(engine) @@ -151,12 +168,12 @@ func TestVault_Update_Repo(t *testing.T) { sec.SetType("repo") sec.SetImages([]string{"foo", "bar"}) sec.SetEvents([]string{"foo", "bar"}) - sec.SetAllowCommand(false) type args struct { version string prefix string } + tests := []struct { name string args args @@ -165,6 +182,7 @@ func TestVault_Update_Repo(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -180,7 +198,7 @@ func TestVault_Update_Repo(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Update("repo", "foo", "bar", sec) + got, err := s.Update("repo", "foo", "bar", sec) if resp.Code != http.StatusOK { t.Errorf("Update returned %v, want %v", resp.Code, http.StatusOK) @@ -189,6 +207,10 @@ func TestVault_Update_Repo(t *testing.T) { if err != nil { t.Errorf("Update returned err: %v", err) } + + if !reflect.DeepEqual(got, sec) { + t.Errorf("Update returned %s, want %s", got, sec) + } }) } } @@ -201,31 +223,37 @@ func TestVault_Update_Shared(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/v1/secret/shared/foo/bar/baz", func(c *gin.Context) { + engine.PUT("/v1/secret/shared/foo/bar/baz", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v1/shared.json") }) - engine.PUT("/v1/secret/shared/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + engine.GET("/v1/secret/shared/foo/bar/baz", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v1/shared.json") }) - engine.GET("/v1/secret/data/shared/foo/bar/baz", func(c *gin.Context) { + engine.PUT("/v1/secret/data/shared/foo/bar/baz", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v2/shared.json") }) - engine.PUT("/v1/secret/data/shared/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + engine.GET("/v1/secret/data/shared/foo/bar/baz", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/shared.json") }) - engine.GET("/v1/secret/data/prefix/shared/foo/bar/baz", func(c *gin.Context) { + engine.PUT("/v1/secret/data/prefix/shared/foo/bar/baz", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v2/shared.json") }) - engine.PUT("/v1/secret/data/prefix/shared/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") + engine.GET("/v1/secret/data/prefix/shared/foo/bar/baz", func(c *gin.Context) { + c.Header("Content-Type", "application/json") + c.Status(http.StatusOK) + c.File("testdata/v2/shared.json") }) fake := httptest.NewServer(engine) @@ -240,12 +268,12 @@ func TestVault_Update_Shared(t *testing.T) { sec.SetType("shared") sec.SetImages([]string{"foo", "bar"}) sec.SetEvents([]string{"foo", "bar"}) - sec.SetAllowCommand(false) type args struct { version string prefix string } + tests := []struct { name string args args @@ -254,6 +282,7 @@ func TestVault_Update_Shared(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -269,7 +298,7 @@ func TestVault_Update_Shared(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Update("shared", "foo", "bar", sec) + got, err := s.Update("shared", "foo", "bar", sec) if resp.Code != http.StatusOK { t.Errorf("Update returned %v, want %v", resp.Code, http.StatusOK) @@ -278,6 +307,10 @@ func TestVault_Update_Shared(t *testing.T) { if err != nil { t.Errorf("Update returned err: %v", err) } + + if !reflect.DeepEqual(got, sec) { + t.Errorf("Update returned %s, want %s", got, sec) + } }) } } @@ -290,32 +323,23 @@ func TestVault_Update_InvalidSecret(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/v1/secret/repo/foo/bar/baz", func(c *gin.Context) { + engine.PUT("/v1/secret/repo/foo/bar/baz", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v1/invalid_repo.json") }) - engine.PUT("/v1/secret/repo/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") - }) - engine.GET("/v1/secret/data/repo/foo/bar/baz", func(c *gin.Context) { + engine.PUT("/v1/secret/data/repo/foo/bar/baz", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v2/invalid_repo.json") }) - engine.PUT("/v1/secret/data/repo/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") - }) - engine.GET("/v1/secret/data/prefix/repo/foo/bar/baz", func(c *gin.Context) { + engine.PUT("/v1/secret/data/prefix/repo/foo/bar/baz", func(c *gin.Context) { c.Header("Content-Type", "application/json") c.Status(http.StatusOK) c.File("testdata/v2/invalid_repo.json") }) - engine.PUT("/v1/secret/data/prefix/repo/foo/bar/baz", func(c *gin.Context) { - c.String(http.StatusNoContent, "") - }) fake := httptest.NewServer(engine) defer fake.Close() @@ -335,6 +359,7 @@ func TestVault_Update_InvalidSecret(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -343,6 +368,7 @@ func TestVault_Update_InvalidSecret(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -358,7 +384,7 @@ func TestVault_Update_InvalidSecret(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Update("repo", "foo", "bar", sec) + _, err = s.Update("repo", "foo", "bar", sec) if resp.Code != http.StatusOK { t.Errorf("Update returned %v, want %v", resp.Code, http.StatusOK) @@ -390,6 +416,7 @@ func TestVault_Update_InvalidType(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -398,6 +425,7 @@ func TestVault_Update_InvalidType(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -413,7 +441,7 @@ func TestVault_Update_InvalidType(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Update("invalid", "foo", "bar", sec) + _, err = s.Update("invalid", "foo", "bar", sec) if err == nil { t.Errorf("Update should have returned err") } @@ -440,6 +468,7 @@ func TestVault_Update_ClosedServer(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -448,6 +477,7 @@ func TestVault_Update_ClosedServer(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -463,7 +493,7 @@ func TestVault_Update_ClosedServer(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Update("repo", "foo", "bar", sec) + _, err = s.Update("repo", "foo", "bar", sec) if err == nil { t.Errorf("Update should have returned err") } @@ -479,29 +509,14 @@ func TestVault_Update_NoWrite(t *testing.T) { _, engine := gin.CreateTestContext(resp) // setup mock server - engine.GET("/v1/secret/repo/foo/bar/baz", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/v1/repo.json") - }) engine.PUT("/v1/secret/repo/foo/bar/baz", func(c *gin.Context) { c.Status(http.StatusNotFound) }) - engine.GET("/v1/secret/data/repo/foo/bar/baz", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/v2/repo.json") - }) engine.PUT("/v1/secret/data/repo/foo/bar/baz", func(c *gin.Context) { c.Status(http.StatusNotFound) }) - engine.GET("/v1/secret/data/prefix/repo/foo/bar/baz", func(c *gin.Context) { - c.Header("Content-Type", "application/json") - c.Status(http.StatusOK) - c.File("testdata/v2/repo.json") - }) engine.PUT("/v1/secret/data/prefix/repo/foo/bar/baz", func(c *gin.Context) { c.Status(http.StatusNotFound) }) @@ -523,6 +538,7 @@ func TestVault_Update_NoWrite(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -531,6 +547,7 @@ func TestVault_Update_NoWrite(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -546,7 +563,7 @@ func TestVault_Update_NoWrite(t *testing.T) { t.Errorf("New returned err: %v", err) } - err = s.Update("repo", "foo", "bar", sec) + _, err = s.Update("repo", "foo", "bar", sec) if resp.Code != http.StatusOK { t.Errorf("Update returned %v, want %v", resp.Code, http.StatusOK) diff --git a/secret/vault/vault.go b/secret/vault/vault.go index f49755737..997ff46f5 100644 --- a/secret/vault/vault.go +++ b/secret/vault/vault.go @@ -58,7 +58,7 @@ type ( // New returns a Secret implementation that integrates with a Vault secrets engine. // -// nolint: revive // ignore returning unexported client +//nolint:revive // ignore returning unexported client func New(opts ...ClientOpt) (*client, error) { // create new Vault client c := new(client) @@ -132,7 +132,7 @@ func New(opts ...ClientOpt) (*client, error) { // secretFromVault is a helper function to convert a HashiCorp Vault secret to a Vela secret. // -// nolint: gocyclo,funlen // ignore cyclomatic complexity and function length due to conditionals +//nolint:gocyclo,funlen // ignore cyclomatic complexity and function length due to conditionals func secretFromVault(vault *api.Secret) *library.Secret { s := new(library.Secret) diff --git a/secret/vault/vault_test.go b/secret/vault/vault_test.go index d7d62e6ac..7ba0ac254 100644 --- a/secret/vault/vault_test.go +++ b/secret/vault/vault_test.go @@ -23,6 +23,7 @@ func TestVault_New(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -31,6 +32,7 @@ func TestVault_New(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -58,6 +60,7 @@ func TestVault_New_Error(t *testing.T) { version string prefix string } + tests := []struct { name string args args @@ -66,6 +69,7 @@ func TestVault_New_Error(t *testing.T) { {"v2", args{version: "2", prefix: ""}}, {"v2 with prefix", args{version: "2", prefix: "prefix"}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s, err := New( @@ -146,6 +150,7 @@ func TestVault_secretFromVault(t *testing.T) { type args struct { secret *api.Secret } + tests := []struct { name string args args @@ -153,6 +158,7 @@ func TestVault_secretFromVault(t *testing.T) { {"v1", args{secret: inputV1}}, {"v2", args{secret: inputV2}}, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := secretFromVault(tt.args.secret) diff --git a/util/util.go b/util/util.go index 6e7813807..31cdd1104 100644 --- a/util/util.go +++ b/util/util.go @@ -5,15 +5,19 @@ package util import ( - "github.com/gin-gonic/gin" + "html" + "strings" + + "github.com/go-vela/types/library" + "github.com/gin-gonic/gin" "github.com/go-vela/types" ) // HandleError appends the error to the handler chain for logging and outputs it. func HandleError(c *gin.Context, status int, err error) { msg := err.Error() - // nolint: errcheck // ignore checking error + //nolint:errcheck // ignore checking error c.Error(err) c.AbortWithStatusJSON(status, types.Error{Message: &msg}) } @@ -41,3 +45,88 @@ func MinInt(a, b int) int { return b } + +// FormParameter safely captures a form parameter from the context +// by removing any new lines and HTML escaping the value. +func FormParameter(c *gin.Context, parameter string) string { + return EscapeValue(c.Request.FormValue(parameter)) +} + +// QueryParameter safely captures a query parameter from the context +// by removing any new lines and HTML escaping the value. +func QueryParameter(c *gin.Context, parameter, value string) string { + return EscapeValue(c.DefaultQuery(parameter, value)) +} + +// PathParameter safely captures a path parameter from the context +// by removing any new lines and HTML escaping the value. +func PathParameter(c *gin.Context, parameter string) string { + return EscapeValue(c.Param(parameter)) +} + +// SplitFullName safely splits the repo.FullName field into an org and name. +func SplitFullName(value string) (string, string) { + // split repo full name into org and repo + repoSlice := strings.Split(value, "/") + if len(repoSlice) != 2 { + return "", "" + } + + org := repoSlice[0] + repo := repoSlice[1] + + return org, repo +} + +// EscapeValue safely escapes any string by removing any new lines and HTML escaping it. +func EscapeValue(value string) string { + // replace all new lines in the value + escaped := strings.Replace(strings.Replace(value, "\n", "", -1), "\r", "", -1) + + // HTML escape the new line escaped value + return html.EscapeString(escaped) +} + +// Unique is a helper function that takes a slice and +// validates that there are no duplicate entries. +func Unique(stringSlice []string) []string { + keys := make(map[string]bool) + list := []string{} + + for _, entry := range stringSlice { + if _, value := keys[entry]; !value { + keys[entry] = true + + list = append(list, entry) + } + } + + return list +} + +// CheckAllowlist is a helper function to ensure only repos in the +// allowlist are specified. +// +// a single entry of '*' allows any repo to be enabled. +func CheckAllowlist(r *library.Repo, allowlist []string) bool { + // check if all repos are allowed to be enabled + if len(allowlist) == 1 && allowlist[0] == "*" { + return true + } + + for _, repo := range allowlist { + // allow all repos in org + if strings.Contains(repo, "/*") { + if strings.HasPrefix(repo, r.GetOrg()) { + return true + } + } + + // allow specific repo within org + if repo == r.GetFullName() { + return true + } + } + + return false +} diff --git a/version/version.go b/version/version.go index 3368f7a64..30312ee24 100644 --- a/version/version.go +++ b/version/version.go @@ -34,7 +34,7 @@ var ( func New() *version.Version { // check if a semantic tag was provided if len(Tag) == 0 { - logrus.Warningf("no semantic tag provided - defaulting to v0.0.0") + logrus.Warning("no semantic tag provided - defaulting to v0.0.0") // set a fallback default for the tag Tag = "v0.0.0" @@ -42,7 +42,7 @@ func New() *version.Version { v, err := semver.NewVersion(Tag) if err != nil { - fmt.Println(fmt.Errorf("unable to parse semantic version for %s: %v", Tag, err)) + fmt.Println(fmt.Errorf("unable to parse semantic version for %s: %w", Tag, err)) } return &version.Version{